案发场景:
晚上 8 点,电商秒杀系统上线。为了防止商品超卖,你在扣库存的代码外层加了一把 Redis 分布式锁。
你的青铜代码:
// 1. 加锁,设置 10 秒过期时间(防止服务器宕机死锁)redis.setIfAbsent("lock:sku:10086","1",10,TimeUnit.SECONDS);try{// 2. 执行扣库存业务逻辑doBusiness();}finally{// 3. 释放锁redis.delete("lock:sku:10086");}灾难降临(锁失效与误删并发):
假设此时发生了网络抖动,或者 JVM 触发了一次长达 5 秒的 Full GC。导致你的doBusiness()耗时达到了15 秒!
- 第 10 秒:锁过期了,Redis 自动释放了锁。
- 第 11 秒:线程 B 以为没人加锁,成功抢到了锁,开始执行业务。此时,线程 A 和 线程 B 都在扣同一个商品的库存!超卖发生!
- 第 15 秒:线程 A 终于执行完了。它来到了
finally块,执行delete。可怕的事情发生了:线程 A 把线程 B 刚刚加上的锁给删掉了! - 第 16 秒:线程 C 又趁虚而入……
整个系统瞬间变成了“裸奔”状态,你的SETNX成了一个彻头彻尾的笑话。
1. 锁的自我修养:如何防止“误删别人的锁”?
为了解决案发场景中的“误删”问题,架构师们迈出了进阶的第一步:身份验证。
加锁的时候,不能随便塞个"1"进去,必须塞入当前线程的唯一标识 (UUID + ThreadId)。
释放锁的时候,不能闭着眼睛DEL,必须先查一下:“这把锁还是不是我的?是我的我才删。”
为了保证“查询并删除”这两个动作的绝对原子性,我们必须使用上一期讲过的Lua 脚本。
-- Lua 脚本:安全的释放锁ifredis.call("GET",KEYS[1])==ARGV[1]thenreturnredis.call("DEL",KEYS[1])elsereturn0end解决了误删问题,那“业务没执行完,锁却提前过期了”的问题怎么解决?
2. 终极矛盾:TTL 的生死困局
过期时间 (TTL) 设多久永远是个两难的哲学问题。
- 设短了(比如 5 秒):稍微遇到个慢 SQL,业务还没跑完,锁就没了,并发安全被击穿。
- 设长了(比如 5 分钟):如果拿到锁的服务器突然停电宕机了,这把锁要等 5 分钟才会自动释放。这 5 分钟内,所有其他用户的请求全部被阻塞,业务陷入死寂。
我们需要一种动态伸缩的机制:
“只要我的业务还在执行,你就一直帮我把锁的时间延长;只要我的服务器宕机了,你就不再延长,让它立刻过期释放。”
这,就是名震江湖的Redisson WatchDog (看门狗) 机制。
3. 破局者:Redisson WatchDog 底层魔法
Redisson 是目前最强大的 Java Redis 客户端之一,它彻底封装了分布式锁的复杂底层逻辑。
当我们调用redissonClient.getLock("myLock").lock()时,一场精妙的“续租”游戏就在后台悄悄上演了。
看门狗的工作原理:
- 默认租期:加锁成功时,Redisson 默认给这把锁设置30 秒的 TTL。
- 启动定时任务 (TimerTask):Redisson 会在后台线程开启一个定时任务(这只狗)。
- 三分之一续期法:这只看门狗会每隔10 秒(即默认 30 秒的 1/3)醒来一次。
- 探活与续租:它会去检查一下,拿着这把锁的线程是不是还活着、业务是不是还在跑?如果是,它就会向 Redis 发送一段 Lua 脚本,重新把这把锁的 TTL 刷新回 30 秒!
- 安全释放:
- 如果业务正常执行完,调用了
unlock(),Redisson 会主动删除锁,并且把看门狗任务取消掉。 - 极限宕机测试:如果拿着锁的 Java 进程突然被
kill -9杀死了,或者断电了。那么看门狗线程也会跟着一起死掉!没人再给锁续期了。经过最多 30 秒,Redis 里的锁就会自然过期释放,完美避免死锁!
- 如果业务正常执行完,调用了
4. 代码落地:Spring Boot + Redisson 实战与惊天巨坑
接入 Redisson 非常简单,但里面隐藏着一个无数高级研发都会踩中的巨坑。
importorg.redisson.api.RLock;importorg.redisson.api.RedissonClient;importorg.springframework.stereotype.Service;importjava.util.concurrent.TimeUnit;@ServicepublicclassOrderService{privatefinalRedissonClientredissonClient;publicOrderService(RedissonClientredissonClient){this.redissonClient=redissonClient;}publicvoidcreateOrder(){// 1. 获取锁对象 (只是拿到引用,还没去 Redis 加锁)RLocklock=redissonClient.getLock("lock:order:create");// --- 错误写法 (巨坑警告!) ---// lock.lock(10, TimeUnit.SECONDS);// 很多人习惯性地加个 10 秒参数,以为更安全。// 殊不知!只要你传了 leaseTime 参数,Redisson 就会认为你自己能掌控过期时间,// 从而 彻底关闭 WatchDog 看门狗机制! 导致锁在 10 秒后必定失效!try{// --- 正确写法 (召唤看门狗) ---// 只有无参的 lock(),或者 tryLock 的 leaseTime 传 -1 时,// 才会触发 WatchDog 机制!默认锁 30 秒,并每 10 秒自动续期。lock.lock();// 或者带有等待时间,但不传 leaseTime (传 -1) 的 tryLock// boolean isLocked = lock.tryLock(5, -1, TimeUnit.SECONDS);System.out.println("成功获取锁,看门狗已就位,开始执行长达 40 秒的超慢业务...");Thread.sleep(40000);}catch(Exceptione){e.printStackTrace();}finally{// 安全释放锁 (Redisson 内部会判断这把锁是不是当前线程的,不会误删)if(lock.isLocked()&&lock.isHeldByCurrentThread()){lock.unlock();}}}}5. 进阶探讨:分布式锁的终极梦魇 —— Redlock (红锁)
有了 Redisson 看门狗,你的业务在单机 Redis 上已经 100% 安全了。
但是!如果你的 Redis 部署的是主从集群 (Master-Slave)呢?
CAP 定理的无情制裁:
- 线程 A 在 Master 节点加锁成功。
- 还没来得及异步同步给 Slave 节点,Master 机器主板烧了!
- 哨兵机制 (Sentinel) 立刻将 Slave 提升为新的 Master。
- 线程 B 过来加锁,发现新的 Master 上没有锁,加锁成功!
- 灾难再次发生:线程 A 和 线程 B 同时拥有了锁!
为了解决这个极端问题,Redis 作者 Antirez 提出了Redlock (红锁)算法。
其核心思想是:不搞主从复制了。直接部署 5 台独立的 Redis Master 机器。客户端加锁时,并发向这 5 台机器发送命令。只要**超过半数(3台)**加锁成功,才算真正拿到了锁。
架构师的权衡:要不要上 Redlock?
在工业界,Redlock 引发了极其激烈的争论(著名的 Martin vs Antirez 世纪辩论)。
绝大多数互联网大厂的最终结论是:不推荐使用 Redlock。
- 它的运维成本极高(需要 5 台独立 Master)。
- 它严重依赖服务器时间的同步(NTP 时钟跳跃会导致红锁失效)。
- 终极妥协:如果你的业务对数据一致性要求极其变态(比如金融级核心账本),你不应该用 Redis 做锁,你应该去用 ZooKeeper(基于 CP 模型,强一致性)或数据库级悲观锁。Redis(基于 AP 模型)天生就是为了高性能而生的,容忍极小概率的主从切换锁丢失,换取极致的吞吐量,这才是它的归宿。
总结
分布式锁的演进,是一场与并发、网络、宕机时间进行极限博弈的战争。
从裸奔的SETNX,到带过期时间的SET EX NX;从 Lua 脚本防误删,再到 Redisson WatchDog 的自动续期守护。我们穷尽了工程学的智慧。
最后请记住这个铁律:使用 Redisson 时,如果你不懂底层原理,千万不要画蛇添足地给lock()方法传过期时间参数。相信那只默默无闻的看门狗,它比你更懂什么时候该放手。