Redis系列我写到第三篇了,这篇标题是 redis-3-Hash-List,按计划把 Hash 和 List 两种数据类型一次讲透。前两篇已经铺垫过基础概念和 String 类型,但说实话,项目里真正让新人懵的往往不是 String,而是 Hash 和 List:都是 Spring 里一个 opsForXxx,一个用来存对象一个用来做队列,到底谁是谁,什么时候用哪个,底层又是什么结构,很多人是一团浆糊的。
这篇文章我不打算按官方文档的目录平铺,而是按我实际干活的经验来:先讲选型判断,再逐个拆 Hash 和 List 的底层与命令,然后给一套从安装到 Spring Boot 整合的完整实操路径,最后把面试题和缓存治理里最容易踩的坑统一收拾一遍。适合三类人:刚学 Redis 不知道数据类型怎么用的新手、准备面试想补底层细节的同学、以及在做缓存设计时纠结该用 Hash 还是 List 的后端开发。
1. 先把选型想清楚:Hash与List在数据类型版图里的位置
1.1 Redis数据类型全景速览
Redis 对外暴露的数据类型,官方口径是 5 种:String、Hash、List、Set、ZSet(有序集合)。后面又陆续加了 Bitmap(本质是 String 上的位图操作)、HyperLogLog(基数估算,同样挂在 String 语义下)、Geo(地理坐标,底层基于 ZSet 实现)、Stream(5.0 开始的消息队列专用结构)。
所以面试的时候别只说"Redis 有五种数据类型",能提到 Stream 和 Geo,说明你对新版本有跟进,这是第一层加分项。
我自己的分类习惯是:String 解决"一个值怎么存",Hash 解决"一个对象怎么存",List 解决"一串有序的数据怎么存",Set 解决"去重和集合运算",ZSet 解决"带分数的排序场景"。这篇只展开 Hash 和 List,但你在心里要有这张大图,不然很容易出现"两个类型都能实现需求,但我选错了"的情况。
1.2 两种结构,两种思维模型
Hash 和 List 的定位差异,我用大白话总结:
Hash 是一个扁平的小型数据库表。一个 key 对应一张表,field 是行主键,value 是字段值。
List 是一条可以在两端操作的有序队列。可以从左边放,右边取,也能按区间截取。
生活化类比:Hash 像一个带抽屉的文件柜。你打开一个柜子(key),里面有多个抽屉(field),每个抽屉里放一个小物件(value)。你只想改其中一个抽屉的时候,完全不需要把整个柜子搬出来。List 则像一条传送带:工人从左侧放货(LPUSH)、右侧取货(RPOP),需要时还能从中间抽查某一节(LINDEX/LRANGE)。
这两个类比几乎能覆盖 90% 的选型场景:对象字段经常部分更新 → Hash;数据有先后顺序、要按队列消费 → List。
1.3 接到需求,怎么判断用哪种
我一般用下面这张表快速做决策:
| 你的需求 | 推荐类型 | 为什么 |
|---|---|---|
| 缓存一个对象的多个字段,经常只改其中几个 | Hash | 字段级更新,避免整体读写 |
| 需要按顺序保存、消费消息或任务 | List | 两端操作 O(1),还支持阻塞读取 |
| 只需要缓存一个字符串或数字 | String | 命令最少,语义最清晰 |
| 需要去重、交集并集 | Set | 专门的集合运算能力 |
| 需要按分数排序、取 TopN | ZSet | 跳表加分数的天然排序 |
举个例子。产品提了个需求:商品详情页展示标题、销量、库存,运营要能单独改标题,销量要实时递增。
如果你图省事用 String 存整个 JSON,改标题就得读出来反序列化、改字段、再序列化写回去,并发下还容易互相覆盖。换成 Hash 就很自然:HSET 改标题,HINCRBY 加销量,都是单字段操作。这就是 Hash 在缓存场景里最值钱的地方。
2. Hash类型:底层结构、核心命令与实战用法
2.1 底层结构:小体积的listpack,大字段的hashtable
Hash 底层不是一上来就上 hashtable 的,Redis 为了省内存做了很经典的两级设计。
数据量小、字段值也小的时候,Hash 用一段连续内存把 field 和 value 依次排布,老版本叫 ziplist,Redis 7.0 之后换成了 listpack。这两种结构本质上都是"紧凑排列的字节数组",好处是几乎不浪费内存;坏处是查找需要线性扫描,字段多了会变慢。
当字段数超过阈值,或者某个 value 的长度超过阈值,Redis 就会把整个 Hash 从紧凑结构转换为真正的 hashtable:数组加链表,读写变成 O(1),但每个节点都有额外的指针元数据,内存开销变大。
这里面有一个很多人背错的点:转换阈值在不同版本里不一样。以我常用的配置为例:
| Redis 版本 | 小数据量编码 | 字段数阈值配置 | 字段值阈值配置 |
|---|---|---|---|
| 6.x 及以前 | ziplist | hash-max-ziplist-entries(默认 512) | hash-max-ziplist-value(默认 64) |
| 7.x | listpack | hash-max-listpack-entries(默认 128) | hash-max-listpack-value(默认 64 bytes) |
注意,7.x 把字段数阈值从 512 改成了 128。很多人拿着旧文档背"512",到了新版本就露馅。最靠谱的做法是连上你自己的实例,执行CONFIG GET hash-max-*看一眼真实值,别凭记忆。
2.2 核心命令对照表与参数语义
Hash 的命令不多,但细节坑不少。我做成速查表:
| 命令 | 作用 | 注意点 |
|---|---|---|
| HSET key field value [field value ...] | 设置一个或多个字段 | 一次设多个字段,减少 RTT |
| HGET key field | 获取单个字段 | 字段不存在返回 nil |
| HMSET/HMGET | 批量设置/获取 | 4.0 后 HSET 已支持多字段,HMSET 基本可弃用 |
| HGETALL key | 返回所有 field 和 value | 字段多时是大坑,别在生产大规模用 |
| HDEL key field ... | 删除字段 | 支持一次删多个 |
| HEXISTS key field | 判断字段是否存在 | 常用于幂等判断 |
| HINCRBY key field increment | 字段值自增 | 字段必须是整数 |
| HINCRBYFLOAT key field increment | 浮点自增 | 用于金额、评分等 |
| HKEYS/HVALS | 取所有字段 / 所有值 | 同样注意大 Hash 场景 |
| HLEN key | 取字段数量 | O(1) |
| HSCAN key cursor | 增量遍历 | 遍历大 Hash 只推荐这个方法 |
我自己实际开发中最常用的组合是 HSET + HGET + HINCRBY + HSCAN。HSET 和 HGET 解决读写,HINCRBY 解决计数器类需求,HSCAN 用于排查或者数据迁移。
2.3 实战场景:对象信息缓存与购物车
最典型的场景是用户信息缓存。假设 key 是user:1001,里面存 name、age、email、vipLevel 这些字段。用户改昵称的时候:
HSET user:1001 name zhangsan不用像 String 方案那样先把整个对象读出来再写回去。这里有个容易被忽略的好处:String+JSON 的写操作天然是"读-改-写"三步,在高并发下如果没有加锁,两个请求同时改不同字段也会互相覆盖;Hash 字段级操作是原子的,改 name 不影响 email。
购物车也是 Hash 的高频场景。key 用cart:{userId},field 用 SKU ID,value 用数量:
HSET cart:1001 sku_001 2 HINCRBY cart:1001 sku_001 1加购就是 HINCRBY 加一,改数量就是 HSET,删一项就是 HDEL。字段天然按商品维度隔离,不需要额外设计。
还有一类用法容易被忽略:把 Hash 当配置中心用。我做过一个活动页配置缓存,一个 key 装整个页面的配置,每个配置项一个 field。运营改一个按钮文案,直接 HSET 改一个字段,比整个 JSON 覆盖快得多,也安全得多。
2.4 内存优势、字段过期与Big Key警告
Hash 的内存优势在字段值都很小的时候非常明显。官方博客给过一个经典的对比案例:100 万个字段放一个 Hash 里,比拆成 100 万个独立的 String key 能省下非常可观的量级,具体数值会随字段长度浮动,但数量级的差距是真实的。原因就是小 Hash 用 listpack 这种连续内存结构,几乎没有额外指针开销;而 100 万个 String key 光 key 本身的 dict 结构和元数据就是很大一笔开销。
但 Hash 也不是没有坑,主要有三个:
第一,没有字段级过期。Redis 7.4 之前,Hash 的字段不能单独设 TTL,整个 key 过期是全量的。社区一直呼吁字段级过期,Redis 7.4 才开始有实验性的 field TTL 能力,生产环境不建议依赖。替代方案是:把过期时间塞进 value 里,读取时自己判断;或者老老实实整个 key 过期后重建。
第二,字段数量膨胀会变成 Big Key。一个 Hash 字段数几万、几十万甚至上百万,就是标准的 Big Key。这时候执行 HGETALL 会让 Redis 在事件循环里长时间阻塞,其他请求全部排队,引发连锁超时。凡是看到 HGETALL、HKEYS、HVALS 这类一次性返回全量数据的命令,都要先确认这个 Hash 到底有多大。
第三,切换了编码之后内存优势会缩水。字段数超过阈值变成 hashtable 后,指针开销上来了,这时候 Hash 相比独立 String key 的省内存优势会变小,但字段级更新的功能优势还在。
3. List类型:底层原理、命令细节与消息队列玩法
3.1 底层结构:quicklist的折中设计
List 的底层在 Redis 3.2 之后统一为 quicklist。quicklist 是双向链表和压缩列表的混合体:整体是一个双向链表,但每个节点内部是一小块连续内存,老版本是 ziplist,7.0 后换成 listpack。
为什么这么设计?如果只用简单的双向链表,每个节点单独 malloc,内存碎片和指针开销很大,而且节点在内存里分散,缓存命中率很低。如果只用数组,两端插入删除要整体搬移数据,复杂度 O(N)。quicklist 的思路是:把元素分成一节一节的"车厢",每节车厢内部尽量装紧凑,车厢之间用指针挂起来。
我常打的比方是火车。每节车厢内部是装得整整齐齐的集装箱(listpack),车厢之间靠挂钩连接。元素少的时候,可能整个 List 就一节车厢,看起来像一块连续内存;元素多了,就挂上多节车厢。Redis 还允许你通过配置控制每节车厢的大小,比如list-max-listpack-size -2表示每节车厢不超过 8KB。
正因为有这层结构,List 的两端操作 LPUSH/RPOP 是 O(1),但中间操作 LINSERT、LINDEX 最坏要遍历节点,复杂度是 O(N)。很多人以为 List 和编程语言里的数组一样按下标随机访问,这是底层认知的误区。
3.2 常用命令与常见误用
| 命令 | 作用 | 注意点 |
|---|---|---|
| LPUSH/RPUSH key element [element ...] | 从左边 / 右边推入 | 返回当前长度,可批量推 |
| LPOP/RPOP key | 从左边 / 右边弹出一个 | 空列表返回 nil |
| LRANGE key start stop | 取区间元素 | 下标是闭区间,负索引从尾部算 |
| LINDEX key index | 按下标取元素 | 最坏 O(N) |
| LSET key index element | 按下标改元素 | 下标越界报错 |
| LTRIM key start stop | 只保留区间内元素 | 经常被误解为"删除区间" |
| LREM key count element | 删除匹配元素 | count 正负控制方向,0 删除全部匹配 |
| LINSERT key BEFORE/AFTER pivot element | 在某个值前后插入 | 需要先定位 pivot |
| BLPOP/BRPOP key timeout | 阻塞弹出 | timeout 单位秒,0 表示永远等 |
最容易出错的是 LTRIM。它的语义是"保留 start 到 stop 区间内的元素,其余删除",和直觉上的"删掉这段"正好相反。比如要做"只保留最近 100 条消息":
LTRIM news:list 0 99这条执行完,列表里就只剩前 100 个元素了。
阻塞命令 BLPOP/BRPOP 返回的不是单个值,而是一个数组,包含 key 和弹出的元素。这在高并发消费场景下要注意解析。
3.3 用List实现轻量消息队列
List 做消息队列是老传统了,经典组合是 LPUSH + BRPOP:生产者从左边推,消费者从右边阻塞弹出。BRPOP 的好处是队列为空时线程挂起等待,而不是空轮询打爆 Redis。
# 生产者 LPUSH task:queue task-001 # 消费者 BRPOP task:queue 0多个消费者同时 BRPOP 一个 key 时,Redis 的弹出操作是原子的,每条消息只会被一个消费者取走,天然实现了任务分发。
但这个方案有个致命细节:BRPOP 取到消息之后,如果消费者进程崩溃或者处理失败,消息就丢了。我早期做任务队列的时候就踩过这个坑:一个异步任务取出来还没执行完,服务重启,任务直接消失。
可靠的轻量方案是采用备份队列模式:用 BRPOPLPUSH(老版本)或 LMOVE/BLMOVE(新版本)把消息从主队列弹出来,同时 push 进一个 processing 队列;处理成功后再 LREM 删除;如果处理失败,可以从 processing 队列取回来重试。
# 原子地把 task:queue 右边弹出,推入 task:processing 左边 BRPOPLPUSH task:queue task:processing 0 # 处理成功 LREM task:processing 0 task-001这个模式比裸 BRPOP 多了一点点复杂度,但可靠性和可观测性都上来了。需要强调的是:如果消息场景已经复杂到需要消费者组、消息确认、独立消费游标,就别再用 List 硬扛了,直接上 Redis 5.0 引入的 Stream,那是专门为消息队列设计的结构。
3.4 和编程语言的List到底差在哪
热词里有人问"numpy 和 list 比快在哪",也有人讨论"list 的模拟实现"。放在 Redis 语境里,我觉得值得把"Redis 的 List"和"编程语言里的 List"做一次彻底区分。
Redis 的 List 是服务端进程里的数据结构,只要连得上 Redis,任何语言都能读写同一个 List,进程退出数据还在(取决于持久化配置)。命令执行是单线程的,LPUSH 和 BRPOP 天然原子,多线程并发读写得心应手,不需要像操作本地 LinkedList 那样自己加锁。
编程语言里的 List(Python list、Java ArrayList、C++ vector)是进程内的连续数组或者链表。按下标取值 O(1),但跨进程共享、持久化、并发安全这些事全得自己做。
复杂度上也要做区分:Redis List 的 LPUSH/RPOP 是 O(1),LRANGE 取区间是 O(N),LINDEX 按下标找也是 O(N);而 Java ArrayList 按下标取是 O(1),中间插入是 O(N)。所以拿 Redis List 当随机访问数组用,是方向性错误。
至于 Numpy 比 Python 原生 list 快,核心是 NumPy 的数据在连续内存里做向量化运算,少了解释器逐元素解释的开销。这和 Redis 用 listpack / quicklist 追求紧凑内存布局是同一个道理:数据结构的效率,很大程度上取决于内存怎么排布。
4. 实操过程:从安装部署到Spring Boot整合
4.1 环境准备:三分钟跑起一个Redis
很多人卡在第一步,我先给一条 Linux 上的干净安装路径。从官网下载源码包后用 tar 解压,进入源码目录依次执行:
make make install PREFIX=/usr/local/redis编译前确保系统有 gcc 和 make。装完后把 /usr/local/redis/bin 加进 PATH,用 redis-server 启动,redis-cli ping 验证,返回 PONG 就说明通了。
想省事就直接用 Docker,我日常本地开发都是这么起的:
docker run -d --name redis -p 6379:6379 redis:7.2 --requirepass yourpassword连接测试:
docker exec -it redis redis-cli -a yourpassword pingWindows 环境要特别注意:Redis 官方不维护 Windows 原生版本。我不建议去跑那些第三方老旧的 exe 移植版,更稳妥的是两条路,要么用 WSL2 装 Ubuntu 后在 Linux 子系统里跑,要么用兼容层方案。用 WSL 时如果遇到wsl --list --online报 0x80072ee7 这种网络错误,通常和网络连通性或 WSL 版本太旧有关,先把 WSL 更新到最新版再重试,别纠结报错本身。
4.2 可视化客户端:挑一个顺手的,别在生产乱点
可视化客户端的选择,我实际用下来比较推荐这几个:
Redis 官方出的 RedisInsight 免费且功能全,支持内存分析、慢日志、图形化查看 key 的TTL和数据结构,适合刚开始学习的时候直观理解 Hash 和 List 长什么样。老牌的 Redis Desktop Manager(RDM)老用户多,社区版也能用。Another Redis Desktop Manager(ARDM)界面更现代,支持集群和 SSH 隧道,团队协作场景比较方便。
工具只是辅助,真正重要的是红线意识。生产环境我基本只用 redis-cli,而且严禁在可视化工具里手滑执行几条危险命令:KEYS *在 key 数量大的时候会让 Redis 单线程卡死;FLUSHALL更是直接把所有数据清掉。如果你只是排查问题,优先用SCAN和HSCAN这类增量命令。
4.3 Spring Boot整合Hash与List
Spring Boot 里整合 Redis,最简单的方式是直接用 StringRedisTemplate,它底层帮你把 key、field、value 都按字符串处理,避免了一堆序列化问题。代码示例:
@Autowired private StringRedisTemplate redisTemplate; // Hash:保存用户对象字段 public void saveUser(User u) { String key = "user:" + u.getId(); Map<String, String> fields = new HashMap<>(); fields.put("name", u.getName()); fields.put("age", String.valueOf(u.getAge())); redisTemplate.opsForHash().putAll(key, fields); } // Hash:读取单个字段 public String getUserName(Long userId) { return (String) redisTemplate.opsForHash().get("user:" + userId, "name"); } // List:往队列左侧推任务 public void pushTask(String taskId) { redisTemplate.opsForList().leftPush("task:queue", taskId); } // List:从右侧阻塞弹出任务 public String popTask() { return redisTemplate.opsForList().rightPop("task:queue", 5, TimeUnit.SECONDS); }注意rightPop(key, timeout, unit)这个方法对应的是 Redis 的 BLPOP/BRPOP 阻塞语义,队列为空时会阻塞等待最多 5 秒,超时返回 null。如果你在消费端循环调用,要处理好 null 的情况,别把超时当成正常业务结果。
4.4 序列化问题:乱码Key是怎么来的
很多人在 Redis 客户端里看到自己的 Hash key 或者 field 前面有一串类似\xAC\xED\x00\x05t的乱码,第一反应是 Redis 坏了,其实这是 Spring 默认序列化器的问题。
Spring Data Redis 的 RedisTemplate 默认用的是 JdkSerializationRedisSerializer,会把 Java 对象序列化成二进制字节流,所以 key、field、value 看起来全是乱码。这样带来的实际问题不止是难看:key 占用内存变大、跨语言无法读取、排查问题费劲。
解决办法有两个方向。最简单的是直接用 StringRedisTemplate,key 和 value 都是字符串,你自己在业务层完成对象和 JSON 的互转。如果需要自动序列化,就自定义 RedisTemplate,把 key 和 hash key 设成 StringRedisSerializer,把 value 和 hash value 设成 JSON 序列化器:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }这里有个我踩过的坑:很多人只改了 key 和 value 的序列化器,忘记 setHashKeySerializer 和 setHashValueSerializer,结果 Hash 的 key 和 field 是正常的,value 里却出现一串乱码。配置序列化器一定要四个都设置,缺一不可。
5. 问题排查、缓存治理与进阶认知
5.1 高频面试题里的Hash和List考点
面试里关于这两类数据结构的高频题,我整理一下参考答案的要点:
为什么 Redis 快?核心是内存操作加单线程 IO 多路复用加高效数据结构。单线程的好处是不用考虑锁竞争,IO 多路复用让网络事件处理很高效,数据结构的精心设计让内存和 CPU 开销都可控。
Hash 的底层结构是什么?要能答出:小数据量用 ziplist 或 listpack,大数据量用 hashtable,以及版本间的阈值差异。主动带出"7.x 默认 128 个字段或 64 字节 value"这种细节,面试官会知道你真的看过配置。
List 的底层结构是什么?quicklist,双向链表套压缩节点。7.0 后的核心是 listpack 节点,配置项是 list-max-listpack-size。
Hash 和 Set 的区别?Hash 是一个 key 下的 field-value 映射,field 不能去重但 value 任意;Set 是无序去重的集合,适合交集并集运算。两者名字容易混,但语义差很远。
HGETALL和LRANGE 0 -1有什么问题?都是 O(N) 的大范围读取,在 key 很大时会阻塞单线程 Redis,应该用 HSCAN 和分段 LRANGE 替代。
5.2 分布式锁和Hash的关系
热词里有人问"redis 分布式锁"和"后端 hash 和 linkhash",这里必须把分布式锁和 Hash 的关系说清楚。
最基础的分布式锁实现是这条命令:
SET lock:order:1001 client-001 NX PX 30000NX保证只有 key 不存在时才能写入,PX设置过期时间防死锁,value 放客户端唯一标识以便释放时校验归属。释放锁要小心翼翼:先比较 value 再删除,两步之间必须用 Lua 脚本保证原子性,否则可能删掉别人刚持有的锁。
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end那 Hash 在这里扮演什么角色?简单锁用 String 就够了,但讲到可重入锁,Hash 就登场了。Redisson 的 RLock 底层用的就是 Hash 结构:key 是锁名称,field 是"客户端标识:线程ID",value 是重入次数。同一个线程重复 lock,就在这个 Hash 上做一次 HINCRBY 计数;unlock 时递减,减到 0 才 HDEL 删除整个 key。
所以答案是:分布式锁有两种实现路径,裸 SET 用 String,可重入锁用 Hash。面试里能把这个细节讲清楚,分量很足。
5.3 缓存治理中的几个坑
缓存治理是一个系统工程,围绕 Hash 和 List 我挑几个最值得说的。
Key 设计必须有规范。我见过最乱的生产环境,key 全是随意拼接的,没有任何前缀语义。Redis 官方也建议 key 用业务:对象:ID的层级方式:比如user:info:1001、cart:1001:items。Hash 的 field 命名同理,要能一眼看出含义。
缓存穿透、击穿、雪崩是绕不开的三座山。穿透用空值缓存或布隆过滤器;击穿用互斥锁或逻辑过期;雪崩最有效的办法是过期时间加随机值,避免大量 key 同时失效。这些方案和 Hash/List 没有直接绑定,但如果你用 Hash 缓存对象列表,批量重建时要注意避免瞬时对后端数据库造成压力。
大 key 治理要前置。Hash 的 field 无限膨胀(比如把用户会话全塞进去)、List 被疯狂 LPUSH 导致长度几十万,都是典型的大 key 来源。治理手段无非三种:拆 key、换结构、设置上限配合定期清理。比如 List 做时间线时,每次写入后用 LTRIM 只保留最近 N 条,这是最简单有效的预防手段。
5.4 主从与集群环境下的特别提醒
热词里有 docker 安装 redis 主从、kubesphere 搭建 redis 这类部署话题。篇幅原因我不展开 K8s,只讲两个和 Hash/List 直接相关的部署注意事项。
主从复制的时候,Hash 和 List 的数据是通过 RDB 快照加增量命令传播来完成同步的,从库可以完整读到这些结构,也能执行只读命令。但要注意:Big Key 的主从同步会放大网络开销,一个几十 MB 的 Hash 在初次同步或者断线重连时,会让主从之间的网络瞬间打满。所以大 key 治理不只是单机问题,直接影响主从架构的稳定性。
集群环境下有一个名字特别容易混淆的概念:Hash Slot。Redis Cluster 根据CRC16(key) % 16384把 key 分配到不同槽位,这里的"Hash"和 Hash 数据类型没有任何关系。你可能遇到这种需求:想把一个用户的所有相关 key 放在同一个节点上做批量操作,那就用 Hash Tag 语法,把要参与哈希计算的部分放进花括号里,比如{user:1001}.base_info和{user:1001}.detail,Redis 只会对花括号里的内容做 CRC16,从而保证两个 key 落在同一槽位。
我自己开发时就是用这个思路给用户维度的 Hash + List 组合做集群亲和性设计,实测下来对批量读取的延迟改善很明显。
主从的最小验证方式,用 Docker Compose 写两个服务就能跑起来:
version: '3.8' services: redis-master: image: redis:7.2 command: ["redis-server", "--appendonly", "yes"] redis-slave: image: redis:7.2 command: ["redis-server", "--replicaof", "redis-master", "6379"] depends_on: - redis-masterdocker compose up -d起来后,在主库写入一个 Hash,到从库HGETALL能查到,就说明主从复制对 Hash 的同步链路是通的。
最后分享一点我自己的体会。写这篇之前,我重新翻了一遍当年做活动页缓存时的老代码:最开始用 String 存整个 JSON,运营改一个按钮文案就把后端折腾够呛;后来改成 Hash 一个配置项一个 field,改哪就 HSet 哪,刷新成本几乎降为零。后来做异步任务队又踩了一次裸 BRPOP 丢消息的坑,才老老实实把 LMOVE 加备份队列的套路用起来。数据结构这东西,背一百遍文档不如在生产环境被坑一次记得牢。如果你正在学 Redis,我建议先把 Hash 和 List 的底层阈值参数、常用命令做成速查卡,然后在本地 Docker 里把大 key、阻塞、序列化这些坑一个个亲手踩过去,踩完你就不会再纠结选型了。