一条完整的事故时间线
Redis 部署成主从加哨兵(Sentinel)是标配:主库扛写,从库兜底,主库挂了哨兵自动提升从库。一切正常时天下太平,直到某一秒,四个动作撞在了一起:
t0 客户端A 向主库 M 发 SET lock 8f3a NX PX 10000 → OK,A 拿到锁
t0+1ms 主库 M 宕机,lock 这条写还没来得及复制给从库 S
t0+30s 哨兵仲裁完成,S 升级为新主库
t0+31s 客户端B 连新主库 SET lock ... NX PX → OK(key 在新主上不存在)
↑ A 和 B 同时「持有」锁,临界区里出现了两个人注意 A 并没有做错任何事:它按规矩抢锁、按规矩干活,锁丢在主从异步复制这个它根本控制不了的环节上。A 拿锁期间如果又崩又复活(第 5 篇的 GC 场景),连「感知丢锁」的机会都没有。这就是 Redis 锁最被诟病的一击,也是 Martin 批评 RedLock 时的核心论据——只要锁的持久性依赖异步复制,主从切换就永远是锁的真空时刻。这不是配置能根治的:Redis 为了写性能选择了异步复制,性能与安全的天平在架构上就已倾斜。
缓解一:min-replicas 限写
antirez 官方建议的止血方案是两个参数:
min-replicas-to-write 1 # 至少 1 个从库在线才接受写入
min-replicas-max-lag 10 # 且该从库复制延迟不超过 10 秒思路是「与其发一把可能丢失的锁,不如不发」:主库检测到没有任何健康的从库(延迟超限)就拒绝写锁请求,抢锁的客户端直接失败。故障切换的窗口被压缩了——从「主挂了锁还在飞」变成「主挂了锁服务先停摆,新主起来再恢复」。安全性上有所改善,代价是可用性:从库抖动会连带锁服务不可用,业务要能接受「Redis 不可用时拿不到锁」的降级路径(排队、本地限流、或直接拒绝)。
缓解二:WAIT 与它的不严格
Redis 3.0 起提供了 WAIT 命令:SET 写锁成功后跟一条 WAIT 1 100(等 1 个从库确认、最多等 100 毫秒),看起来是同步复制语义。但要交底清楚:WAIT 只保证写入已进入从库的复制流,不保证从库已完成落盘,更不保证 failover 后的完整链路——WAIT 返回后主库立刻宕机,极端情况下从库升主时那条锁记录依然可能丢。WAIT 把丢锁概率从「毫秒窗口」压到「极小概率」,但压不到零,它的每一点改善都在吃写延迟。真要严格同步复制,Redis 给不了,这是定位问题不是参数问题。
Redis 锁的诚实交底
到这里,Redis 锁的底牌全部亮完,可以给它下结论了:Redis 锁是效率锁的王者,是正确性锁的赌徒。快(内存操作)、轻(不引入新组件)、生态好(Redisson 全家桶),防重复跑、防缓存击穿、防重复建单这类「双跑浪费但不致命」的场景闭眼用。但碰钱、碰库存、碰任何「双跑即事故」的场景,单靠它就是赌——要么赌主从切换不撞上临界区(多数时候能赢),要么配上 fencing token 兜底(第 16 篇),要么换一把「天生为一致而生」的锁。第 5 篇那张选型表的最后一行,就是接下来四篇的主角:ZooKeeper——一个把「所有节点对同一件事达成一致」写进设计目标、用 ZAB 协议背书的协调服务。下一篇先补齐它的基础课:ZNode、Watch、临时节点,三个概念撑起它的全部精彩。
Cluster 分片:同一出戏演 N 遍
有人会想:换成 Redis Cluster 是不是就好了?答案是原样重演。Cluster 只是把数据按 slot 分片到多个主从组,每一把锁落在某个 slot 上,就只归那一个分片的主库管——每个分片内部依然是异步复制的主从结构,主从切换时丢锁的窗口一秒都没少。分片多反而让「哪把锁在哪片」的心智负担更重。一句话总结 Redis 锁的复制观:单机、哨兵、Cluster,三种架构三种壳,异步复制的芯一模一样。要跟这种原罪正面硬刚,只能靠 RedLock 的多数派、或下一篇登场的 ZAB 与 Raft 这类真协议。
评论 (0)