
Redis这东西属于那种“入门容易精通很难”的中间件。前面两篇实战笔记已经把基础命令、内存模型、淘汰策略聊清楚了这篇是第三篇我打算把实战里最容易出问题的几个场景集中过一遍数据类型的高阶用法、分布式锁的坑、持久化配置怎么选、缓存治理方案外加部署运维落地的那些破事。内容比较杂但每一节都是我在线上环境里真金白银踩出来的经验。如果你已经会用SET/GET、知道 Redis 是单线程的但一碰到复杂业务场景就不知道该怎么设计 key、怎么保证数据一致性、怎么在故障时恢复数据那这篇就是给你写的。1. 五大数据类型的高阶实战用法1.1 List 不只是消息队列很多人对 List 的认知停留在LPUSH BRPOP当队列用。确实这是最经典的用法但你要真把它往生产环境一扔问题就来了。先说队列场景。LPUSH往左推BRPOP从右边阻塞弹出天然就是 FIFO。但有个隐藏问题消费端挂了或者处理超时消息会积压在 List 里而且没有 ACK 机制消费者把消息RPOP出来之后就相当于“确认”了要是处理过程中宕机这条消息就永久丢了。我建议别用原生 List 做需要可靠投递的业务队列除非你明确知道消息丢了无所谓。如果一定要用有个补救思路用RPOPLPUSH的变体BRPOPLPUSH把弹出的消息同时备份到另一个备份 List 里消费者处理完再删除。这样至少有个“处理中”的副本宕机了还能手动捞回来算是徒手实现了个半吊子 at-least-once 语义。List 另一个好用的场景是时间线/最新列表。比如首页 Feed 流每产生一条新内容就LPUSH到user:{uid}:feed再用LTRIM只保留前 500 条配合分页时LRANGE一次取 20 条。这种场景下 List 的性能优势特别明显LPUSH LTRIM是 O(1) 操作压几千 QPS 毫无压力。但要注意千万别用LLEN当总数去做分页因为列表被LTRIM截断过长度和实际业务总量对不上。正确的做法是只存“最近 N 条”的索引总量用另外的计数器维护。1.2 Set 才是去重和关系运算的王道Set 最基础的功能是去重但实战价值远不止这个。Sinterstore 是 去重和关系运算的核心这功能我用来做“共同关注”简直顺手。比如用户 A 关注了 50 个作者用户 B 关注了 80 个作者分别存成follow:{a}和follow:{b}两个 SetSINTER一次就能把共同关注的人全捞出来时间复杂度是 O(N*M) 但集合不大的时候毫秒级返回。换成关系型数据库你得写两条查询再在内存里做交集麻烦且慢。还有一个被低估的场景UV 去重统计。比如一个内容的访问用户 ID直接SADD page:{contentId}:uv userIdSCARD就是 UV 数。如果是在大流量的场景下内存开销会比较厉害这种时候我会毫不犹豫换 HyperLogLogPFADD/PFCOUNT占用空间极小误差在 0.81% 以内对绝大多数 UV 统计场景完全够用。Set 做随机抽奖也很好用SRANDMEMBER和SPOP的差别要注意SPOP会从集合里移除元素适合“抽完就没了”的一次性活动SRANDMEMBER不删除适合“抽奖后还能继续抽”的重复参与场景。真做过一次运营活动几千人同时抢几百个名额用SPOP没出现任何并发问题就是因为 Redis 单线程原子性。1.3 ZSet 的排序魔力与“分数”设计ZSet 大概是 Redis 里最值钱的数据类型跳表实现的有序集合。延迟队列、排行榜、滑动窗口限流都离不开它。排行榜是最典型的应用但很多人栽在“分数怎么设计”上。如果你的需求是“总榜 周榜 热度实时变化”切忌一个 key 打天下。我见过一个项目把“分数”直接设成了某个业务数值结果想“按时间衰减热度”时没法改历史分数只能把 key 删了重新建线上直接甩了一脸血。我自己的做法是分数用复合值编码。比如score 点赞数 * 10000000000 发布时间戳这样既能保证主排序按点赞量点赞量相同时按时间先后排。注意时间戳要转换成“倒序”才能让新内容排前面——用Long.MAX_VALUE - 当前时间戳。要再复杂一点可以判断时间窗口内分数衰减就在定时任务里用ZINCRBY给参与者的 score 加一个负值模拟热度下降。延迟队列的场景我重点说下。ZSet 的 member 存任务 IDscore 存到期时间戳开一个循环任务用ZRANGEBYSCORE key -inf 当前时间戳 LIMIT 0 1把到期任务拉出来处理。这里有个坑多个消费者同时拉取同一批到期任务会产生重复消费。实际项目里我用ZREM配合循环来解决先ZRANGEBYSCORE拿到任务再ZREM成功才算拿到如果是多个线程要把ZRANGEBYSCORE和ZREM放进 Lua 脚本保证原子性否则有并发间隙。这一条我在线上真的遇到过——多个 worker 同时把同一个任务处理了两遍业务方直接报警。2. 分布式锁的正确打开方式2.1 从 SETNX 到 SET NX EX第一步别写错分布式锁是面试必考题也是实际工程中的“高危区域”。早年间的经典写法是SETNX lock_key unique_value EXPIRE lock_key 30懂的人会看出问题这两条命令不是原子的。要是SETNX之后进程突然崩溃锁就永远不会释放其他线程全部卡死。后来官方推荐了原子的写法SET lock_key unique_value NX EX 30一条命令搞定加锁和过期时间保证原子性。但你以为这就完了还没完。释放锁的时候也不简单很多人直接DEL lock_key这是极端危险的。要是 A 线程锁的过期时间是 30 秒结果业务处理了 35 秒锁先自动过期了B 线程拿到锁开始干活然后 A 处理完了跑来DEL把 B 的锁删了C 线程又能进来了。这就是典型的“锁误删”。解决方案是释放前先比对value是不是自己加锁时设的唯一标识比对通过才删。这个比较和删除也要保证原子性不能写成两步的GETDEL要写进 Lua 脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end用 jedis 或 lettuce 执行这段脚本才算是一个合格的基础版分布式锁。2.2 Redisson 与看门狗机制为什么推荐框架我上面讲的这些你如果自己用原生命令实现代码量不小而且容易漏边界。生产上我更推荐直接用 Redisson它已经把加锁、释放、续期这些逻辑都封装好了。RLock lock redissonClient.getLock(inventory:1001); boolean acquired lock.tryLock(3, 30, TimeUnit.SECONDS); if (acquired) { try { // 业务逻辑 } finally { lock.unlock(); } }Redisson 默认有一个“看门狗”机制如果你没指定leaseTime它会给锁自动续期默认每隔 10 秒把锁的过期时间刷新到 30 秒直到业务执行完主动unlock。这样就避免了“业务还没干完锁先过期”的尴尬。但要说清楚看门狗不是万能的。如果业务线程被阻塞住、或者 Redis 主节点宕机导致锁数据丢失这个机制也救不了你。我之前碰到过一个极端案例业务代码里有个 RPC 调用超时时间设了 60 秒而锁的看门狗续期在旧版本里依赖 netty 线程如果 netty 事件循环被阻塞续期也会断。后来排查到是线程池配置问题所以别把分布式锁完全交给框架业务上还是要尽量缩短锁内耗时锁内别做重活。2.3 RedLock 要不要用我的看法面试题里特别喜欢问 RedLock红锁——向多个独立的 Redis 节点同时申请锁过半成功就算拿到。这个方案在理论上是解决“主节点宕机丢失锁”的但我自己不会轻易在生产环境用。原因很简单RedLock 依赖“多个节点是相互独立的”而且要有 N 个奇数节点运维成本翻倍。真遇到主节点宕机你首先要做的是故障转移而不是分布式锁“绝对安全”。现实业务里绝大多数场景对分布式锁的要求是不误删别人的锁、锁能自动过期、性能好这些用单机 Redis Redisson 已经能覆盖 95% 的需求。只有在你对“锁的绝对可靠”有硬性要求、而且多个服务实例分布在跨机房跨区域时才考虑 RedLock。否则我更倾向的做法是Redis 主从切换后业务自己通过数据库唯一索引兜底保证幂等。分布式锁解决的是并发协作问题不是数据正确性的最后防线。3. 持久化实战RDB、AOF 与混合持久化3.1 RDB 快照的触发机制和坑Redis 持久化一句话总结RDB 是内存数据的二进制快照AOF 是每条写命令的日志。两者各有侧重但线上配置不是非此即彼。RDB 默认配置大概是save 900 1、save 300 10、save 60 10000意思是在 900 秒内有 1 次写、或者 300 秒内有 10 次写、或者 60 秒内有 10000 次写就触发一次持久化。注意这里不是“三个条件同时满足”而是“任意满足一个就触发”。触发 RDB 后会调用fork()子进程去写磁盘。这里的坑是在 Redis 内存占用特别大时fork 可能很慢甚至报错。我见过一个内存 32GB 的实例fork 一度耗时 3 秒多期间所有客户端请求都被卡住监控直接飘红。后来我把save策略调保守了改成 120 秒 10000 次才触发配合 AOF 兜底才好多了。另外SAVE和BGSAVE要分清。SAVE是同步阻塞的生产环境千万别敲BGSAVE才是在后台 fork 子进程执行。写脚本或运维命令时一定要用BGSAVE。3.2 AOF 三种刷盘策略怎么选AOF 是追加日志文件可配置的刷盘策略有三个策略说明数据安全性性能appendfsync always每次写命令都刷盘最高最多丢一条慢QPS 下降明显appendfsync everysec每秒刷一次最多丢 1 秒数据适中推荐appendfsync no交给操作系统刷可能丢几秒数据快但不安全我默认会用everysec。别迷信always真的很伤性能尤其大批量写的时候能感觉到明显的吞吐下降。还有一个经常被忽略的地方AOF 文件会无限增长必须配auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb触发自动重写。如果 AOF 重写失败日志文件残留在磁盘上占空间倒是小事关键重启时会读取这个坏文件。Redis 其实对坏 AOF 有处理策略默认aof-load-truncated yes会加载截断数据但如果你是追求完美的工程师建议把这个设成 no 并在启动前用redis-check-aof修复。3.3 混合持久化才是线上标配Redis 4.0 之后支持了混合持久化aof-use-rdb-preamble yes。简单说AOF 文件开头是 RDB 格式的快照后面是增量命令。这个模式兼顾了 RDB 的加载速度和 AOF 的数据完整性我测试过同样 10GB 的数据集纯 RDB 启动加载大概 5 秒纯 AOF 重放可能要 30 秒以上混合模式基本和 RDB 差不多。所以我的配置方案是RDB 留着当兜底AOF 开混合模式刷新策略 everysec。这样能把故障恢复时间做到最短数据丢失控制在 1 秒内代价是磁盘占用比纯 RDB 多但对大多数业务来说完全可接受。如果你用的是云厂商的 Redis注意他们默认的持久化策略不一定适合你的业务。我有次接了个业务云上的实例默认启用了 AOF但没开混合模式结果重启一次集群花了快 20 分钟才知道 AOF 文件已经涨到 8GB。后来手动执行BGREWRITEAOF把文件压缩重写并配置了自动重写阈值启动时间才恢复正常。4. 缓存治理穿透、击穿、雪崩与序列化4.1 三种经典故障的区分和应对网上关于缓存穿透、击穿、雪崩的文章很多但实际操作时很多人分不清业务现象。我用自己的话重新梳理一遍并且把应对手段串起来。缓存穿透查询一个根本不存在的数据。缓存里没有数据库里也没有每次请求都打到 DB 上只能用“空值缓存”或“布隆过滤器”挡。空值缓存简单粗暴key 不存在就缓存一个特殊值过期时间设短一点比如 5 分钟。布隆过滤器适合 key 集合固定、能提前构建的场景比如卖家 ID 白名单用户 ID 范围固定。别在每次请求都动态构建布隆那是把性能问题换了个地方出现。缓存击穿某个 key 瞬间过期恰好有大量请求同时访问它直接打穿到数据库。应对手段是加互斥锁就是我们前面说的分布式锁第一个请求拿到锁去查库回填其他请求等锁释放后直接读缓存。也可以用“逻辑过期”方案value 里存一个过期时间字段读取时发现逻辑上过期了立刻返回旧值并异步重建缓存。缓存雪崩大量 key 同时过期或者 Redis 实例宕机。大量 key 过期很好解决过期时间加随机值比如SETEX的时候在过期时间上加大 1~5 分钟的随机数避免同一时刻雪崩。Redis 宕机的场景我经历得多一点这类问题没有银弹能做的就是主从哨兵/集群、多级缓存本地 Caffeine Redis 双缓冲、数据库限流降级。大概意思就是不要让 Redis 成为唯一的数据源万一缓存挂了至少挡住一部分请求。4.2 缓存与数据库双写的三种策略这个其实比加锁还麻烦。主流策略无非三种Cache-Aside、Write-Through、Write-Back但绝大多数互联网公司实际用的是 Cache-Aside思路是读缓存、没读到就查库回填写的时候先更新数据库、再删除缓存。很多人会在“先删缓存再更新 DB”和“先更新 DB 再删缓存”之间纠结。我是坚定的“先更新 DB 再删缓存”党。原因很简单先删缓存的话如果 DB 更新失败缓存里就没数据了下次请求就会击穿到 DB我先更新 DB就算删缓存失败大不了多一次 cache miss但不会出现缓存和 DB 长时间不一致的窗口。删除缓存失败的坑我踩过。解决方式一般有两种一是用消息队列补偿把删缓存失败的消息投递到 MQ异步重试二是订阅 binlog 用 Canal 同步删缓存。如果觉得自己搭一套 Canal 太重至少用带重试的本地任务顶着总比啥都不做然后数据不一致强。4.3 序列化方式决定了你的性能和兼容性Redis 的序列化问题主要是 Java 技术栈里用 RedisTemplate 时比较明显。默认的 JdkSerializationRedisSerializer 序列化出来是一串带类型头部的二进制不但体积大、可读性差而且跨语言基本没法用。做微服务、数据要对接其他团队时这个问题特别头疼。我自己在 Spring Boot 项目里的标配Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); GenericJackson2JsonRedisSerializer serializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); return template; }注意key 用 Stringvalue 用 JSONhash 的 field 也尽量用 String。这样做有两点好处一是可视化工具里能直接看到 key 和部分 value排查问题不用解码二进制二是兼容性极强Python、Go 都能直接反序列化。但也有一个坑用GenericJackson2JsonRedisSerializer序列化对象时默认会把类型信息class写进 JSON。要是这个 JSON 给前端直接用前端解析会遇到没见过的属性。建议只在 Redis 和业务服务之间传递这个 JSON不要直接发给外部系统。要发给外部就用 DTO 自定义序列化把类型信息剥离掉。5. 运维部署与排查实战经验5.1 单机版、主从哨兵、Cluster 怎么选聊到部署经常有人问我到底该用单机、主从、哨兵还是 Cluster我的建议是按业务体量阶梯选择单机版适合并发量不大、数据量小、可以接受短暂不可用的场景。我建议开发环境和预发布环境用单机就行别过度设计。主从 哨兵适合必须保证高可用、但总数据量在几十 GB 以内、只有一个写入源的业务。哨兵负责故障自动切换主挂了从顶上。Cluster 集群适合数据量超过单机内存、或者写入吞吐量需要横向扩展的场景。Cluster 的槽位分配、MGET跨 slot 限制、重定向机制都要熟悉。我自己有套比较稳的套路中小型业务直接上主从哨兵大型高并发业务直接上 Cluster。中间态最尴尬比如你用了 Cluster 但数据量怎么都过不了 100GB还不如主从省心。选完架构再看安装。如果你在 Windows 上开发调试别去官网找 Windows 版——Redis 官方其实不维护 Windows 版本。下载redis-windows社区版或者 5.0.14.1 这种老版本凑合用没问题但生产环境一定要用 Linux别杠内存效率、IO 性能、官方支持都在 Linux 上。5.2 Docker 部署 Redis 和可视化工具开发环境用 Docker 部署 Redis 是最省事的docker run一条命令就搞定docker run -d --name redis-dev \ -p 6379:6379 \ -v /data/redis:/data \ --restartalways \ redis:7.0 redis-server --appendonly yes --requirepass yourpass注意我把数据目录挂载到了宿主机并且开了 AOF这样容器重启数据不丢。密码我建议设置哪怕只是开发环境。--requirepass一旦设置客户端连接时就必须带上AUTH很多初学者连不上 Redis 就是漏了这一步。可视化工具这块我最早用 Redis Desktop ManagerRDM后来它改名了也收费了。现在个人使用我推荐Another Redis Desktop Manager开源免费连接信息、查看 key、执行命令都方便macOS/Windows/Linux 都有。RedisInsight 也不错官方出品适合做性能分析能查看命令耗时、内存诊断。我最近两个都在用RedisInsight 看大 key 分布很直观。部署层面有个老生常谈但特别常见的错误忘记设置 maxmemory 和 maxmemory-policy。默认情况下 Redis 不会限制内存写爆了操作系统直接 OOM。生产环境我强制要求配置maxmemory 8gb maxmemory-policy allkeys-lru5.3 线上排障三板斧慢查询、大 Key、命令扫描真到了线上 Redis 出问题我的排查顺序是“慢查询 → 大 key → 热点 key”。慢查询日志通过SLOWLOG GET查看线上建议设置slowlog-log-slower-than 1000010 毫秒把超过阈值的命令记录下来。我遇到过一个大KEYS *查询导致 Redis 阻塞 5 秒的案例这类命令在线上一定禁止使用要扫 key 就用SCAN每次返回少量结果然后游标继续迭代。大 key 的排查可以用redis-cli --bigkeys扫描它会统计每种类型最大的 key 和最多的元素。发现大 key 后如果是个大 List可以拆成多个小 key按时间或业务模块分片。一个线上大 key 如果达到了几十万条一次LRANGE或DEL都可能阻塞 Redis 好几秒。热点 key 排查稍微复杂点我在 key 数量多的业务里会用 RedisInsight 的 keyspace analysis 功能或者直接在客户端统计访问频率访问量超过阈值的 key 统一登记再做本地缓存或 key 拆分为多个副本key:1到key:N分散读压力。有人会觉得这个操作有点脏但面对单 key 热点导致的“缓存击穿”问题这是最直接有效的。6. 面试中高频的 Redis 陷阱题整理6.1 单线程为什么能这么快又为什么不要乱用命令Redis 单线程能支撑每秒十万级的 QPS核心是内存存储 非阻塞 IO 多路复用。但要注意“单线程”指的是命令执行是单线程网络 IO 和持久化另有处理比如 Redis 6.0 以后引入了 IO 多线程网络读写可以并行但命令执行依然是单线程的。面试官最爱挖坑的就是这里既然单线程有哪些命令会阻塞KEYS、FLUSHALL、FLUSHDB、SAVE这四类是重灾区另外对超大集合执行SORT、LREM也可能阻塞。平时写代码时尽量用SCAN代替KEYS用BGSAVE代替SAVE对 key 数量特别大的集合操作分批执行。6.2 过期删除和内存淘汰别再分不清很多刚接触 Redis 的人把“过期”和“淘汰”混为一谈。实际上过期是某个 key 到了 TTL 时间后主动删除内存淘汰是内存达到maxmemory时被动删除。过期删除有三个策略惰性删除访问时检查、定期删除后台随机抽查、惰性 定期组合。Redis 默认是组合使用这导致在某些场景下大量过期 key 并不会被立即清理内存可能仍然被占用需要从业务层设置合理的过期时间和主动清理策略。内存淘汰策略面试常问策略说明noeviction达到上限直接报错写入失败默认allkeys-lru所有 key 中淘汰最近最少未使用volatile-lru设置了过期时间的 key 中淘汰 LRUallkeys-random所有 key 随机淘汰volatile-ttl按剩余 TTL 最短淘汰我线上一般用allkeys-lru但如果缓存里还存了持久化数据就会用volatile-lru保证非过期 key 不被淘汰。6.3 主从复制的延迟和脑裂怎么兜底主从复制天然有延迟所以像秒杀库存这种强一致场景绝对不能只读从库。我在秒杀项目里是强制走主库读的从库只用于统计和报表。如果要追求最终一致性可以用WAIT命令等待从库同步但代价是延迟增大得根据业务接受度取舍。脑裂问题主节点网络分区后旧主还在运行但哨兵已提升新主会导致数据丢失尤其是主从切换阶段。兜底方法就是持久化配置要做好主库开 AOF everysec即使旧主恢复也能从 AOF 里恢复部分数据减少损失。写在最后的几点经验Redis 的实战经验说到底就是“搞清楚每种数据结构适合什么场景搞清楚每个配置项会带来什么后果”。我个人的习惯是每个 Redis 实例上线前一定写好一张配置对照表从持久化、淘汰策略、慢查询阈值、内存上限到连接数限制全部列清楚发布时由运维逐项检查。还有一个建议不要盲目追求新版本和复杂架构。Redis 7 的某些新特性很好用但如果你只是存缓存、做队列老版本 5.x、6.x 也够稳。架构上先单机压力大了再上主从哨兵最后才考虑 Cluster。脚踏实地方案永远比炫技方案更能扛住线上事故。最后分享一个很实用的小技巧线上执行任何批量操作或者危险命令之前先在测试环境用同一份数据量压一遍看看耗时。我吃过几次“以为很安全结果线上直接阻塞”的亏从那以后这条原则成了我自己的铁律。希望这篇笔记能帮你省下几个深夜排查事故的工夫。