分布式每日一学 — Day 5:分布式锁

发布时间:2026/8/11 12:56:58
分布式每日一学 — Day 5:分布式锁 分布式每日一学 — Day 5分布式锁前四天我们聊了 CAP 定理、Raft/Paxos 共识、分布式事务。这些知识点都指向同一个问题在多台机器共享同一份逻辑资源的场景下怎么保证「同一时刻只有一个执行者在做事」这正是分布式锁要解决的问题。 Day 4 思考题解答回顾题目用户 A 给用户 B 转账 100 元要走① 风控服务 → ② 账户服务A -100B 100→ ③ 账本服务写流水→ ④ 积分服务A 10 积分→ ⑤ 通知服务短信/站内信。问题 1你会用 Saga 还是 TCC为什么选 Saga不用 TCC。理由如下维度分析链路长度5 个子事务链路较长。TCC 每个服务都要写 Try/Confirm/Cancel 三套接口业务侵入性太高资金安全账户操作②可以用幂等 对账兜底不需要 TCC 的冻结机制积分和通知④积分是福利、⑤通知是尽最大努力这两步根本不需要强一致TCC 大材小用性能Saga 不锁资源异步补偿吞吐远高于 TCC选型关键TCC 适合短链路 资金核心 强一致要求高如支付扣款转账链路有 5 步且后两步是福利型操作Saga 更合适。问题 2如果用 Saga② “A -100、B 100” 这个子事务补偿动作具体怎么设计补偿动作 C2 A 100、B -100把钱退回去。注意以下边界条件边界条件说明幂等补偿可能被重试多次。每次执行前检查A 的余额是否已经被加回用流水号 状态机保证幂等B 余额不足如果 B 已经把钱花掉了B -100 会导致负数。方案B 的账户允许透支或者记录一条欠款记录后续追回补偿顺序必须先补偿 B-100再补偿 A100。如果先给 A 加钱A 可能立刻又转走部分成功如果 A -100 成功但 B 100 还没执行就挂了补偿时只需要把 A 的钱退回不需要扣 B账本流水补偿操作必须写一条反向流水记录和正向操作一一对应便于对账正向 T2: A: 200 → 100 B: 50 → 150 (A -100, B 100) 补偿 C2: A: 100 → 200 B: 150 → 50 (A 100, B -100) ↑ 如果 B 余额不够允许透支记欠款问题 3如果 MQ 通知用户短信发送失败重试到第 3 次还是失败怎么办这条业务流算成功还是失败业务流算成功。分析如下子事务失败影响处理策略① 风控可重试失败重试即可风控是前置检查② 账户核心必须成功失败走 Saga 补偿③ 账本核心必须成功和②一起做④ 积分福利型失败可补偿不影响主流程⑤ 通知尽最大努力失败不影响转账结果核心原则⑤通知服务是尽最大努力通知Best Effort模式。转账本身①②③已经成功通知失败不代表转账失败。处理方案重试MQ 消费失败后重试 N 次如 3 次每次间隔递增降级重试失败后转为站内信、App Push 等其他通知渠道兜底全部通知渠道都失败记录到通知失败表人工或定时任务后续补发幂等通知服务必须幂等——即使重试多次用户只收到一条短信面试金句业务流程的成功边界由业务定义不是由技术定义。转账成功 钱到了不等于用户收到了短信。一、为什么需要分布式锁单进程里的锁比如 Java 的synchronized、Go 的Mutex只能管住一个进程里的多个线程。一旦业务拆成多个服务、多个实例单进程锁就失效了。 典型场景场景问题电商秒杀100 台应用实例同时扣减同一 SKU 库存超卖怎么办订单幂等用户重复点击「支付」如何保证只扣一次钱定时任务多个实例都启了同样的 Job谁跑资源初始化全局配置缓存只初始化一次多个节点并发加载怎么避免分布式锁的本质提供一个所有进程都能访问的「互斥令牌」同一时刻只有拿到令牌的进程能进入临界区。二、分布式锁的基本要求一个靠谱的分布式锁至少得满足以下几点要求含义互斥性同一时刻只有一个客户端能持有锁可重入性可选但重要同一个线程/客户端可以重复加锁防死锁客户端宕机后锁能自动释放别一直卡着高可用锁服务自己不能是单点容错性部分节点故障时锁服务仍能正常工作性能加锁/解锁要快别成瓶颈其中互斥性和防死锁是底线缺一不可。三、方案 1基于 Redis 的锁Redis 因为性能高、部署简单是分布式锁最常见的选择。 阶段 1SETNX入门版SETNX lock:order:123owner-1# 如果不存在就设置存在则不设置EXPIRE lock:order:12310# 10 秒后自动过期这看起来没毛病但有一个经典漏洞客户端 A: SETNX 成功 客户端 A: 还没执行 EXPIRE进程挂了 结果: 锁永久存在死锁 阶段 2SET NX EX正确版Redis 2.6.12 后支持原子命令SET lock:order:123owner-1NX EX10NXOnly set if Not eXistsEX 1010 秒后自动过期这把「加锁 设过期时间」变成了原子操作解决了阶段 1 的死锁问题。 阶段 3解锁要验证身份不能直接用DEL lock:order:123因为可能删了别人的锁A 拿到锁业务执行慢锁过期自动释放 B 此时拿到锁 A 业务完成执行 DEL结果把 B 的锁删了正确做法用 Lua 脚本原子地「检查 value 是否是自己的是自己的才删」。ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end Redisson生产环境的首选Redisson 是 Redis 的 Java 客户端封装了非常成熟的分布式锁实现RLocklockredisson.getLock(order:123);try{lock.lock();// 执行业务}finally{lock.unlock();}它内部做了什么特性实现看门狗自动续期默认 30 秒过期看门狗每 10 秒续期可重入用 Hash 结构记录重入次数公平锁可选 FairLock按请求顺序获取联锁/红锁支持同时锁住多个资源⚠️Redisson 默认会启动看门狗线程给锁续期所以业务代码里一定要finally { unlock() }否则锁会等到看门狗停掉才真正释放。四、方案 2Redlock 算法Redis 多主模式如果你担心单点 Redis 挂了导致锁失效Redis 作者 antirez 提出了Redlock算法。核心思想不在一个 Redis 实例上加锁而是在N 个互相独立的 Redis 主节点上分别加锁假设 N5客户端向 5 个 Redis 实例申请锁 只要能在大多数实例3上成功加锁并且总耗时小于锁过期时间就认为加锁成功流程1. 获取当前时间戳 T1 2. 依次向 5 个 Redis 实例发送 SET key value NX PX 过期时间 3. 统计成功加锁的实例数并获取当前时间戳 T2 4. 如果 成功数 N/2 且 锁剩余有效时间 0则加锁成功 5. 否则向所有实例发送解锁脚本 为什么很多人对 Redlock 有争议Martin Kleppmann《Designing Data-Intensive Applications》作者写了一篇著名文章How to do distributed locking指出 Redlock 的问题问题说明依赖时钟如果某个 Redis 节点时钟漂移锁可能提前过期不是共识协议Redlock 没有 Raft/Paxos 那样的严格多数派保证故障恢复问题加锁时某个节点挂了重启后如果没带持久化可能丢失锁信息工程建议如果你需要 CP 级强一致的锁不要用 Redlock如果只是 AP 场景、允许偶尔竞态Redisson 单实例/哨兵/Cluster 就够了。五、方案 3基于 ZooKeeper 的锁ZooKeeper 天生适合做分布式锁因为它提供了临时顺序节点EPHEMERAL_SEQUENTIAL客户端断开自动删除Watcher 机制节点变化可通知等待者强一致性基于 ZAB 协议写操作是线性一致的实现原理所有客户端都在 /locks/order 下创建临时顺序节点 /locks/order/lock-00000001 /locks/order/lock-00000002 /locks/order/lock-00000003 谁拿到最小序号谁就获得锁。 没拿到锁的客户端监听自己前面那个节点等它删除。优点优点原因无死锁客户端断开临时节点自动删除顺序公平按创建顺序获取锁不会饥饿CP 保证ZK 是 CP 系统一致性高缺点缺点说明性能不如 Redis每次写都要 ZAB 同步单写 Leader写请求都走 Leader存在瓶颈羊群效应大量客户端同时监听同一个节点唤醒时瞬间涌入CuratorApache 的 ZK 客户端已经封装好了InterProcessMutex生产环境直接用即可。六、方案 4基于 etcd 的锁etcd 是 Kubernetes 的元数据存储基于 Raft 协议天然适合高可靠的分布式锁。核心 API# 原子加锁如果 key 不存在就创建并设置租约ectctl lock my-locketcd 内部实现用了concurrency.Mutex// Go 示例s,err:concurrency.NewSession(client,concurrency.WithTTL(10))mu:concurrency.NewMutex(s,/my-lock/)mu.Lock(context.Background())defermu.Unlock(context.Background())特点特性说明基于 RaftCP 系统一致性强Lease 机制租约过期自动释放防死锁revision顺序类似 ZK 的顺序节点按 revision 排队性能比 ZK 好gRPC Raft吞吐更高对比 ZooKeeper维度etcdZooKeeper协议RaftZAB类 PaxosAPIgRPCRESTJute 协议部署简单Kubernetes 原生较重Java 生态更熟适用云原生、KubernetesHadoop 生态、传统微服务七、方案 5Chubby 锁Google 的祖师爷方案Google 的 Chubby 是分布式锁的「教科书级」实现很多后来的锁服务都受其影响。设计特点以文件系统为抽象锁就是文件客户端持有文件的句柄长期会话 KeepAlive客户端和 Chubby Master 维持长连接断开后锁自动释放建议性锁Chubby 不强制阻止未持锁者访问资源靠业务自觉检查可缓存 sequencer 机制客户端可以缓存锁状态用 sequencer 判断锁是否还有效sequencer 机制客户端拿到锁时Chubby 返回一个 sequencer类似单调递增的数字 客户端访问受保护资源时把 sequencer 带过去 资源服务端检查 sequencer 是否有效防止客户端在锁过期后还操作资源Chubby 后来被开源社区映射为 ZooKeeper / etcd 的思路但工程细节更加丰富。八、Redis 锁 vs ZooKeeper/etcd 锁CAP 视角维度Redis 锁RedissonZooKeeper/etcd 锁CAP 倾向偏 AP偏 CP一致性最终一致强一致/线性一致可用性高分区时牺牲部分可用性能极高中等死锁防护依赖过期时间/看门狗依赖会话/租约更可靠实现复杂度低中典型场景限流、缓存预热、幂等配置中心选主、任务调度 选型口诀要速度选 Redis要可靠选 etcd/ZK。九、分布式锁的十大坑血泪总结坑后果解法1. 只 SETNX 不设过期时间客户端宕机死锁SET ... NX EX 看门狗续期2. 解锁时不验证 owner误删别人的锁Lua 脚本判断 value 再删除3. 锁过期时间设太短业务还没执行完锁释放了Redisson 看门狗自动续期4. 锁粒度太大并发性能差按用户 ID / 订单 ID 细分5. 锁内代码抛异常没解锁死锁try { lock() } finally { unlock() }6. 主从 Redis 异步复制主节点挂了锁没同步到从节点用 Redlock 或 etcd7. 可重入没处理自己锁自己用 Hash 结构记录重入次数8. 锁续期逻辑有 bug业务结束了还在续绑定线程/上下文生命周期9. 没考虑时钟漂移锁提前或延后过期用共识协议型锁服务10. 锁服务自身单点锁服务挂了全崩Redis Cluster / ZK 集群 / etcd 集群十、和前四天知识点的连接之前学的和分布式锁的关系CAP 定理Day 1Redis 锁偏 APZK/etcd 锁偏 CPRaftDay 2etcd 用 Raft 保证锁状态的一致性PaxosDay 3ZooKeeper 的 ZAB 是类 Paxos 协议分布式事务Day 4锁常用于保证事务/补偿过程中的互斥比如 Saga 协调器选主 今日思考题你负责一个电商秒杀系统库存只有 100 件瞬时流量 100 万 QPS。你决定用 Redis Cluster Redisson 做分布式锁。问题 1如果某个分片上的 Master 挂了Redis Cluster 正在故障转移这段时间还能扣库存吗会不会超卖问题 2如果 Redisson 看门狗因为 GC 停顿没及时续期锁过期了业务还在执行会发生什么问题 3同样的场景如果换成 etcd 做分布式锁CAP 上会有什么不同提示想想 Redis Cluster 的cluster-require-full-coverage和 Redisson 的看门狗机制。一句话带走分布式锁没有银弹Redis 锁快但不那么「硬」适合限流/幂等/缓存类场景etcd/ZK 锁慢但更「稳」适合选主/配置/强一致协调。理解 CAP 取舍才能选对锁。明天预告Day 6 — 一致性模型线性一致、顺序一致、因果一致、最终一致帮你搞懂「一致性」到底有多少种「强度」️ 标签:#分布式锁#Redis#Redisson#ZooKeeper#etcd#CAP 进度: Day 5 / ∞ 已覆盖: CAP 定理 → Raft → Paxos → 分布式事务 → 分布式锁