我用 Redis 加了分布式锁防并发,结果偶尔还是有两个节点同时拿到锁、把临界区并发执行了,我对着这把"形同虚设"的锁排查了大半天的复盘

我用 Redis 的 SETNX 加锁、设过期时间防死锁、用完 DEL 释放,实现了分布式锁防并发,结果临界区偶尔还是被两个节点同时执行、锁形同虚设,还极其偶发难复现。把时序画出来才懂两个致命问题:① 业务执行时间超过了锁的过期时间,锁自动到期释放、另一个节点趁机拿到锁,于是两节点同时持锁;② 释放锁直接 DEL 没校验"是不是自己的锁",A 超时后 B 拿到锁,A 执行完却把 B 的锁误删了。还有 SETNX+EXPIRE 非原子可能死锁。这篇从分布式锁的核心难点讲起,到 SET NX EX+唯一value 原子加锁、Lua 脚本校验归属再删的原子释放、看门狗自动续期防超时的正解、主从丢锁/可重入/GC停顿等其他坑、几种方案对比(优先 Redisson)、分布式同步远不止锁、一个含 Lua 释放的锁封装,以及那句最戳心的——分布式系统建立在不可靠部件之上却要提供可靠服务,要敬畏不确定性、预设并对抗节点崩溃网络分区时钟差异每一种失败,别把单机直觉想当然搬过来。

我用 Redis 加了分布式锁防并发,结果偶尔还是有两个节点同时拿到锁、把临界区并发执行了,我对着这把"形同虚设"的锁排查了大半天的复盘

这是一个让我对"分布式锁"敬畏起来的故事。我有个操作,必须保证"同一时刻只有一个节点在执行"。多节点部署下,单机的锁不管用了,我就用 Redis 实现了一把分布式锁:用 SETNX(set if not exists)抢锁、设个过期时间防死锁、用完 DEL 释放。逻辑看着挺周全。可上线后,我发现那个本该"互斥"的临界区,偶尔还是被两个节点同时执行了——分布式锁,形同虚设;而且这个问题极其偶发、难以复现,只在某些特定时机才冒出来。我明明加了锁,为什么还会有两个节点同时进临界区?

我顺着"偶发并发"的线索,把加锁、执行、释放的时序在纸上画了一遍,才终于揭开真相,补上了我对分布式锁两个致命的认知漏洞:问题的核心,有两个第一个,是"锁的过期时间"和"业务执行时间"不匹配:我给锁设了个过期时间(比如 10 秒,为了"防止持锁节点崩溃后锁永远不释放"——这本身是对的);可有时,我的业务,执行得比 10 秒还久;于是,当业务还没执行完时,锁就因为到期而自动释放;这时,另一个节点,就趁虚而入、拿到了锁——于是,两个节点,同时在执行临界区!锁,因为提前过期,失去了互斥的意义第二个,更隐蔽:我释放锁,是直接 DEL,没有校验"这把锁,到底是不是我自己加的";接着第一个问题:A 节点的锁超时释放了,B 节点拿到了锁;此时 A 业务终于执行完,它不知道锁早已不是自己的了,还傻乎乎地 DEL 了一把锁——结果把 B 正持有的锁,给误删!于是 B 的锁也没了,第三个节点 C 又能拿到锁……锁的互斥性,彻底崩坏我这才痛彻地明白:分布式锁,远不是"SETNX 加锁 + DEL 释放"这么简单;它充满了时序、时钟、原子性上的陷阱:锁会提前过期(业务太慢)、释放时会误删别人的锁(没校验归属)、"判断+删除"不原子(中间被插队)。一把真正可靠的分布式锁,必须解决这些问题:给锁一个唯一的归属标识(释放时只删自己的)、用 Lua 脚本保证"判断归属 + 删除"的原子性、用"看门狗"自动续期来应对业务超时分布式环境下,任何"看似简单"的同步原语,背后都暗藏着远比单机复杂的深渊

故障现场:锁提前过期 + 误删别人的锁

我把这把"形同虚设"的锁的现场,用伪代码摊开给你看:

# ✗ 灾难: SETNX 加锁 + DEL 释放, 锁提前过期 + 误删别人的锁

# 加锁(✗ 看似没问题):
SETNX lock_key 1          # 抢锁: 不存在才设成功
EXPIRE lock_key 10        # 设 10 秒过期, 防持锁者崩溃导致死锁
# ⚠ 注意: SETNX + EXPIRE 两条命令非原子! 应该用 SET key val NX EX 10 一条搞定。
# ... 执行业务(可能超过 10 秒)...
DEL lock_key              # ✗ 释放: 直接删, 没校验是不是自己的锁!

# 问题1: 锁提前过期(业务比锁的过期时间还久)
#   T0: A 加锁(10秒过期), 开始执行业务。
#   T10: 锁自动过期释放 —— 但 A 的业务还没跑完!
#   T10: B 趁机 SETNX 成功, 拿到锁, 开始执行业务。
#   → A 和 B 同时在临界区! 锁失效。

# 问题2: 释放时误删别人的锁(接问题1)
#   T15: A 业务终于跑完, 执行 DEL lock_key
#        —— 但此刻这把锁是 B 的! A 把 B 的锁删了!
#   T15: C 又能 SETNX 成功 ... 互斥彻底崩坏。

# 问题3: SETNX + EXPIRE 非原子
#   - 若 SETNX 成功后、EXPIRE 前, 进程崩了 → 锁没过期时间 → 永久死锁!
#   - 必须用 SET lock_key uniqueVal NX EX 10 一条原子命令。

# 根因: 锁过期时间<业务时间 → 提前释放、多节点同时持锁;
#   释放直接 DEL 没校验归属 → 误删别人的锁; SETNX+EXPIRE 非原子 → 可能死锁。

看着这张时序图,我才算彻底想明白了这把锁失效的根源。问题一,锁提前过期:A 加锁(10 秒过期)开始执行,业务跑了超过 10 秒,锁到 T10 自动过期释放——可 A 还没跑完;B 趁机拿到锁,于是 A、B 同时在临界区问题二,释放时误删别人的锁(接问题一):T15 时 A 业务终于跑完、执行 DEL,可此刻这把锁是 B 的,A 把 B 的锁删了;C 又能拿到锁……互斥彻底崩坏问题三,SETNX + EXPIRE 非原子:SETNX 成功后、EXPIRE 前进程崩了,锁就没有过期时间、变成永久死锁;必须用 SET lock_key uniqueVal NX EX 10 一条原子命令归根结底:锁过期时间 < 业务时间 → 提前释放、多节点同时持锁;释放直接 DEL 没校验归属 → 误删别人的锁;SETNX+EXPIRE 非原子 → 可能死锁——这,就是根源。

第一件事:搞懂分布式锁的几个核心难点

定位到根源,我必须把"分布式锁难在哪"从根上彻底搞清楚:

分布式锁: 看似简单, 实则充满时序/原子/时钟陷阱

# 分布式锁要满足什么?(基本要求)
#   - 互斥: 同一时刻只有一个客户端持有(核心)。
#   - 防死锁: 持锁者崩了, 锁也要能释放(靠过期时间)。
#   - 不误删: 只能释放自己加的锁(别删别人的)。
#   - 容错: Redis 本身要可用(单点/主从切换也有坑)。

# 难点1: 过期时间的两难
#   - 设太短: 业务没跑完锁就过期 → 多节点同时持锁(本文)。
#   - 设太长: 持锁者崩了, 别人要等很久才能拿锁。
#   - 解法: 看门狗(watchdog)自动续期 —— 业务还在跑就定期延长锁。

# 难点2: 释放锁的"归属"和"原子性"
#   - 必须校验"这锁是我的"才能删(给锁设唯一 value, 如 UUID)。
#   - "判断是不是我的 + 删除" 必须原子(否则判断后、删除前锁过期被别人拿走,
#     你还是删了别人的)→ 用 Lua 脚本把两步合成一个原子操作。

# 难点3: 加锁的原子性
#   - SETNX + EXPIRE 两条非原子 → 用 SET key val NX EX seconds 一条命令。

# 难点4: Redis 主从/集群下的可靠性(更深的坑)
#   - 主节点加锁成功但还没同步到从, 主就挂了 → 从被选为主 → 锁丢了。
#   - 严格场景用 Redlock(多节点)或换 etcd/zk(强一致), 但更重更复杂。

# 关键认知: 分布式锁是"分布式共识"的简化, 单机锁的直觉在这里全部失效。
#   - 要同时考虑: 节点崩溃、网络分区、时钟、消息延迟、原子性。

# 核心: 分布式锁要互斥+防死锁+不误删+容错; 难在过期时间两难(看门狗续期)、
#   释放要校验归属且原子(Lua)、加锁原子(SET NX EX)、主从切换可靠性。

原理终于清晰了。分布式锁要满足:互斥(核心)、防死锁(持锁者崩了锁也能释放、靠过期时间)、不误删(只释放自己的锁)、容错(Redis 本身要可用)它的难点很多:难点 1,过期时间的两难——设太短业务没跑完就过期(本文)、设太长持锁者崩了别人等很久,解法是看门狗自动续期(业务还在跑就定期延长锁);难点 2,释放锁的归属和原子性——必须校验"这锁是我的"才能删(给锁设唯一 value 如 UUID),且"判断+删除"必须原子(否则判断后、删除前锁过期被别人拿走,你还是删了别人的),用 Lua 脚本合成原子操作;难点 3,加锁的原子性——SETNX+EXPIRE 非原子,用 SET key val NX EX 一条命令;难点 4,Redis 主从/集群下的可靠性——主加锁成功但没同步到从、主就挂了,从被选为主,锁就丢了,严格场景用 Redlock 或换 etcd/zk由此,我刻下一个关键认知:分布式锁是"分布式共识"的简化,单机锁的直觉在这里全部失效;要同时考虑节点崩溃、网络分区、时钟、消息延迟、原子性。归根结底:分布式锁要互斥+防死锁+不误删+容错;难在过期时间两难(看门狗续期)、释放要校验归属且原子(Lua)、加锁原子(SET NX EX)、主从切换可靠性。

第二件事:正解——唯一标识 + Lua 原子释放 + 看门狗续期

搞懂了原理,正解就清晰了:加锁用原子命令 + 唯一 value;释放用 Lua 脚本"校验归属 + 删除"原子完成;业务可能超时就用看门狗续期

# ✓ 正解一: 加锁 —— 原子命令 + 唯一 value(标识"这锁是我的")
SET lock_key  NX EX 10
#   - : 每个客户端生成一个唯一值(如 UUID), 用来标识归属。
#   - NX: 不存在才设(互斥); EX 10: 10秒过期(防死锁); 一条命令原子完成。

# ✓ 正解二: 释放锁 —— 用 Lua 脚本"判断是不是自己的 + 删除"原子完成
# release.lua:
#   if redis.call("GET", KEYS[1]) == ARGV[1] then
#       return redis.call("DEL", KEYS[1])    # 只有 value 是自己的 unique_id 才删
#   else
#       return 0                             # 不是自己的, 不删(防误删别人的锁)
#   end
# 调用: EVAL release.lua 1 lock_key 
#   → "判断归属" 和 "删除" 在一个 Lua 里原子执行, 中间不会被插队。

# ✓ 正解三: 看门狗(watchdog)—— 业务还在跑就自动续期, 防锁提前过期
#   - 加锁后, 起一个后台定时任务(如每 1/3 过期时间), 检查业务是否还在跑。
#   - 还在跑 → 用 Lua 给锁续期(PEXPIRE, 同样要校验是自己的锁再续)。
#   - 业务结束 → 停掉看门狗 + 释放锁。
#   → Redisson 等成熟库内置了看门狗, 强烈建议直接用成熟库, 别自己造。

# ✓ 正解四: 合理设过期时间 + 业务尽量短
#   - 过期时间 > 业务正常执行时间(留余量), 配合看门狗兜底超时。
#   - 临界区里别做慢操作/RPC/IO, 尽量短, 减少持锁时间。

# ✓ 正解五(严格场景): 用更可靠的方案
#   - Redlock(多 Redis 节点, 但有争议); 或 etcd/zookeeper(强一致, 更重)。
#   - 普通场景: 单 Redis + Redisson 够用; 钱/库存等强一致: 上 etcd/zk 或 DB 唯一约束。

# 核心: 加锁用 SET NX EX + 唯一value; 释放用 Lua 校验归属再删(原子, 防误删);
#   看门狗续期防超时; 优先用 Redisson 等成熟库, 别自己造轮子。

修复的方向,是把那把"裸锁"升级成"可靠锁"。正解一,加锁用原子命令 + 唯一 value:SET lock_key <unique_id> NX EX 10——一条命令原子完成"不存在才设(互斥)+ 设过期(防死锁)",而 unique_id(如 UUID)用来标识"这锁是我加的"正解二,释放用 Lua 脚本:在 Lua 里"先 GET 看 value 是不是自己的 unique_id、是才 DEL"——把"判断归属 + 删除"放进一个 Lua 原子执行,中间不会被插队,既防误删别人的锁,又保证了原子性正解三,看门狗续期:加锁后起一个后台定时任务,业务还在跑就定期给锁续期(同样要校验是自己的锁),业务结束就停看门狗+释放锁——这根治了"锁提前过期"正解四,合理过期时间 + 业务尽量短;正解五(严格场景),用 Redlock 或 etcd/zk这里有个极重要的工程建议:分布式锁的坑太多太深,强烈建议直接用 Redisson 等成熟库(它内置了唯一标识、Lua 释放、看门狗续期),别自己手写归根结底:加锁用 SET NX EX + 唯一 value;释放用 Lua 校验归属再删(原子、防误删);看门狗续期防超时;优先用 Redisson 等成熟库,别自己造轮子。

第三件事:分布式锁的其他坑与边界

这次踩坑后,我把分布式锁其他容易忽略的坑和边界,也一并梳理清楚了:

分布式锁的其他坑与边界

# 1. Redis 主从切换丢锁(最深的坑)
#   - 主加锁成功, 还没同步到从, 主挂了 → 从升主 → 锁没了 → 别人能拿。
#   - 普通场景容忍; 强一致用 Redlock(多节点)或 etcd/zk(基于一致性协议)。

# 2. 锁的粒度
#   - 锁太粗(如锁整个表) → 并发度低, 性能差。
#   - 锁太细 → 管理复杂。按"资源 id"加锁(如 lock:order:123)是常见折中。

# 3. 可重入
#   - 同一线程/请求要再次拿同一把锁(嵌套调用)→ 简单锁会死锁。
#   - 用可重入锁(记录持有者+重入次数, Redisson 支持)。

# 4. 客户端时钟 / GC 停顿
#   - 客户端长 GC 停顿期间, 锁可能已过期被别人拿走, 它醒来还以为自己持锁。
#   - 这是分布式锁的根本难题之一(无法100%避免), 关键操作要配合幂等/CAS兜底。

# 5. 别把分布式锁当"万能"
#   - 能用"数据库唯一约束""乐观锁(版本号)""队列串行化"解决的, 未必要上分布式锁。
#   - 分布式锁有性能开销和复杂度, 是手段不是目的。

# 6. 锁和幂等要配合
#   - 锁降低并发概率, 但不能100%保证(主从丢锁/GC等); 关键写操作仍要幂等兜底。

# 关键认知: 分布式锁能降低并发风险, 但在分布式环境里没有"绝对可靠"的锁。
#   - 关键操作: 锁 + 幂等/CAS 双保险, 别只靠锁。

# 核心: 分布式锁还有主从丢锁、粒度、可重入、GC停顿等坑; 它是手段不是万能;
#   没有绝对可靠的分布式锁, 关键操作要锁+幂等双保险。

原来分布式锁的深渊,比我想的还深。主从切换丢锁(最深的坑:主加锁成功未同步到从、主挂了从升主、锁就没了,强一致要用 Redlock/etcd/zk);锁的粒度(太粗并发低、太细管理难,按"资源 id"加锁是常见折中);可重入(同一请求嵌套拿同一把锁,简单锁会死锁,用可重入锁);客户端 GC 停顿(GC 期间锁可能已被别人拿走,它醒来还以为自己持锁——分布式锁的根本难题,无法 100% 避免);别把分布式锁当万能(能用唯一约束/乐观锁/队列串行化解决的未必要上锁);锁和幂等要配合由此,我刻下一个关键认知:分布式锁能降低并发风险,但在分布式环境里没有"绝对可靠"的锁;关键操作要"锁 + 幂等/CAS"双保险,别只靠锁。归根结底:分布式锁还有主从丢锁、粒度、可重入、GC 停顿等坑;它是手段不是万能;没有绝对可靠的分布式锁,关键操作要锁+幂等双保险。

下面这张图,是这把分布式锁失效的成因与解法:

第四件事:分布式锁几种实现方案的对比

这次踩坑后,我把分布式锁的几种实现方案,横向比了一遍,按场景对号入座。

方案 原理 可靠性 适用
裸 SETNX+DEL 手写, 无归属无续期 ✗ 各种坑(本文) 别用
SET NX EX + Lua + 唯一value 原子加锁+原子释放 较好(无看门狗仍怕超时) 简单场景手写下限
Redisson 内置唯一标识+Lua+看门狗+可重入 ★★★ 推荐 大多数 Redis 锁场景
Redlock 多 Redis 节点过半加锁 更高(但有学界争议) 对单点丢锁敏感
etcd / ZooKeeper 基于一致性协议(Raft/ZAB) 最高(强一致) 金融/强一致严格场景
数据库唯一约束/乐观锁 用 DB 的约束/版本号 看实现 已有DB, 不想引中间件

把它们排在一起,选择就清楚了。SETNX+DEL 是本文的坑,别用;手写下限是 SET NX EX + 唯一 value + Lua 释放(但没看门狗仍怕业务超时);大多数 Redis 锁场景,首选 Redisson(它内置了唯一标识、Lua 释放、看门狗续期、可重入,把所有坑都替你填了);对单点丢锁敏感Redlock(多节点过半,但有学界争议);金融/强一致的严格场景etcd/ZooKeeper(基于 Raft/ZAB 一致性协议、最可靠,但更重);如果已有数据库、不想引中间件,也可以用 数据库唯一约束或乐观锁它给我的最大启发是:分布式锁,是一个"自己手写极易出错、且有成熟轮子"的典型领域;除非你深入理解了所有的坑、且有充分理由,否则就直接用 Redisson 这样的成熟库——把"填那些深坑"的活,交给那些已经被无数生产环境验证过的库,远比自己重新踩一遍坑靠谱;知道"什么时候不要自己造轮子",也是一种重要的工程智慧

第五件事:分布式同步,远不止"锁"一种思路

这次踩坑也让我反思:解决"并发冲突"未必非得用"分布式锁"。我把几种"分布式同步/避免冲突"的思路梳理了一遍。

思路 做法 适用
分布式锁 抢锁, 串行进临界区 需要严格互斥的临界区(本文)
数据库唯一约束 靠 DB 约束防重复 创建类去重(如防重复下单)
乐观锁(版本号/CAS) 更新时校验版本, 冲突则重试 冲突不频繁的更新
队列串行化 同一资源的操作路由到一个消费者 把并发变串行, 天然无冲突
分段/分片 按 key 哈希分片, 各片独立 降低锁竞争, 提高并发
幂等设计 操作重复执行也安全 配合一切, 兜底

这张表,让我看到了"分布式锁只是众多同步思路之一"。它们各有所长:需要严格互斥的临界区,用分布式锁;创建类去重(防重复下单),用数据库唯一约束;冲突不频繁的更新,用乐观锁(版本号/CAS、冲突才重试,性能比悲观锁好);想把并发彻底变串行,用队列串行化(同一资源的操作路由到同一个消费者,天然无冲突);想提高并发,用分段/分片(按 key 哈希分片、降低锁竞争);而幂等,则是配合一切的兜底它给我的最大启发是:面对并发冲突,"加把锁"往往是最直接、却未必最优的第一反应;很多时候,有比"锁"更轻、更高效、甚至更优雅的思路:能用唯一约束防重的,何必加锁?能用乐观锁的,何必用重的悲观锁?能把并发变串行(队列)从根上消除冲突的,何必费力地去协调互斥?。所以,遇到并发问题,先别急着"上分布式锁",而要退一步想:"这个冲突的本质是什么?有没有比'锁'更适合它的解法?"——在工具箱里多备几把"刀",并懂得在合适的场景拿出合适的那把,才是高手的做法

第六件事:要用分布式锁时,我现在会怎么决策

现在,每当我准备用分布式锁,脑子里都会过一遍这张决策图——核心两问:真的需要锁吗?用了怎么保证可靠?

这张图的灵魂,是两个必问的前置问题第一问:真的需要"严格互斥的临界区"吗?——创建去重用唯一约束、更新冲突不频繁用乐观锁、能串行化用队列,这些都比锁更轻;确实需要互斥,才上分布式锁。第二问:自己写还是用库?——优先用 Redisson 等成熟库(它把唯一标识、Lua 释放、看门狗都填好了);非要手写,至少做到 SET NX EX+唯一 value+Lua 释放+看门狗再看可靠性要求:对单点丢锁敏感/强一致,用 Redlock 或 etcd/zk;否则单 Redis 够用最后两步,是我以前最缺的:关键操作再配幂等/CAS 双保险(锁不是 100% 可靠)、压测并模拟超时/主从切换,验证确实不会并发

我立下的几条规矩

这场"分布式锁形同虚设"的事故,换来了我做分布式系统时,刻进骨子里的几条铁律:

  1. 加锁用 SET NX EX 一条原子命令。SETNX+EXPIRE 非原子,中间崩了会死锁;且锁的 value 要设唯一标识。
  2. 释放锁要校验归属,且用 Lua 原子完成。只删自己的锁(value 是自己的才删),"判断+删除"放进 Lua 防中间被插队。
  3. 业务可能超时就用看门狗续期。别让锁因为业务太慢而提前过期,导致多节点同时持锁。
  4. 优先用 Redisson 等成熟库。分布式锁坑太深,成熟库已填好所有坑,别自己造轮子。
  5. 没有绝对可靠的分布式锁。主从丢锁、GC 停顿都可能让锁失效;关键操作要锁+幂等/CAS 双保险。
  6. 先问"真的需要锁吗"。唯一约束/乐观锁/队列串行化常比分布式锁更轻更优;锁是手段不是目的。
  7. 上线前压测+模拟故障。模拟业务超时、主从切换,验证临界区确实不会被并发执行。

附:一个相对靠谱的分布式锁封装(含 Lua 释放)

如果确实要手写(或想理解 Redisson 在做什么),下面是一个相对靠谱的分布式锁封装,把唯一标识、Lua 原子释放都做了:

import uuid, time, threading
import redis

r = redis.Redis()

# 释放锁的 Lua: 只有 value 是自己的 token 才删(判断+删除 原子)
RELEASE_LUA = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end
"""
# 续期的 Lua: 只有是自己的锁才续(看门狗用)
RENEW_LUA = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("PEXPIRE", KEYS[1], ARGV[2])
else
    return 0
end
"""

class RedisLock:
    def __init__(self, key, ttl_ms=10000):
        self.key = key
        self.ttl = ttl_ms
        self.token = str(uuid.uuid4())   # ✓ 唯一标识, 标记"这锁是我的"
        self._stop = threading.Event()

    def acquire(self, timeout=5):
        deadline = time.time() + timeout
        while time.time() < deadline:
            # ✓ 原子加锁: 不存在才设 + 带过期 + value 是唯一 token
            if r.set(self.key, self.token, nx=True, px=self.ttl):
                self._start_watchdog()    # ✓ 加锁成功, 启动看门狗
                return True
            time.sleep(0.05)              # 抢不到, 短暂等待重试
        return False

    def release(self):
        self._stop.set()                 # 停看门狗
        # ✓ Lua 原子释放: 只删自己的锁
        r.eval(RELEASE_LUA, 1, self.key, self.token)

    def _start_watchdog(self):
        def renew():
            while not self._stop.wait(self.ttl / 1000 / 3):   # 每 1/3 ttl 续一次
                r.eval(RENEW_LUA, 1, self.key, self.token, self.ttl)  # ✓ 续期(校验归属)
        threading.Thread(target=renew, daemon=True).start()

# 用法:
lock = RedisLock("lock:order:123")
if lock.acquire():
    try:
        do_critical_work()      # 临界区
    finally:
        lock.release()          # ✓ 一定在 finally 里释放
else:
    handle_lock_failed()

# 核心: 唯一token标识归属 + SET NX PX 原子加锁 + Lua原子释放(只删自己的)
#   + 看门狗定期续期(校验归属), 这就是 Redisson 内部做的事的简化版。

这个封装,把前面讲的所有要点,落成了一段可参照的代码它的几个关键设计:第一,每个锁实例生成一个唯一 token(UUID),作为"这锁是我的"的凭证;第二,r.set(key, token, nx=True, px=ttl) 一条原子命令加锁(不存在才设+带过期+带唯一 value);第三,释放用 RELEASE_LUA——在 Lua 里"判断 value 是不是自己的 token、是才 DEL",原子完成、绝不误删别人的锁;第四,加锁成功就启动看门狗——每隔 1/3 的 ttl,用 RENEW_LUA 给锁续期(同样校验归属),只要业务还在跑、锁就不会过期;第五,释放一定放在 finally,保证异常时也能释放。这一整套,正是 Redisson 等成熟库内部做的事的简化版。这,也正是我想用这段代码,留给每一个做分布式系统的人的最后一课:看懂了这段封装,你就真正理解了"一把可靠的分布式锁,到底要解决哪些问题";而理解之后,你反而会更坚定地选择直接用成熟库——因为你亲眼看到了,要把这件"看似简单"的事做对,需要多少细致的考量理解原理是为了会判断、会取舍,而站在巨人(成熟库)的肩膀上,则是为了把精力,留给真正属于你的业务难题

写在最后

回头看,这场由"分布式锁"引发的、看似有锁实则并发的事故,真正教给我的,是一个比"用对锁"本身更深的道理:当我们从单机走向分布式,很多在单机世界里"天经地义、坚如磐石"的基础设施(比如一把锁),都会因为"多了网络、多了节点崩溃、多了时钟差异",而变得脆弱、充满了你想象不到的失败模式单机的锁,之所以可靠,是因为它运行在一个"可信赖的、共享内存的、不会无故消失的"环境里;可一旦把"锁"这个概念,搬到分布式环境——持锁的节点会毫无征兆地崩溃、网络会延迟和分区、各节点的时钟会有偏差、消息会乱序和丢失——那个简单的"锁",就必须去对抗这一整个"不可靠的世界",于是它的实现,也变得无比复杂。这让我深刻地领悟到:分布式系统的本质难点,在于它是建立在一堆"不可靠的部件"(会崩的节点、会断的网络)之上,却要努力提供"可靠的服务";而我们写的每一个分布式组件,都必须预设并对抗这些不可靠所以,做分布式,要永远怀着一份"悲观"和"谦卑":不要把任何"单机的直觉"想当然地搬过来,而要逐一追问:"如果这个节点此刻崩了这条网络此刻断了这两台机器时钟不一致,我的逻辑还成立吗?"敬畏分布式的不确定性,预设并对抗每一种失败——这,是我用一次"分布式锁失效"的事故,换来的、关于架构、也关于分布式系统本质的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次用分布式锁时,不再以为"加了锁就高枕无忧",而是去认真对待它背后的那些深坑,那我对着那把形同虚设的锁熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我把一篇超长文档整个塞给大模型让它总结,结果它的回答只覆盖了前半部分、后半段像没看见一样,我对着这个被静默截断的输入排查了大半天的复盘

2026-6-2 4:24:43

技术教程

我在 C# 里用 struct 定义数据放进 List,想改它的字段却怎么改都不生效、传进方法改也白改,我对着值类型的拷贝语义排查了大半天的复盘

2026-6-2 4:38:55

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索