找回密码
 立即注册
首页 业界区 业界 分布式锁的代价与选择:为什么我们最终拥抱了Redisson? ...

分布式锁的代价与选择:为什么我们最终拥抱了Redisson?

枢覆引 7 天前
写在前面的话
不知道你有没有过这种经历:在本地开发测试时一切顺风顺水,逻辑严丝合缝。可一旦代码部署到线上,面对高并发的真实流量,各种匪夷所思的数据异常就开始冒头了。
我最早遇到的"库存超卖"就是这样一个典型案例。从最初相信 Java 自带的锁,到后来手写 Redis 锁,再到最后折腾出稳定方案,这个过程其实就是对"并发"二字理解不断加深的过程。
今天想聊聊这块内容,不堆砌概念,只讲讲这条路是怎么一步步走过来的。
一、一切的起点:synchronized 的舒适区

刚开始写代码时,思维往往停留在"单机"模式。遇到需要控制并发的地方,直觉反应就是加个 synchronized 关键字。
1. 曾经写过的代码
  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)。这名字听起来就天生是为了抢占资源设计的。
  1. # 谁先执行成功,谁就抢到了锁SETNX lock:product:101 1
复制代码
逻辑很简单:

  • 多个服务器同时发 SETNX 命令。
  • 只有一个能返回 1(成功),其他的返回 0(失败)。
  • 抢到锁的执行业务,做完之后 DEL 删除锁。
2. 现实中的意外

这个方案最大的隐患在于“删锁”这步。
如果代码在执行业务逻辑时,服务器突然断电了,或者进程崩溃了,导致 DEL 命令没来得及发出。
后果:这把锁就像"幽灵"一样永远存在于 Redis 里。后续所有针对这个商品的请求,都会因为拿不到锁而被死死卡住。
改进方案:必须加过期时间。
  1. SETNX lock:product:101 1EXPIRE lock:product:101 10  # 10秒后自动过期
复制代码
3. 还是不够完美

SETNX 和 EXPIRE 是两条命令,不是原子操作。如果在第一句和第二句之间由于网络抖动或者服务重启断开了,锁依然会变成"死锁"。
适用场景
这种简单的 SETNX 方案,在很早期的 Redis 版本或者一些非核心业务(比如简单的定时任务去重)中还可以见到,但在对于数据准确性要求极高的交易核心链路,它显然过于脆弱了。
三、进阶:原子性与"锁不住"的尴尬

吸取了死锁的教训,后来 Redis 官方推出了原子命令,或者我们通用 Lua 脚本来保证操作原子性。
1. 修复死锁问题
  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. 什么是看门狗?

其实原理很朴素:既然我不知道业务要跑多久,那我能不能搞个"助理"在后台盯着?
sequenceDiagram    participant Client as 客户端    participant Redisson as Redisson SDK    participant Redis as Redis Server    participant WatchDog as 后台看门狗    Client->>Redisson: 1. 加锁 (lock)    Redisson->>Redis: 2. SETNX + PEXPIRE (Lua脚本)    Redis-->>Redisson: 3. 加锁成功    Redisson-->>WatchDog: 4. 启动定时任务        loop 每隔 10秒 (默认LockWatchdogTimeout/3)        WatchDog->>Redis: 5. 续命 (业务还在跑?TLL重置为30s)    end    Client->>Redisson: 6. 业务结束,解锁 (unlock)    Redisson->>WatchDog: 7. 停止续命任务    Redisson->>Redis: 8. 删除锁 (DEL)简单来说就是:

  • 只要业务线程还在跑,"看门狗"会每隔一会儿就去 Redis 喊一声:"大哥,还没完呢,给我续个杯!"
  • Redis 收到通知,就把过期时间重新填满。
  • 如果业务线程挂了,看门狗也没了,没人续杯,锁自然就过期了。
2. 使用起来的感受

代码变得异常清爽,仿佛回到了单机锁的时代:
  1. // 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)
    如果锁的一致性比可用性更重要(比如涉及资金转账),请转身拥抱 ZooKeeperEtcd。它们天生就是为 CP(强一致性)设计的,不要勉强 AP(高可用)的 Redis 做它不擅长的事。
  • 方案三:更通用的选择
    在 99.9% 的业务场景下,接受 Redis 主从切换可能带来的极短暂锁丢失风险
想一想,主节点宕机的概率是多少?正好在宕机那几毫秒持有锁的概率是多少?为了解决这微乎其微的概率,引入复杂的 Redlock,往往得不偿失。
六、最后的一点心得

技术方案的演进,本质上是在做取舍

  • Synchronized 胜在简单,败在扩展。
  • Redis SETNX 胜在性能,败在极端情况的可靠性。
  • Redisson 胜在可靠和完备,但在集群极端场景下依然有软肋。
  • Zookeeper 胜在强一致,但性能和维护成本是硬伤。
在实际工作中,我们不必言必称 Redlock,也不必因为一点点极端风险就焦虑。软件工程没有银弹,只有最适合当下的选择。
很多时候,我们从简单方案过渡到复杂方案,并不是因为想炫技,而是在无数次"掉坑"之后,对代码、对线上的敬畏。但同样,在面对过度设计时,也要有敢于说"不"的底气:如果单实例够用,就别搞集群;如果 Redis 够用,就别上 Redlock。
愿你的代码,既能跑得快,又能扛得住;愿你的架构,既有深度,又有温度。
文章的最后,想和你多聊两句。
技术之路,常常是热闹与孤独并存。那些深夜的调试、灵光一闪的方案、还有踩坑爬起后的顿悟,如果能有人一起聊聊,该多好。
为此,我建了一个小花园——我的微信公众号「[努力的小郑]」。
这里没有高深莫测的理论堆砌,只有我对后端开发、系统设计和工程实践的持续思考与沉淀。它更像我的数字笔记本,记录着那些值得被记住的解决方案和思维火花。
如果你觉得今天的文章还有一点启发,或者单纯想找一个同行者偶尔聊聊技术、谈谈思考,那么,欢迎你来坐坐。

愿你前行路上,总有代码可写,有梦可追,也有灯火可亲。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册