分布式锁的代价与选择:为什么我们最终拥抱了Redisson?
写在前面的话
不知道你有没有过这种经历:在本地开发测试时一切顺风顺水,逻辑严丝合缝。可一旦代码部署到线上,面对高并发的真实流量,各种匪夷所思的数据异常就开始冒头了。
我最早遇到的"库存超卖"就是这样一个典型案例。从最初相信 Java 自带的锁,到后来手写 Redis 锁,再到最后折腾出稳定方案,这个过程其实就是对"并发"二字理解不断加深的过程。
今天想聊聊这块内容,不堆砌概念,只讲讲这条路是怎么一步步走过来的。
一、一切的起点:synchronized 的舒适区
刚开始写代码时,思维往往停留在"单机"模式。遇到需要控制并发的地方,直觉反应就是加个 synchronized 关键字。
1. 曾经写过的代码
// 简单的库存扣减
public synchronized void deductStock(String productId) {
// 1. 查询库存
Product product = stockMapper.selectById(productId);
// 2. 判断并扣减
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
stockMapper.updateById(product);
}
}
2. 这个方案能用吗?
能用,但有前提。
如果你的系统是一个简单的后台管理系统,或者是一个单节点部署的内部工具,并发量极低,那么 synchronized 完全足够。它简单、高效,且无需引入外部依赖,是解决单机并发问题的"如意金箍棒"。
3. 为什么后来不行了?
问题的关键在于”跨进程“。
当业务发展,服务需要部署两台甚至更多服务器时,每台服务器都有一个独立的 JVM。
- 服务器 A 的
synchronized锁住了它自己的线程。 - 服务器 B 的
synchronized锁住了它自己的线程。 - 结果:A 和 B 同时放行了一个请求,扣减了同一件商品。库存立刻变负数。
这时候我们意识到:我们需要一把能管得住所有服务器的"大锁"。
二、初尝分布式锁:Redis SETNX 的尝试
既然 JVM 内部的锁不管用了,那自然要找一个所有服务器都能访问到的第三方组件来存这把锁。Redis 因为其高性能和简单的 API,成了首选。
1. 最直观的写法
Redis 有个命令叫 SETNX (SET if Not Exists)。这名字听起来就天生是为了抢占资源设计的。
# 谁先执行成功,谁就抢到了锁
SETNX lock:product:101 1
逻辑很简单:
- 多个服务器同时发
SETNX命令。 - 只有一个能返回
1(成功),其他的返回0(失败)。 - 抢到锁的执行业务,做完之后
DEL删除锁。
2. 现实中的意外
这个方案最大的隐患在于“删锁”这步。
如果代码在执行业务逻辑时,服务器突然断电了,或者进程崩溃了,导致 DEL 命令没来得及发出。
后果:这把锁就像"幽灵"一样永远存在于 Redis 里。后续所有针对这个商品的请求,都会因为拿不到锁而被死死卡住。
改进方案:必须加过期时间。
SETNX lock:product:101 1
EXPIRE lock:product:101 10 # 10秒后自动过期
3. 还是不够完美
SETNX 和 EXPIRE 是两条命令,不是原子操作。如果在第一句和第二句之间由于网络抖动或者服务重启断开了,锁依然会变成"死锁"。
适用场景:
这种简单的 SETNX 方案,在很早期的 Redis 版本或者一些非核心业务(比如简单的定时任务去重)中还可以见到,但在对于数据准确性要求极高的交易核心链路,它显然过于脆弱了。
三、进阶:原子性与"锁不住"的尴尬
吸取了死锁的教训,后来 Redis 官方推出了原子命令,或者我们通用 Lua 脚本来保证操作原子性。
1. 修复死锁问题
# 一条命令搞定加锁和过期时间
SET lock:product:101 uuid NX PX 10000
这就解决了原子性问题。只要锁加上了,由于有过期时间,哪怕服务器爆炸,锁最终也会自动消失,系统能自动恢复。
2. 引入了新问题:锁因为超时提前释放了
假设我们将锁的过期时间设为 10秒。
但那天的数据库特别卡,业务逻辑执行了 15秒。
这就出现了一个严重的逻辑漏洞:
- T0秒:线程 A 加锁成功。
- T10秒:锁自动过期释放。
- T11秒:线程 B 进场,发现没锁,加锁成功。
- T15秒:线程 A 终于执行完了,发起
DEL删除锁。- 关键点:此时 A 删掉的,其实是 B 的锁!
这就导致了连锁崩溃:锁失效 -> A 删 B 的锁 -> B 裸奔 -> B 删 C 的锁...
适用场景:
这种方案适用于业务执行时间非常短且稳定的场景。但只要涉及网络调用(如第三方支付、跨服务调用),执行时间不可控,这种固定过期时间的方案就始终悬着一把剑。
四、最终方案:Redisson 的守候
为了解决"锁过期时间不好估算"的痛点,Redisson 带着它的看门狗(WatchDog) 机制出现了。这也许是目前 Java 生态中最成熟的分布式锁方案。
1. 什么是看门狗?
其实原理很朴素:既然我不知道业务要跑多久,那我能不能搞个"助理"在后台盯着?
简单来说就是:
- 只要业务线程还在跑,"看门狗"会每隔一会儿就去 Redis 喊一声:"大哥,还没完呢,给我续个杯!"
- Redis 收到通知,就把过期时间重新填满。
- 如果业务线程挂了,看门狗也没了,没人续杯,锁自然就过期了。
2. 使用起来的感受
代码变得异常清爽,仿佛回到了单机锁的时代:
// 1. 获取锁对象
RLock lock = redisson.getLock("lock:product:101");
try {
// 2. 加锁(开启看门狗,默认30秒过期,每10秒续期一次)
lock.lock();
// 3. 执行业务(哪怕跑了1分钟,锁也不会丢)
complexBusinessLogic();
} finally {
// 4. 释放锁(只有当锁存在,且是当前线程加的锁时,才释放)
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
3. 稳在哪儿?
Redisson 帮我们把最难处理的几个点屏蔽了:
- 自动续期:不用纠结
expire设置多少秒合适。 - 防止误删:解锁时会校验线程 ID,不会删掉别人的锁。
- 可重入:和
synchronized一样,同一个线程可以多次获取同一把锁。
适用场景:
几乎涵盖了所有需要强一致性的分布式并发场景。无论是秒杀扣库存、金融账户扣款,还是定时任务的分发执行,Redisson 都是目前最稳健的选择。
五、集群下的隐忧:Redlock 是救世主吗?
讲到这里,很多细心的朋友可能会问:
"如果 Redis 是主从集群(Cluster),主节点挂了,锁还没同步到从节点,从节点升级为主,锁不就丢了吗?"
这一针见血。
为了解决这个问题,Redis 之父 Antirez 提出了 Redlock 算法:让客户端向 N 个独立的 Redis 节点同时申请锁,只要超过半数(N/2+1)申请成功,就认为获取了锁。
1. 为什么我不推荐 Redlock?
在实际工程落地中,Redlock 的投入产出比(ROI)并不高:
- 部署成本高:你需要至少 3 个(最好 5 个)完全独立的 Redis 实例,而不是主从集群。
- 性能折损:客户端要顺序去多个节点加锁,网络开销成倍增加。
- 并非绝对安全:分布式系统的时钟跳跃(Clock Drift)或者长 GC 依然可能打破 Redlock 的安全性(这也是著名的 Martin Kleppmann 与 Antirez 辩论的焦点)。
2. 更有性价比的选择
如果你的业务真的无法容忍哪怕百万分之一的"主从切换丢锁"风险,我的建议是:
- 方案一:独立部署
专门部署一个单机版 Redis 实例(不做集群),只用来存锁。哪怕它挂了,整个业务熔断,也好过并发乱了。简单粗暴,但极其有效。 - 方案二:拥抱强一致性(CP)
如果锁的一致性比可用性更重要(比如涉及资金转账),请转身拥抱 ZooKeeper 或 Etcd。它们天生就是为 CP(强一致性)设计的,不要勉强 AP(高可用)的 Redis 做它不擅长的事。 - 方案三:更通用的选择
在 99.9% 的业务场景下,接受 Redis 主从切换可能带来的极短暂锁丢失风险。
想一想,主节点宕机的概率是多少?正好在宕机那几毫秒持有锁的概率是多少?为了解决这微乎其微的概率,引入复杂的 Redlock,往往得不偿失。
六、最后的一点心得
技术方案的演进,本质上是在做取舍。
- Synchronized 胜在简单,败在扩展。
- Redis SETNX 胜在性能,败在极端情况的可靠性。
- Redisson 胜在可靠和完备,但在集群极端场景下依然有软肋。
- Zookeeper 胜在强一致,但性能和维护成本是硬伤。
在实际工作中,我们不必言必称 Redlock,也不必因为一点点极端风险就焦虑。软件工程没有银弹,只有最适合当下的选择。
很多时候,我们从简单方案过渡到复杂方案,并不是因为想炫技,而是在无数次"掉坑"之后,对代码、对线上的敬畏。但同样,在面对过度设计时,也要有敢于说"不"的底气:如果单实例够用,就别搞集群;如果 Redis 够用,就别上 Redlock。
愿你的代码,既能跑得快,又能扛得住;愿你的架构,既有深度,又有温度。
文章的最后,想和你多聊两句。
技术之路,常常是热闹与孤独并存。那些深夜的调试、灵光一闪的方案、还有踩坑爬起后的顿悟,如果能有人一起聊聊,该多好。
为此,我建了一个小花园——我的微信公众号「[努力的小郑]」。
这里没有高深莫测的理论堆砌,只有我对后端开发、系统设计和工程实践的持续思考与沉淀。它更像我的数字笔记本,记录着那些值得被记住的解决方案和思维火花。
如果你觉得今天的文章还有一点启发,或者单纯想找一个同行者偶尔聊聊技术、谈谈思考,那么,欢迎你来坐坐。
愿你前行路上,总有代码可写,有梦可追,也有灯火可亲。

很多时候,我们从简单方案过渡到复杂方案,并不是因为想炫技,而是在无数次"掉坑"之后,对代码、对线上的敬畏。但同样,在面对过度设计时,也要有敢于说"不"的底气:如果单实例够用,就别搞集群;如果 Redis 够用,就别上 Redlock。

浙公网安备 33010602011771号