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

张开发
2026/4/16 10:04:19 15 分钟阅读

分享文章

分布式锁的代价与选择:为什么我们最终拥抱了Redisson?
一、一切的起点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 ServerRedisson SDK客户端后台看门狗Redis ServerRedisson SDK客户端loop[每隔 10秒(默认LockWatchdogTimeout/3)]1. 加锁 (lock)2. SETNX PEXPIRE (Lua脚本)3. 加锁成功4. 启动定时任务5. 续命 (业务还在跑TLL重置为30s)6. 业务结束解锁 (unlock)7. 停止续命任务8. 删除锁 (DEL)简单来说就是只要业务线程还在跑看门狗会每隔一会儿就去 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/21申请成功就认为获取了锁。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胜在性能败在极端情况的可靠性。

更多文章