在单体架构时代,遇到并发问题,我们直接上 synchronized 或者 ReentrantLock 就能轻松搞定。但一到微服务、分布式时代,这些本地锁就集体罢工了。这时候,我们通常会请出 Redis 来救场,实现分布式锁

很多人拍脑袋一想,Redis 做分布式锁还不简单?用 SETNX(Set if Not eXists)不就行了?抢到返回 1,没抢到返回 0。

但如果你在生产环境真敢这么写,马上就会被现实教做人。Redis 分布式锁其实是一路升级打怪的过程,里面藏着无数个深坑。今天咱们就用大白话,把实现 Redis 分布式锁会遇到的坑,以及怎么填坑,挨个捋一遍。


第一个坑:服务宕机,死锁诞生了

场景:线程 A 用 SETNX 抢到了锁,开开心心去执行业务代码。结果刚执行到一半,这台服务器突然停电宕机了。
问题:锁还在 Redis 里,但原本要释放锁的线程 A 已经“阵亡”了。这把锁永远没人去释放(DEL 操作没执行),其他服务器上的线程再也抢不到锁,整个业务彻底卡死。这就是最经典的死锁

怎么解?
加一个过期时间(TTL)。就算服务器挂了,只要时间一到,Redis 自动把锁清理掉,其他线程就能重新抢锁了。


第二个坑:加锁和设置过期时间不是原子操作

场景:既然要加过期时间,那我就写两行代码呗:

  1. SETNX lock_key 1 (加锁)

  2. EXPIRE lock_key 10 (设置 10 秒过期)
    问题:这依然有死锁的风险。万一第一步 SETNX 刚执行完,还没来得及执行第二步 EXPIRE,服务器就宕机了呢?这两步操作不是原子的(要么都成功,要么都失败),中间是可以被打断的。

怎么解?
在 Redis 2.6.12 版本之后,官方提供了一个原子的命令,把这两个操作合二为一了:
SET lock_key 1 NX PX 10000
这句话的意思是:如果 key 不存在就设置(NX),并且设置 10000 毫秒的过期时间(PX),一气呵成。


第三个坑:不小心释放了别人的锁

场景:现在我们用了原子操作,也有了过期时间,看起来很完美了对吧?看看下面这个流程:

  1. 线程 A 抢到了锁,过期时间是 10 秒。

  2. 线程 A 的业务太复杂,或者遇到了网络卡顿,执行了 15 秒还没完。

  3. 第 10 秒的时候,Redis 发现锁过期了,自动释放了。

  4. 此时线程 B 跑过来,顺利抢到了锁,开始执行业务。

  5. 第 15 秒,线程 A 终于执行完了,它顺手执行了一句 DEL lock_key 来释放锁。
    问题:线程 A 把线程 B 正在用的锁给删了!然后线程 C 又能抢到锁,并发彻底乱套了。

怎么解?
解铃还须系铃人,谁加的锁,只能谁来解
加锁的时候,不要随便存个 1,而是存一个唯一的标识(比如 UUID 或者当前线程的 ID)。
SET lock_key 唯一标识 NX PX 10000
释放锁的时候,先查一下这个 key 的值是不是自己的唯一标识。如果是,才执行删除。

注意!“判断是不是自己”和“删除”又是两步操作,为了保证原子性,这里必须使用 Lua 脚本交给 Redis 执行。


第四个坑:锁到期了,但业务还没执行完

场景:这就紧接着刚才的第三个坑。虽然我们通过唯一标识防止了“误删别人的锁”,但核心问题没解决啊:线程 A 的业务没执行完,锁就被 Redis 提前回收了,导致 A 和 B 同时在跑业务,这还叫什么锁?

有人说:“那我把过期时间设置长一点,设成 1 分钟、1 小时不行吗?”
不行。万一服务器真挂了,别的线程得干等 1 小时才能接盘,系统性能直接扑街。

怎么解?
这就需要引入大名鼎鼎的**“看门狗(Watch Dog)”机制**了。
这事儿不用自己写代码,直接上工业级开源框架 Redisson。它的原理是:
假设设置锁默认过期时间为 30 秒。当你抢到锁之后,Redisson 会在后台偷偷开启一个线程(看门狗),每隔 10 秒(过期时间的 1/3)去检查一下:
“兄弟,你业务干完了没?没干完的话,我帮你跟 Redis 说一声,把过期时间重新续回到 30 秒。”
这样一来,只要你的业务没执行完,锁就不会失效;如果你的服务器宕机了,看门狗也跟着死了,没人续期,锁到期自然释放。堪称完美!


第五个坑:Redis 主从切换导致锁丢失

场景:稍微大一点的公司,Redis 肯定不是单机,而是“主从集群”(一主多从)。

  1. 线程 A 在 主节点(Master) 抢到了锁。

  2. Master 还没来得及把这把锁的数据同步给 从节点(Slave),Master 突然死机了。

  3. 哨兵机制发现 Master 死了,赶紧把其中一个 Slave 提拔成新的 Master。

  4. 这个新的 Master 上压根没有线程 A 的锁数据

  5. 线程 B 跑来一抢,成功获取了锁。并发安全再次崩溃。

怎么解?
为了解决这个问题,Redis 的作者提出了 Redlock(红锁)算法
大致原理是:搞 5 个相互独立的 Redis Master 节点。线程来抢锁的时候,必须在过半的节点(比如 3 个以上)都加锁成功,才算真正抢到了锁。就算挂掉一两个节点,也不会影响整体的正确性。

但说句实在话,实际生产中很少人用 Redlock。
为什么?因为太重了,性能极差,而且搭建和维护成本太高。
业界的普遍共识是
如果你追求绝对的强一致性(比如金融级资金转账,一分钱都不能差),那请你放弃 Redis 锁,直接去用 Zookeeper 做分布式锁,ZK 从底层机制上就保证了数据的强一致性。
如果是普通的电商秒杀、积分扣减等日常业务,直接用 Redisson 的单机锁或集群锁就足够了,极小概率的主从切换丢锁问题,靠数据库层面的乐观锁兜底或者人工对账补偿即可,没必要为了 0.01% 的概率牺牲 99.99% 的性能。


总结:抄作业时间

看了这么多坑,是不是觉得手写 Redis 分布式锁简直是个灾难?
没关系,前人栽树后人乘凉,我们只需要记住最终的实战结论:

  1. 绝对不要自己用 SETNX 去手写分布式锁的逻辑,坑太多你填不过来。

  2. 日常 Java 开发,直接引入 Redisson 框架。

  3. 使用 Redisson 的 RLock,它自带了Lua脚本防误删看门狗自动续期、甚至还支持可重入锁。一行代码搞定:

    RLock lock = redisson.getLock("myLock");
    lock.lock();
    try {
        // 执行业务逻辑
    } finally {
        lock.unlock(); // 别忘了放在 finally 里释放
    }
  4. 如果面试官问你原理,把上面说的死锁、误删、原子性、看门狗、主从丢失这 5 个坑挨个讲给他听,保准他频频点头,直接发 Offer。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐