分布式锁的几种实现方案与常见陷阱
一、为什么需要分布式锁
单机多线程用 synchronized / Lock 就能互斥。但服务一旦部署多实例、或多台机器共享同一份资源(库存、账户余额、定时任务),本地锁就失效了——因为锁只在单个进程内有效。
分布式锁要保证:同一时刻,跨进程/跨机器,只有一个持有者能操作共享资源。实现上有几种主流方案,各有取舍。
二、基于 Redis 的分布式锁
最常见的做法,用 SET key value NX PX ttl 原子地“不存在才设置 + 带过期时间”:
SET lock:order:123 <token> NX PX 30000
NX:键不存在才设置(保证互斥)。PX 30000:30 秒自动过期(防止持锁者宕机导致死锁)。value用唯一 token(如 UUID),释放时校验是自己的锁才删。
释放锁必须用 Lua 脚本保证“校验 + 删除”原子:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
坑一:锁过期但业务没跑完。业务执行时间超了 TTL,锁自动释放,别人拿到锁 → 并发问题。解决:用看门狗(后台线程定期续期,如 Redisson 的 watchdog),或合理评估并设置足够长的 TTL。
坑二:主从切换丢锁。主节点写入的锁还没同步到从库就宕机,主从切换后锁丢失。更强一致可用 RedLock(多节点多数派加锁),但 RedLock 本身也有争议,需权衡。多数业务用单节点/哨兵 + 看门狗已足够。
三、基于数据库的分布式锁
- 唯一索引:用一张锁表,对资源 id 建唯一索引,插入成功即获锁,删除即释放。简单可靠,但 DB 压力大、性能一般,适合低频场景。
SELECT ... FOR UPDATE:行锁,事务内互斥,注意必须走索引否则会锁全表;且锁跟随事务,事务结束自动释放。
数据库方案不依赖额外组件,一致性由 DB 保障,但吞吐低,不适合高并发抢锁。
四、基于 ZooKeeper / etcd 的分布式锁
利用临时顺序节点:在锁节点下创建临时有序子节点,序号最小的持有锁;释放时删除自己节点,后一个监听前一个节点。
优点:
- 会话机制天然防死锁:客户端断连后临时节点自动删除。
- 强一致,适合对一致性要求高的场景(如选主、配置协调)。
缺点:依赖 ZK/etcd 集群,运维成本高;性能不如 Redis。
五、三种方案怎么选
| 方案 | 性能 | 一致性 | 复杂度/运维 | 典型场景 |
|---|---|---|---|---|
| Redis | 高 | 最终一致(单节点+看门狗够用) | 低 | 大多数互联网业务抢锁 |
| 数据库 | 低 | 强(由 DB 事务保证) | 低(已有 DB) | 低频、强一致的小场景 |
| ZK/etcd | 中 | 强 | 高 | 选主、协调、强一致场景 |
原则:优先 Redis(配合看门狗 + token 校验),一致性要求极高再考虑 ZK/etcd,简单低频场景直接用数据库唯一索引。
六、通用正确性要点
- 加锁与过期必须原子(Redis 用
SET NX PX一条命令,别拆成先 set 再 expire)。 - 释放必须校验持有者(token),避免误删别人的锁。
- 设置合理 TTL + 续期机制,防死锁也防“锁提前过期”。
- 加锁失败要有降级/重试策略:立即返回失败、自旋重试还是排队等待,按业务定。
- 锁粒度尽量小:锁到具体资源 id,而不是全局一把大锁,减少争用。
七、常见坑
- 业务超时 > 锁 TTL,导致并发进入(最经典)。
- 释放锁不校验 token,删掉了别人的锁。
- 把
setnx和expire分开调用,中间崩溃就死锁。 - 重入问题:同一线程重复加锁要能重入(Redisson 的可重入锁按线程计数),自己实现的要留意。
- 主从架构下的锁丢失,对强一致场景不够安全。
- 锁范围内做耗时操作(远程调用、大事务),拖长持锁时间放大风险。
八、总结
分布式锁的核心目标是跨进程互斥。工程上最实用的组合是:Redis SET NX PX + 唯一 token + Lua 校验释放 + 看门狗续期,能覆盖绝大多数业务;数据库唯一索引适合低频强一致的小场景;ZK/etcd 用于选主等强一致协调场景。真正决定成败的往往不是选哪个组件,而是TTL、续期、token 校验、锁粒度这些细节是否处理到位。
- 点赞
- 收藏
- 关注作者
评论(0)