· 王思腾 · 系统架构 · 9 分钟阅读
分布式锁原理与实践:从 Redis 锁到 Redlock
单机锁管不住跨进程、跨节点的并发。分布式锁用于在多实例间互斥访问共享资源(防超卖、防重复任务、幂等)。本文先讲清分布式锁的四大要求(互斥、防死锁、可重入、解铃还须系铃人),再逐个聊 SETNX、Lua 原子性、看门狗续期、Redlock 争议,以及基于数据库/ZooKeeper 的方案与选型。

在单机应用里,我们用进程内锁(synchronized、Lock)保护临界区。但一旦服务多实例部署,同一操作可能在不同机器上并发执行——比如秒杀下单、定时任务、重复扣款——进程内锁就失效了。这时需要一个所有实例都能看到的“全局锁”,这就是分布式锁。
一、分布式锁要满足什么
一把合格的分布式锁,至少要保证:
- 互斥性:同一时刻,只有一个客户端能持有锁;
- 防死锁:持锁者崩溃没释放,锁也能在一段时间后自动解除(要有过期时间);
- 解铃还须系铃人:只能释放自己加的锁,不能误删别人的锁;
- 可重入(可选):同一客户端可多次获取同一把锁;
- 高可用:锁服务本身不能成为单点。
二、基于 Redis 的锁:从 SETNX 说起
Redis 单线程、命令原子、性能高,是最常用的分布式锁实现。
1. 错误写法
SETNX lock:order:123 "1" // 加锁
DEL lock:order:123 // 解锁问题:
- 没设过期时间:持锁进程崩溃,锁永远不释放 → 死锁;
- 分开执行
SETNX和EXPIRE不是原子的,中间宕机同样死锁。
2. 正确加锁:一条命令搞定
SET lock:order:123 <唯一token> NX PX 30000NX:key 不存在才设置(保证互斥);PX 30000:30 秒自动过期(防死锁);<唯一token>:存一个只有加锁者知道的随机值(如 UUID),解锁时用来校验。
一条命令原子完成“判断 + 设置 + 过期”,避免中间状态。
3. 安全解锁:Lua 脚本保证原子
解锁时不能直接 DEL——可能锁早已过期、被别人拿走,你这一删就把别人的锁删了。必须“先判断 token 是自己的,再删除”,而“判断 + 删除”要用 Lua 脚本保证原子:
-- KEYS[1]=锁key, ARGV[1]=自己的token
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endRedis 执行 Lua 是原子的,杜绝了“判断完、还没删,锁刚好过期被他人获取”的竞态。
三、锁过期了但业务没执行完:看门狗
过期时间是个两难:设太短,业务还没做完锁就失效(失去互斥);设太长,持锁者崩溃后要等很久才释放。
看门狗(watchdog)续期机制(如 Redisson 的实现)解决这个问题:
- 加锁时不固定死过期时间,而是启动一个后台线程;
- 只要持锁客户端还活着、业务没结束,就定期自动延长锁的过期时间(如每 10 秒续到 30 秒);
- 业务完成或客户端宕机,续期停止,锁到期自动释放。
这样既防死锁,又避免业务未完成锁提前失效。
四、Redis 主从切换的问题与 Redlock
上面的锁在单机/主从下有个隐患:Redis 主节点加锁成功后、还没把数据同步到从节点就宕机,从节点提升为主,另一个客户端也能加锁成功 → 互斥被破坏。
Antirez(Redis 作者)提出 Redlock 算法应对:
- 部署 N 个(通常 5 个)相互独立、无主从复制的 Redis 主节点;
- 客户端依次向多数节点(≥ N/2+1)申请锁,并统计耗时;
- 在多数节点上加锁成功且总耗时小于锁有效期,才算加锁成功,否则向所有节点释放;
- 解锁时向所有节点发释放命令。
但 Redlock 一直有争议。分布式系统专家 Martin Kleppmann 指出:它依赖时钟假设,在 GC 停顿、时钟跳变等情况下仍可能出问题,且不提供 fencing(隔离令牌)能力,无法防止“持锁者在锁失效后仍写共享资源”。
工程共识:
- 大多数业务场景(防重复、效率型互斥),单 Redis + 唯一 token + 过期 + Lua 解锁(或直接用 Redisson)足够;
- 对正确性要求极高的场景(涉及资金),不要把宝押在锁上,应在数据层做兜底(见第六节)。
五、其他实现方案
| 方案 | 原理 | 特点 |
|---|---|---|
| Redis(Redisson) | SET NX PX + Lua + 看门狗 | 性能高、最常用,AP 倾向,极端情况可能失锁 |
| 数据库唯一约束 | 利用唯一索引/主键冲突 | 简单可靠、强一致,适合“防重复插入”(如幂等建单) |
| 数据库乐观锁 | UPDATE ... WHERE version=? 或状态条件 | 适合冲突少的并发更新,无需额外组件 |
| ZooKeeper / etcd | 临时顺序节点 + watcher | CP 倾向、一致性强,天然支持锁/选主,性能低于 Redis |
选型思路:
- 要高性能、能容忍极小概率失效 → Redis/Redisson;
- 要强一致、已有 ZK/etcd → 用它们的锁原语更稳;
- 本质是防重复数据 → 数据库唯一索引/乐观锁往往更直接可靠。
六、比锁更重要的:兜底设计
分布式锁无法做到 100% 严密,真正高可靠的系统不会只依赖锁:
- fencing token(栅栏令牌):锁服务在每次授锁时返回一个单调递增的令牌,客户端写共享资源时带上它,存储端拒绝旧令牌的写入,防止“锁失效后的迟到写入”;
- 数据库层幂等/约束:例如防超卖,最终要用
UPDATE stock SET n=n-1 WHERE id=? AND n>0这种带条件的原子更新兜底,锁只是减少冲突、降低压力; - 幂等设计:即使锁偶尔失效重复执行,业务结果依然正确(唯一键、状态机、去重表)。
一句话:锁用于“提高效率、减少并发冲突”,数据正确性要靠底层约束和幂等来保证。
七、常见坑
- 忘记设过期时间 → 死锁;
- 解锁不校验 token → 误删别人的锁;
- “判断 + 删除”没用 Lua → 原子性破洞;
- 锁过期时间拍脑袋,业务没做完锁就没了 → 用看门狗或合理评估耗时;
- 在锁里做 RPC/慢操作 → 持锁时间长、风险高,临界区尽量短;
- 把分布式锁当万能药,忽略数据库唯一约束和幂等兜底;
- Redis 集群故障切换 时误以为锁绝对可靠。
八、小结
- 分布式锁解决多实例间的互斥访问,核心要求是互斥、防死锁、只解自己的锁;
- Redis 标准姿势:
SET key token NX PX <ttl>加锁 + Lua 脚本校验 token 后删除解锁; - 用看门狗续期平衡“防死锁”和“业务未完成”;
- Redlock 试图解决主从切换问题,但有时钟/ fencing 争议,强一致场景别只靠锁;
- 高可靠要用 fencing 令牌 + 数据库原子约束 + 幂等在数据层兜底。
锁是并发控制的工具,不是数据正确的保证——把它用在合适的地方,并永远为“锁偶尔失效”准备好后手。
本文由 AI 辅助创作,所有内容仅供交流与参考。



