· 王思腾 · 系统架构 · 9 分钟阅读

分布式锁原理与实践:从 Redis 锁到 Redlock

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

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

在单机应用里,我们用进程内锁(synchronized、Lock)保护临界区。但一旦服务多实例部署,同一操作可能在不同机器上并发执行——比如秒杀下单、定时任务、重复扣款——进程内锁就失效了。这时需要一个所有实例都能看到的“全局锁”,这就是分布式锁。

一、分布式锁要满足什么

一把合格的分布式锁,至少要保证:

  1. 互斥性:同一时刻,只有一个客户端能持有锁;
  2. 防死锁:持锁者崩溃没释放,锁也能在一段时间后自动解除(要有过期时间);
  3. 解铃还须系铃人:只能释放自己加的锁,不能误删别人的锁;
  4. 可重入(可选):同一客户端可多次获取同一把锁;
  5. 高可用:锁服务本身不能成为单点。

二、基于 Redis 的锁:从 SETNX 说起

Redis 单线程、命令原子、性能高,是最常用的分布式锁实现。

1. 错误写法

SETNX lock:order:123  "1"     // 加锁
DEL   lock:order:123          // 解锁

问题:

  • 没设过期时间:持锁进程崩溃,锁永远不释放 → 死锁;
  • 分开执行 SETNX 和 EXPIRE 不是原子的,中间宕机同样死锁。

2. 正确加锁:一条命令搞定

SET lock:order:123 <唯一token> NX PX 30000
  • NX: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
end

Redis 执行 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临时顺序节点 + watcherCP 倾向、一致性强,天然支持锁/选主,性能低于 Redis

选型思路:

  • 要高性能、能容忍极小概率失效 → Redis/Redisson;
  • 要强一致、已有 ZK/etcd → 用它们的锁原语更稳;
  • 本质是防重复数据 → 数据库唯一索引/乐观锁往往更直接可靠。

六、比锁更重要的:兜底设计

分布式锁无法做到 100% 严密,真正高可靠的系统不会只依赖锁:

  • fencing token(栅栏令牌):锁服务在每次授锁时返回一个单调递增的令牌,客户端写共享资源时带上它,存储端拒绝旧令牌的写入,防止“锁失效后的迟到写入”;
  • 数据库层幂等/约束:例如防超卖,最终要用 UPDATE stock SET n=n-1 WHERE id=? AND n>0 这种带条件的原子更新兜底,锁只是减少冲突、降低压力;
  • 幂等设计:即使锁偶尔失效重复执行,业务结果依然正确(唯一键、状态机、去重表)。

一句话:锁用于“提高效率、减少并发冲突”,数据正确性要靠底层约束和幂等来保证。

七、常见坑

  • 忘记设过期时间 → 死锁;
  • 解锁不校验 token → 误删别人的锁;
  • “判断 + 删除”没用 Lua → 原子性破洞;
  • 锁过期时间拍脑袋,业务没做完锁就没了 → 用看门狗或合理评估耗时;
  • 在锁里做 RPC/慢操作 → 持锁时间长、风险高,临界区尽量短;
  • 把分布式锁当万能药,忽略数据库唯一约束和幂等兜底;
  • Redis 集群故障切换 时误以为锁绝对可靠。

八、小结

  1. 分布式锁解决多实例间的互斥访问,核心要求是互斥、防死锁、只解自己的锁;
  2. Redis 标准姿势:SET key token NX PX <ttl> 加锁 + Lua 脚本校验 token 后删除解锁;
  3. 用看门狗续期平衡“防死锁”和“业务未完成”;
  4. Redlock 试图解决主从切换问题,但有时钟/ fencing 争议,强一致场景别只靠锁;
  5. 高可靠要用 fencing 令牌 + 数据库原子约束 + 幂等在数据层兜底。

锁是并发控制的工具,不是数据正确的保证——把它用在合适的地方,并永远为“锁偶尔失效”准备好后手。

本文由 AI 辅助创作,所有内容仅供交流与参考。
分享:
返回博客
微服务架构设计:从单体拆分到服务治理

微服务架构设计:从单体拆分到服务治理

微服务不是银弹,拆得过细反而带来分布式复杂度。本文梳理微服务的边界划分(DDD)、服务间通信、API 网关、服务发现与配置中心,以及熔断限流、可观测性、分布式事务等治理难题,讨论什么时候该拆、什么时候不该拆。

缓存设计实战:穿透、击穿、雪崩与一致性

缓存设计实战:穿透、击穿、雪崩与一致性

缓存是高并发系统的第一道防线,但用不好会引入数据不一致、缓存穿透、击穿、雪崩等问题。本文系统梳理缓存读写策略(Cache Aside/Read/Write Through)、三大经典问题的成因与解法、热点 key、缓存与数据库一致性,以及 Redis 在生产中的实践要点。

个人与小团队的全栈技术栈:前端、后端到持续部署,怎么做到低成本、快交付

个人与小团队的全栈技术栈:前端、后端到持续部署,怎么做到低成本、快交付

一个人或两三个人做产品,最大的约束不是技术,是时间和钱。这套栈的目标很明确:不养服务器、不写运维、Git push 就上线、流量小的时候几乎零成本。本文从前端静态优先、后端 Serverless、托管数据库,到 CI/CD 自动部署,给出一套能今天动手、明天上线、跑起来月账单趋近于零的务实组合,顺便拿这个网站本身当例子。