6. 分布式:锁 / Redisson / 续期与误删
❓ 面试官:什么是分布式锁?为什么在 Redis 中能实现分布式锁?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): 在分布式系统中,普通的 Java synchronized 锁只能控制单个 JVM 进程内的并发,无法控制多个服务器实例(JVM)同时操作同一个资源。分布式锁就是为了解决跨 JVM 的互斥访问。 Redis 能实现分布式锁,核心是利用了它的单线程特性和 SETNX(SET if Not eXists)命令。
📝 详细原理解析:
- 获取锁:客户端向 Redis 发送一条命令:
SET resource_name my_random_value NX PX 30000。NX:表示如果这个 Key(也就是锁)不存在,我才设置成功(获取到锁)。如果已经存在了,我就设置失败(获取锁失败,别人占着)。PX 30000:表示给这个锁加上 30 秒的过期时间。
- 执行业务:拿到锁的客户端去数据库里扣减库存、或者去调用第三方支付接口。
- 释放锁:业务执行完了,客户端向 Redis 发送
DEL resource_name把锁删掉,别人就可以来抢了。
🌟 面试加分项(实战经验): 面试官常追问:“为什么非要加上 NX 和 PX 这两个参数?我先 SETNX 再 EXPIRE 设置过期时间不行吗?” 答:绝对不行!如果在你执行完 SETNX 获取到锁之后,还没来得及执行 EXPIRE 你的服务器突然宕机了,这个锁就会变成死锁,永远无法被释放。NX 和 PX 必须作为一个原子操作一起发给 Redis,这就是为什么 Redis 2.6 之后在 SET 命令里原生支持了这两个参数。
❓ 面试官:如果我的业务代码还没执行完,但是你设置的 30 秒过期时间到了,锁自动释放了怎么办?(锁超时问题)【中级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 这就是经典的锁超时问题,会导致并发安全失效(两个线程同时持有锁)。业界标准的解决方案是使用 Redisson 框架,它内置了 "看门狗"(Watch Dog)机制,能自动为还没有执行完业务的锁续期。
📝 详细原理解析:
- 问题场景:线程 A 获取了锁,过期时间 10 秒。但 A 的业务代码执行了 15 秒才结束。在第 10 秒的时候,Redis 自动把锁删了。这时候线程 B 跑来获取锁,一看没锁,就拿到了。此时 A 和 B 都在执行扣库存代码,并发安全就彻底崩盘了。
- Redisson 怎么解决的?:
- 当 A 获取锁成功后,Redisson 会在后台默默启动一个定时任务(看门狗)。
- 默认情况下,如果你的锁设置了 30 秒过期时间,看门狗会每隔
10 秒(过期时间的三分之一)去检查一下:A 的业务代码还在跑吗?如果还在跑,看门狗就向 Redis 发送一段 Lua 脚本,把锁的过期时间重新重置回 30 秒(续命)。 - 只要业务没执行完,看门狗就会一直续命下去。如果 A 所在的服务器宕机了,看门狗线程也会跟着挂掉,不再续命,30 秒后 Redis 自动删锁,避免死锁。
🌟 面试加分项(底层原理探讨): 面试官可能会问:"那如果我在业务里手动抛了异常退出,或者忘记手动调用 unlock(),看门狗会一直续命导致死锁吗?" 绝杀回答: "不会。首先,规范的代码一定会把 unlock() 放在 finally 块里确保执行;其次,即使业务抛出了异常退出了,看门狗机制也能感知到持有锁的线程已经不存在(或者说与 Redisson 客户端断开了连接),就不再给它续命了,锁最终会正常过期。"
❓ 面试官:如果线程 A 业务执行太慢导致锁被自动过期释放了,线程 B 拿到了锁。这个时候 A 终于执行完了,去执行 DEL 释放锁,会不会把 B 刚刚加上的锁给误删了?【中级】
频率:🔥🔥🔥
💡 一句话总结(先抛结论): 如果不做特殊处理,一定会误删。解决方案是:在加锁的时候,设置一个唯一且随机的 Value(比如 UUID + 线程ID);在释放锁的时候,先判断 Redis 里这个 Key 的 Value 是不是自己之前设置的,是自己的才能删。
📝 详细原理解析:
- 错误的释放姿势:不管三七二十一,直接调用
jedis.del("my_lock")。 - 正确的释放姿势:java
String myValue = UUID.randomUUID().toString() + Thread.currentThread().getId(); // 加锁 jedis.set("my_lock", myValue, "NX", "PX", 30000); // ... 漫长的业务 ... // 释放锁前先判断 if(myValue.equals(jedis.get("my_lock"))) { jedis.del("my_lock"); } - 关键点(Lua 脚本):上面的 "判断 + 删除" 是两步操作,不是原子的!如果在你判断完
equals是true的那一瞬间,你的锁刚好因为超时被 Redis 删了,然后 B 抢到了锁。紧接着你执行del,还是会把 B 的锁给误删了。 因此,释放锁必须使用 Lua 脚本 把这两步封装成一个原子命令丢给 Redis 执行。这也正是 Redisson 底层unlock()的实现方式。
❓ 面试官:你了解 Redlock(红锁)算法吗?在 Redis 集群或者主从架构下,分布式锁会遇到什么极端问题?【高级】
频率:🔥🔥
💡 一句话总结(先抛结论): 在 Redis 主从架构下,如果主节点宕机,由于主从同步是异步的,会导致新选出的主节点上丢失了刚刚加上的锁,从而导致多个客户端同时持有同一把锁。为了解决这个问题,Redis 作者提出了 Redlock 算法。
📝 详细原理解析:
- 主从架构下的锁失效场景:
- 客户端 A 向 Master 节点成功申请了一把锁
my_lock。 - Master 节点还没来得及把这个锁数据同步给 Slave 节点,Master 突然挂了。
- 哨兵机制立刻把 Slave 提升为新的 Master。
- 此时客户端 B 跑来申请锁,新的 Master 一看自己没这个锁,直接把锁发给了 B。
- 结果:A 和 B 同时拿到了同一把锁。并发彻底失效。
- 客户端 A 向 Master 节点成功申请了一把锁
- Redlock 是怎么解决的?:
- 不再使用主从架构。假设我们部署了 5 个完全独立(互相之间没有主从复制关系)的 Redis 节点。
- 客户端在申请锁时,必须向这 5 个节点分别发送加锁请求。
- 如果客户端能在大多数节点(至少 3 个)上成功加锁,且整个加锁过程耗费的时间小于锁的过期时间,就认为加锁成功。
- 即使其中 1~2 个节点挂了,也不会影响这把锁的全局互斥性。
🌟 面试加分项(架构演进与争议探讨): 面试官可能会问:"你们生产环境有用 Redlock 吗?" 绝杀回答: "我们生产环境并没有用 Redlock,因为它的部署和维护成本极高(需要 5 个独立节点),而且在严重的时钟漂移(网络延迟极高、GC 卡顿极长)下依然可能出错。业界大佬 Martin Kleppmann 曾撰文强烈抨击过 Redlock 算法的严谨性。 对于 99.9% 的公司来说,普通的 Redisson 锁(主从+哨兵或 Cluster)偶尔因为主节点宕机导致极小概率的并发冲突,业务上是完全可以接受的(大不了数据库层面做个乐观锁兜底)。如果你们公司的业务真的是涉及上亿资金的绝对不容忍一丝差错的金融核心系统,请不要用 Redis 做分布式锁,直接用 ZooKeeper 吧! 因为 ZK 的 CP 架构(强一致性)天生就比 Redis 的 AP 架构更适合做分布式锁。"