Redis 分布式锁(SETNX + Lua)
候选人小林在阿里的二面中,面试官问道:
"Redis 怎么实现分布式锁?"
小林说:"SETNX 加锁,然后设置过期时间,最后 del 解锁。"面试官点点头,继续追问:
"如果 SETNX 加了锁,服务崩溃了,还没来得及设置过期时间怎么办?"
小林愣了一下,说:"...用 EXPIRE 设置过期时间?"
面试官打断:"SETNX 和 EXPIRE 是两个命令,不是原子的,怎么保证?"
小林开始慌了。
【面试官心理】 这道题我用来区分"会用"和"会用且理解原理"的候选人。知道 SETNX + EXPIRE 的占 60%,能说出 SET + EX = 原子的占 30%,能说清 owner 验证和 watchdog 的占 10%。分布式锁是 Redis 最经典的应用之一,但真正能用对的人不到 10%。
一、最简单的分布式锁 🔴
1.1 问题拆解
分布式锁的核心需求:
1.2 朴素实现的问题
1.3 ❌ 错误示范
候选人原话:"用 SETNX 加锁,设置过期时间就行了。"
问题诊断:
- 不知道 SETNX + EXPIRE 不是原子的
- 不理解 SET 命令的 NX 和 PX 选项
- 没有考虑 owner 验证
- 没有考虑锁续期
面试官内心 OS:"这个候选人肯定没有在生产环境中用过分布式锁,否则一定会遇到死锁或锁误删的问题。"
二、正确的分布式锁 🔴
2.1 SET + NX + PX = 原子加锁
Redis 2.6.12 之后,SET 命令支持同时设置 NX 和 PX:
2.2 解锁:原子验证 + 删除
问题:客户端 A 的锁过期了(30秒还没执行完),客户端 B 加锁成功。但客户端 A 业务完成后,用自己的 token 删了锁——删的是 B 的锁!
解决方案:解锁前必须验证 token 是否匹配。
2.3 完整分布式锁代码
【面试官心理】 我连环追问的目的是让候选人意识到:分布式锁的坑比想象中多得多。SETNX + EXPIRE 不是原子(老版本问题)、del 不验证 owner(会误删)、没有锁续期(业务超时导致锁自动释放)——这三个问题都是生产环境中真实踩过的坑。
三、锁续期问题 🔴
3.1 锁续期的场景
3.2 Watchdog(看门狗)机制
Redisson 实现了自动锁续期:
3.3 手动续期方案
如果不使用 Redisson,需要自己实现续期:
四、可重入锁 🟡
4.1 什么是可重入?
同一线程可以多次获取同一个锁:
4.2 可重入锁的实现
五、生产避坑
:::warning ⚠️ 生产中的三大翻车点:
-
SETNX + EXPIRE 分开执行:Redis 老版本中 SETNX 和 EXPIRE 是两个命令,服务崩溃在两者之间会导致死锁。解决方案:使用 SET key value NX PX milliseconds。
-
解锁时不验证 owner:解锁时直接 del 而不是验证 token,会误删其他客户端的锁,导致并发问题。解决方案:Lua 脚本保证原子性。
-
业务执行时间超过 TTL:锁自动过期,但业务还在执行,导致多个客户端同时持有锁。解决方案:使用 Redisson 的 watchdog 机制或手动续期。 :::
Redis 分布式锁 vs Zookeeper 分布式锁:
Redisson 最佳实践:
:::tip 💡 生产最佳实践:
- 单机 Redis 锁:SET key token NX PX 30000 + Lua 解锁
- 高可靠场景:使用 Redisson(自动 watchdog)+ 多节点 RedLock
- 性能敏感:锁的粒度要尽可能小(按数据 ID 锁,而不是按业务类型锁)
- 超时设置:锁超时 = 正常业务时间 × 2,避免业务没执行完锁就过期
- 监控:监控锁的等待时间、获取成功率等指标 :::
【面试官心理】 这道题我想最终验证的是候选人的"工程严谨性"。能把 SETNX + EXPIRE 问题说清楚的占 30%,能把 owner 验证说清楚的占 20%,能把 watchdog 说清楚的占 10%。分布式锁是 Redis 最经典的应用,但 90% 的人实现都是错的。这道题能答到最后的,基本都有过实际踩坑经验。