做后端的人,几乎都写过一把分布式锁——用 Redis 的 SET NX EX 加锁,删除 key 解锁,看起来三行代码就能让多个实例”排队”访问共享资源。但真正把它放到生产环境跑一段时间,你就会发现它远比想象中难:锁过期了业务还没跑完、A 删掉了 B 的锁、Redis 主从切换后两个客户端同时持锁……每一个都能让你的数据悄悄错乱。本文就把分布式锁的几类经典坑讲清楚,不谈玄学,只谈能落地的做法。

一、Redis 单实例锁:能用,但要先把三件事做对
最朴素的实现大概是这样的:先 SETNX 判断 key 是否存在,再 EXPIRE 设过期时间。这段代码有两个致命问题:一是 SETNX 和 EXPIRE 不是原子操作,中间进程挂掉就会留下永不释放的死锁;二是解锁时直接 DEL key,如果自己因为 GC 停顿或网络抖动导致锁提前过期,这时删掉的其实是别人刚拿到的锁。
第一个问题的解法很简单:用 SET key value NX PX 30000 一条命令同时完成”加锁 + 设过期”,这是 Redis 2.6.12 之后的原子写法,别再分两步。
第二个问题的关键是:锁必须带唯一标识,解锁必须校验归属。加锁时把 value 设为一个随机 UUID 或客户端 ID,解锁时用 Lua 脚本把”判断 value 是否等于自己”和”删除 key”合并成一个原子操作:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这样就不会出现”删别人的锁”的事故。很多人会问:为什么不干脆用 Redis 官方推荐的 Redlock?Redlock 的争议在于它依赖多个独立 Redis 节点和时钟假设,实际运维成本高,而且在出现网络分区时并不能给出严格意义上的一致性保证(Martin Kleppmann 那篇著名的质疑文章值得一读)。对绝大多数业务来说,单实例 Redis + 唯一标识 + Lua 释放 + 合理的过期时间,已经能覆盖 95% 的场景。

二、锁过期与业务执行时间的赛跑
就算加锁解锁都做对了,还有一个躲不掉的问题:业务执行时间比锁的 TTL 长。你设了 30 秒过期,结果这次数据库慢查询跑了 45 秒,锁在中途就没了,另一个线程立刻进来,两个线程同时操作同一份资源——这就是最典型的锁失效。
常规方案是”看门狗”(watchdog)续期:起一个后台线程,每隔 TTL 的三分之一就检查一次业务是否还在跑,如果在跑就把锁的过期时间重新续上。Redisson 的 RLock 内置了这个机制,用起来很省心。但要注意两点:一是续期线程必须和持锁线程绑定,进程挂了续期自然停止,锁最终会过期,不会造成死锁;二是续期不是万能的——如果 Redis 客户端与服务器之间网络中断,续期请求发不出去,锁依然会失效。
所以更根本的思路是:不要把分布式锁当成”绝对互斥”的保证,而要把它当成”性能优化手段”。真正的数据一致性,最终要靠数据库层面兜底——比如乐观锁版本号、唯一索引、状态机校验。锁只是让绝大多数并发在进入临界区之前就被挡住,降低冲突概率,而不是替你做正确性证明。这样想,你的系统设计会稳健很多。
三、集群与故障转移:为什么”主从 + 哨兵”不够
Redis 主从架构下有个著名的隐患:客户端 A 在主节点上加锁成功,主节点还没来得及把这条命令同步给从节点就宕机了,哨兵立刻把从节点提升为新主节点——而这个新主节点上根本没有那把锁。此时客户端 B 来加锁,同样成功。两个客户端同时持锁,互斥性被打破。
Redis 作者提出的 Redlock 算法就是为了解决这个问题:向 N 个(通常 5 个)互相独立的 Redis 实例依次加锁,只有在超过半数实例上加锁成功、且总耗时小于锁有效期时,才认为加锁成功。理论上它把故障概率从”单点”降到了”多数派同时故障”,但它的正确性依赖于各节点的时钟漂移不能太大——而分布式系统中时钟恰恰是最不可靠的东西之一。
如果你的业务对互斥性要求极高(比如扣款、库存扣减这类不容出错的场景),更稳妥的选择是用具备一致性协议的组件来做锁:etcd 的 Lease + Revision、ZooKeeper 的临时顺序节点,都基于 Raft 或 ZAB 实现了真正的强一致。代价是性能比 Redis 低一个量级、部署运维更重。反过来,如果只是为了避免重复任务、降低并发压力,Redis 方案完全够用。
一句话总结选型:要性能用 Redis,要正确性用 etcd/ZooKeeper,要正确性又不想上重组件,就用数据库的唯一索引或悲观锁。很多时候,INSERT ... ON DUPLICATE KEY 加个唯一索引,比任何分布式锁都简单可靠。

结语:锁不是银弹,边界感才是
分布式锁之所以容易出问题,本质上是因为我们总想用一个”简单的小组件”去解决”分布式系统的一致性”这个宏大问题。但分布式环境里的每一层都可能失败:网络会分区、时钟会漂移、进程会停顿、主从会切换。任何一把锁,都只是在概率意义上收紧了这个窗口,而不是把它关上。
所以我的建议是:先问自己”不加锁会怎样”——如果业务本身可以用幂等设计消化重复请求,那可能根本不需要锁;如果必须互斥,就把锁的职责限定在”减少冲突”,把正确性交给数据库约束、状态机和幂等校验来兜底。技术选型上诚实面对自己的需求边界,比死记某个算法的实现细节重要得多。
毕竟,能长期稳定运行的,从来不是最精巧的那把锁,而是你对系统失效边界的清醒认知。