🎯 一、为什么需要分布式锁
单机里的 synchronized / ReentrantLock 只管一个 JVM 内的线程。当同一份数据被多个服务实例并发修改(超卖、重复对账、幂等防重),就需要一把所有实例都认的锁。
典型场景:秒杀库存扣减、定时任务在多节点只跑一次、分布式事务防重提交。不加锁 → 超卖/重复执行。
一把合格的分布式锁要满足:互斥(任一时刻仅一个持有者)、不死锁(持有者挂了锁能释放)、谁加的锁谁解(防误删)、容错(锁服务本身高可用)。
🔴 二、Redis 分布式锁(最常用)
原子加锁:
SET key uuid NX EX 30一条命令同时完成"不存在才设"和"设置过期时间",避免"SETNX 后崩溃没设过期"导致的死锁。value 用唯一 uuid 标识自己是持有者。
安全解锁:Lua 脚本校验 uuid 再删
不能直接 DEL——否则可能删掉别人刚拿到的锁。必须用 Lua 原子地"判断 value==我的uuid 才删除"。
看门狗自动续期
业务没跑完锁就快过期?Redisson 的 watch dog 在持有期间每 1/3 超时周期自动续期,直到主动释放。避免"业务没完锁先没"。
主从切换的隐患:主库刚写入锁就宕机,从库还没同步就被提升为主——此时另一客户端也能加锁成功,出现"双持锁"。这就是 Redis 官方 Redlock 想解决的,但 Redlock 在分布式学界仍有争议(Martin Kleppmann 批评其依赖时钟假设)。
🟢 三、ZooKeeper 分布式锁
利用 ZK 的临时顺序节点:客户端在锁目录下创建临时顺序节点,序号最小的获得锁;其余监听前一个节点,前一个释放(节点消失)后自己顺位获得。
优点:ZK 用临时节点保证"客户端宕机会话断开→节点自动消失→锁释放",天然防死锁,且靠顺序节点实现公平排队,比 Redis 在"强一致"上更稳。缺点:性能与吞吐不如 Redis,依赖 ZK 集群。
🗃️ 四、数据库锁(兜底方案)
① 唯一索引 / 悲观锁
插入一条带唯一键的记录表示获锁;或
SELECT ... FOR UPDATE 行锁。简单但 DB 成瓶颈,且连接断开才能释放,一般不作首选。② 乐观锁(版本号 / CAS)
更新时带版本号
UPDATE ... SET val=? , version=version+1 WHERE id=? AND version=old,影响行数为 0 即失败重试。适合冲突少的场景,但不是"锁"而是"无锁并发控制"。📊 五、三种方案对比
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| Redis | 性能极高、实现简单 | 主从切换可能双持锁(弱一致) | 绝大多数高并发业务 |
| ZooKeeper | 强一致、防死锁、公平 | 吞吐较低、依赖 ZK | 对正确性要求极高 |
| 数据库 | 零额外组件 | 性能差、易成瓶颈 | 低频、简单场景兜底 |
🎯 六、面试高频追问
Redis 锁为什么要用 Lua 解锁、用 uuid 做 value?
防止 A 的锁过期后,B 拿到锁,A 执行完却把 B 的锁 DEL 了。uuid 保证只删自己的;Lua 保证"校验+删除"原子,不被中间插入。
锁过期时间设多少?业务没跑完怎么办?
宁大勿小易死锁,宁小勿大易并发。正解是看门狗自动续期:业务在跑就不断续,掉线就停止续期自然释放。
Redis 和 ZK 锁怎么选?
高并发、能接受极小概率双持 → Redis;金融级强一致、正确性优先 → ZK。也可 Redlock 折中(注意其争议)。
延伸阅读:分布式缓存与三大问题 · ZooKeeper 详解