分布式锁这个东西,网上一搜一大片,但说实话很多文章都是互相抄,真正能落地、经得起线上考验的并不多。我做了这么多年的后端,从最开始用 setnx 加锁,到后来被线上故障折腾得焦头烂额,再到自己重写了一套跨机房的高可用锁组件,踩过的坑比写过的代码还多。这篇文章不只是讲原理,更会把你把“高可靠性”这三个字背后的一系列问题扒干净:什么情况下分布式锁会失效?主从切换会不会丢锁?业务执行时间超过了锁的过期时间怎么办?为什么你明明加了锁,线上还是出现了并发问题?
这篇文章不是给你讲怎么背面试题的,而是基于真实业务场景,把分布式锁从设计到上线,再到最佳实践完整过一遍。无论你是刚接触分布式系统的初级工程师,还是已经在维护高并发系统的老鸟,都会有一些可以参考的地方。
1. 从单机锁到分布式锁:问题到底出在哪
1.1 你写的锁到底锁住了什么
很多人在学习分布式锁之前,可能连单机锁都没吃透。理解分布式锁的第一步,是先搞清楚单机锁和分布式锁的本质区别。
单机环境下,比如你在一个 JVM 进程内使用 synchronized 或者 ReentrantLock,你的锁对象是某个类的实例或 Class 对象,JVM 内存是共享的,所以线程之间靠内存里的一个 flag 就能完成互斥。这个锁的生命周期是跟着 JVM 进程走的。
分布式环境下,你的代码跑在多个机器上,每个机器上的 JVM 内存是隔离的。你现在想要实现对共享资源的互斥访问,比如同时扣减同一个商品的库存,就需要一个所有进程都能访问到的“公共状态”来充当锁的载体。这个载体可以是一台 Redis、一个 Zookeeper 集群、一个 etcd,或者是一张数据库表。
这就是分布式锁的入门认知:如果 A 进程拿到了锁,那么 B、C、D 进程在尝试获取锁的时候必须被拦住。但就是这三岁小孩都懂的互斥要求,在分布式场景下会衍生出非常多的边界问题。
1.2 分布式锁的三大核心难题
我对团队里新同学讲分布式锁的时候,从来不会一上来就讲 Redis 命令怎么写,而是先把这三道坎立在那里:
第一道坎是死锁防护。假设 A 进程拿到了锁,但还没来得及释放,进程就 crashed 了。如果这把锁没有自动过期机制,整个系统的其他进程会永远卡死。所以任何分布式锁方案都必须要有一个“租约”或者“TTL”的概念,锁最终一定是要能被自动释放的。
第二道坎是锁的归属权。进程 A 拿到锁之后,因为 GC 停顿等原因阻塞了很长时间,锁已经过期了。此时进程 B 成功加锁,然后进程 A 恢复过来了,继续执行自己的业务代码,然后以为自己在持锁状态,直接释放了锁——这就把进程 B 的锁给删掉了,后果是灾难性的。这就是著名的“误删他人锁”问题。
第三道坎是互斥的绝对性与可用性之间的权衡。你选择牺牲一致性去换取更大的可用性,还是要严格互斥哪怕系统不可用?这个问题不会有绝对的答案,完全取决于你的业务场景:库存扣多了是钱的问题,而有些场景下互斥失效可能带来更严重的后果,这就对锁的可靠性提出了更高的要求。
这些难题不是简单用 Redis 一条命令就能解决的,而是需要从整体设计上规避。下面我会把主流的几种实现方案逐一拆开,仔细分析它们的实现原理和可靠性边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis 分布式锁的演进路程:从 setnx 到原子指令再到可重入与红锁
2.1 第一代方案:setnx 加 expire 的“经典”死锁陷阱
网上很多老项目里还在用这套方案:
code复制SETNX key 1
EXPIRE key 10
// 业务逻辑
DEL key
先说结论:这套方案存在明显的设计缺陷。SETNX 和 EXPIRE 是两条独立执行的命令,如果在 SETNX 成功之后、EXPIRE 执行之前,进程突然崩溃了,那么这个 key 就永久存在,其他进程永远拿不到锁,死锁。
当然有解决办法,就是改用 Lua 脚本把两条命令原子化:
lua复制if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
但即使这样,也不能解决前面讲的“误删他人锁”的问题。具体场景就是:A 进程拿到锁,业务执行时间太长锁过期了,B 进程进来拿了锁,然后 A 业务执行完,执行 DEL key,把 B 的锁给删了。所以你
