我用 Redis 加了分布式锁防并发,结果两个进程还是同时执行了、还互相把对方的锁删了,我对着分布式锁的几个致命细节排查了大半天的复盘

做了个多实例部署的定时任务,用 Redis 加分布式锁确保同一时刻只有一个实例执行,以为加了锁就万事大吉。结果生产诡异:有时两个实例同时跑(锁没锁住)导致数据重复处理,有时一个实例把另一个实例的锁删了。盯着 SETNX 抢锁、DEL 释放的标准代码反复看,排查大半天才发现藏着三个致命细节:SETNX 没设过期(持锁者崩溃就死锁)、锁提前过期(业务跑40秒超过锁30秒过期,A还在执行B就抢到锁同时执行)、删锁不验身(A执行完去删,此时锁已是B的,把B的锁删了)。根因是分布式锁看起来简单(抢锁-执行-释放)但要做对有一堆边界要处理(过期/超时/误删/原子性)。这篇从分布式锁必备要素(互斥/防死锁/唯一身份/防提前过期)、原子加锁SET NX EX+唯一token+Lua验身删除+看门狗续期+用Redisson的正解、深层问题(锁提前过期理论难杜绝要配业务幂等双保险/单点可靠性/锁粒度/优先用更简单替代)、错误vs正确写法速查、何时真需要分布式锁、决策图与铁律,到附上一个含续期的相对完整 RedisLock 实现。核心领悟:接口简单不等于实现简单或正确地用简单,对简单表象之下的复杂(尤其分布式环境)要保持敬畏;分布式原语自己手搓看起来能用很容易、各种边界下都正确极难,该站在巨人肩膀上用成熟库;能用简单方案(唯一索引/幂等/调度框架)就别上重武器。

我用 Redis 加了分布式锁防并发,结果两个进程还是同时执行了、还互相把对方的锁删了,我对着分布式锁的几个致命细节排查了大半天的复盘

那是我做的一个定时任务,多个实例部署,我用 Redis 加了个分布式锁,确保"同一时刻只有一个实例在执行"。我以为加了锁就万事大吉。结果生产上出现了诡异的现象:有时两个实例同时跑起了这个任务(锁没锁住!),导致数据被重复处理;还有时一个实例把另一个实例的锁给删了,锁的逻辑彻底乱套。我盯着加锁代码反复看:SETNX 抢锁、执行完 DEL 释放,逻辑明明很标准啊?排查了大半天,我才发现,这段"看起来很标准"的分布式锁代码里,藏着好几个能让锁形同虚设的致命细节。这篇就把这场"分布式锁没锁住"的事故,从头复盘一遍。

故障现场:加了锁,却没锁住

先看现场。问题就藏在我那段"看起来很标准"的加锁代码里:

# 我的分布式锁: 看起来很标准, 实则漏洞百出
import redis
r = redis.Redis()

def do_task():
    # 1. 抢锁: SETNX(set if not exists), 抢到才执行
    locked = r.setnx("task_lock", "1")   # ✗ 问题1: 没设过期时间!
    if not locked:
        return   # 没抢到锁, 退出
    try:
        # 2. 执行业务(假设耗时不定, 有时很久)
        run_business()
    finally:
        # 3. 释放锁
        r.delete("task_lock")            # ✗ 问题2: 可能删了别人的锁!

# 这段代码的几个致命问题:

# 问题1: SETNX 没设过期时间 → 死锁风险
#   - 如果持锁的实例在 run_business() 时【崩溃了】(没走到 finally 的 delete):
#     → 锁永远不会被释放! 其他实例永远抢不到锁 → 整个任务再也跑不了(死锁)。

# 问题2(修了问题1后引入): 锁提前过期 → 两个实例同时执行
#   - 假设给锁设了 30 秒过期(SET task_lock 1 EX 30 NX):
#   - 实例A抢到锁, 但 run_business() 这次跑了 40 秒(超过了锁的30秒)。
#   - 第30秒: 锁自动过期了! 但实例A还在执行(它不知道锁没了)。
#   - 第31秒: 实例B来抢锁, 锁已过期, B 抢到了 → B 也开始执行!
#   - → 现在 A 和 B 【同时】在执行 → 锁形同虚设, 并发问题重现!

# 问题3: 释放锁时, 删了别人的锁
#   - 接上面: 第40秒, 实例A执行完, 走 finally 的 r.delete("task_lock")。
#   - 但此时 task_lock 已经是【实例B抢到的锁】了!
#   - A 的 delete 把 B 的锁删了! → B 的锁没了, 实例C又能抢到 → 雪上加霜。
#   - ★ 根因: A 删锁时, 没有判断"这个锁是不是我自己的"。

# 现象拼图:
#   - 没过期 → 持锁者崩溃就死锁。
#   - 设了过期但业务超时 → 锁提前过期, 多个实例同时执行。
#   - 删锁不验身 → 删了别人的锁, 逻辑彻底乱套。
#   - ★ 根因: 分布式锁看起来简单(抢锁-执行-释放), 但要做对, 有一堆
#     必须处理的边界(过期、超时、误删、原子性), 我一个都没处理好。

看清这几个问题后,我才明白这把锁为什么形同虚设。我那段"看起来标准"的代码,藏着三个致命问题。问题一:SETNX 没设过期时间 → 死锁风险——如果持锁实例在执行时崩溃了(没走到 finally 的 delete),锁永远不会被释放,其他实例永远抢不到、整个任务再也跑不了问题二(给锁设了过期后引入):锁提前过期 → 两个实例同时执行——实例 A 抢到锁但业务跑了 40 秒(超过锁的 30 秒过期),第 30 秒锁自动过期、A 还在执行,第 31 秒实例 B 抢到锁也开始执行,A 和 B 同时执行、锁形同虚设问题三:释放锁时删了别人的锁——第 40 秒 A 执行完去 delete,但此时锁已经是 B 抢到的了,A 把 B 的锁删了、逻辑彻底乱套;根因是 A 删锁时没判断"这个锁是不是我自己的"归根结底:分布式锁看起来简单(抢锁-执行-释放),但要做对,有一堆必须处理的边界(过期、超时、误删、原子性),我一个都没处理好

第一件事:搞懂分布式锁的几个必备要素

要解决它,得先搞懂一个正确的分布式锁,必须满足哪些要素。

分布式锁的必备要素

# 一、互斥性: 同一时刻只有一个客户端能持有锁(锁的最基本职责)。

# 二、防死锁: 锁必须能"自动释放"(设过期时间)
#   - 否则持锁者崩溃/卡住, 锁永不释放 → 所有人都抢不到 → 死锁。
#   - 所以: 加锁必须带过期时间(TTL)。
#   - ★ 且"加锁 + 设过期"必须是【原子】的!
#     SETNX 然后再 EXPIRE 是两步, 中间崩溃就又死锁了。
#     要用 SET key value EX seconds NX(一条命令原子完成抢锁+设过期)。

# 三、锁的"身份": 每个客户端的锁要有唯一标识(防止误删别人的锁)
#   - 加锁时, value 存一个【唯一值】(如 UUID / 客户端ID+随机数)。
#   - 释放锁时, 先检查"锁的value是不是我的", 是我的才删。
#   - ★ 且"检查 + 删除"也必须是【原子】的(用 Lua 脚本)!
#     否则"检查是我的"之后、"删除"之前, 锁过期了被别人拿走, 还是会误删。

# 四、过期时间怎么定? —— 锁提前过期是个难题
#   - 设短了: 业务没执行完锁就过期 → 多个客户端同时执行(问题2)。
#   - 设长了: 持锁者崩溃后, 要等很久锁才释放 → 阻塞其他人。
#   - 理想方案: "看门狗(watchdog)"自动续期 —— 持锁期间, 后台线程定期
#     给锁"续命"(延长过期时间), 业务执行完才停止续期并释放。
#     这样锁既不会"业务没完就过期", 也不会"崩溃后永不释放"(续期会停)。

# 五、可重入(进阶): 同一个客户端能否重复获取同一把锁(像 ReentrantLock)。

# 核心: 分布式锁要 互斥 + 防死锁(原子地加锁+设过期SET NX EX)+ 锁带唯一身份(防误删,
#   原子地检查+删除用Lua)+ 处理锁提前过期(看门狗自动续期); 这些边界一个都不能少。

想透分布式锁的必备要素,才知道一个正确的锁有多少讲究。一、互斥性:同一时刻只有一个客户端能持有锁(最基本职责)。二、防死锁:锁必须能自动释放(设过期时间)——否则持锁者崩溃锁永不释放;且"加锁 + 设过期"必须是原子的(SETNXEXPIRE 两步,中间崩溃又死锁,要用 SET key value EX seconds NX 一条命令原子完成)三、锁的"身份":每个客户端的锁要有唯一标识(防误删)——加锁时 value 存唯一值(UUID),释放时先检查"是不是我的锁"才删;且"检查 + 删除"也必须原子(用 Lua 脚本),否则检查后删除前锁被别人拿走还是会误删四、过期时间怎么定?(锁提前过期是难题)——设短了业务没完就过期、设长了崩溃后阻塞别人;理想方案是"看门狗(watchdog)"自动续期:持锁期间后台线程定期续命,业务完才停止续期并释放,既不会业务没完就过期、也不会崩溃后永不释放五、可重入(进阶):同一客户端能否重复获取同一把锁。

第二件事:正解——原子加锁 + 唯一标识 + Lua 释放 + 自动续期

搞懂了原理,正解就清晰了:原子地加锁+设过期、锁带唯一标识、用 Lua 脚本原子地"验身+删除"、用看门狗自动续期,或直接用 Redisson

import redis, uuid, threading, time
r = redis.Redis()

# ====== 正解一: 原子加锁(SET NX EX) + 唯一标识 ======
def acquire_lock(key, ttl=30):
    token = str(uuid.uuid4())   # ★ 唯一标识: 这把锁是"我"的凭证
    # SET key token EX ttl NX: 原子地完成"抢锁 + 设过期"(一条命令)
    locked = r.set(key, token, ex=ttl, nx=True)
    return token if locked else None
# → 一条命令搞定"抢锁+设过期", 不会出现"抢到锁但没来得及设过期就崩溃"。

# ====== 正解二: 用 Lua 脚本原子地"验身 + 删除"释放锁 ======
RELEASE_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])     -- 是我的锁才删
else
    return 0                              -- 不是我的锁, 不删(防误删!)
end
"""
def release_lock(key, token):
    # Lua 脚本在 Redis 里【原子】执行"检查是不是我的 + 删除"
    r.eval(RELEASE_SCRIPT, 1, key, token)
# → "检查"和"删除"原子完成, 不会出现"检查是我的之后、删除之前锁被别人拿走"。
#   token 对不上(锁已是别人的)就不删 → 杜绝"误删别人的锁"。

# ====== 正解三: 看门狗自动续期(解决"锁提前过期")======
def with_lock(key, ttl=30):
    token = acquire_lock(key, ttl)
    if not token:
        return False
    stop = threading.Event()
    # 后台线程: 持锁期间, 每 ttl/3 给锁续期一次(延长过期时间)
    def watchdog():
        while not stop.wait(ttl / 3):
            # 续期也要验身(用Lua): 是我的锁才续期
            r.eval("if redis.call('get',KEYS[1])==ARGV[1] then "
                   "return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end",
                   1, key, token, ttl)
    t = threading.Thread(target=watchdog, daemon=True); t.start()
    try:
        run_business()       # 不管业务跑多久, watchdog 都会续期, 锁不会提前过期
    finally:
        stop.set()           # 停止续期
        release_lock(key, token)   # 释放锁(验身删除)
    return True
# → 业务执行期间锁一直被续期(不会提前过期); 业务完/崩溃则续期停止、锁自然释放。

# ====== 正解四(强烈推荐): 直接用 Redisson(Java) / 成熟的库 ======
# 自己实现分布式锁, 边界太多、很容易出错。生产强烈建议用成熟库:
# - Java: Redisson(内置看门狗自动续期、可重入、公平锁等, 开箱即用)。
#   RLock lock = redisson.getLock("task_lock");
#   lock.lock();  try { 业务 } finally { lock.unlock(); }
# - 别自己造轮子, 除非你非常清楚所有边界。

# ====== 正解五(更高可靠): 多Redis节点的 RedLock(看场景)======
# 单 Redis 主从切换时, 锁可能丢失(主挂了从还没同步锁)。
# 对一致性要求极高的场景, 可考虑 RedLock(多个独立Redis节点过半数才算加锁成功)。
# 但 RedLock 有争议、复杂, 多数场景单节点Redis锁+续期已够用。

# 核心: 原子加锁(SET NX EX)+ 唯一token标识 + Lua脚本原子验身删除(防误删)
#   + 看门狗自动续期(防提前过期); 强烈建议直接用Redisson等成熟库, 别自己造轮子。

修复的核心,是"把分布式锁的每一个边界都处理到位,或者直接用经过验证的成熟库"正解一:原子加锁(SET NX EX)+ 唯一标识——SET key token EX ttl NX 一条命令原子完成"抢锁+设过期",token 用 UUID 作为"这把锁是我的"的凭证正解二:用 Lua 脚本原子地"验身 + 删除"释放锁——Lua 在 Redis 里原子执行"检查是不是我的 + 删除",token 对不上就不删,杜绝误删别人的锁正解三:看门狗自动续期(解决锁提前过期)——后台线程每 ttl/3 给锁续期一次,业务执行期间锁一直被续期(不会提前过期)、业务完或崩溃则续期停止、锁自然释放正解四(强烈推荐):直接用 Redisson 等成熟库——自己实现边界太多易错,Redisson 内置看门狗续期、可重入、公平锁,开箱即用,别造轮子正解五(更高可靠):多 Redis 节点的 RedLock(单 Redis 主从切换时锁可能丢失,对一致性极高的场景可考虑,但有争议、复杂,多数场景单节点+续期已够用)。归根结底:原子加锁 + 唯一 token + Lua 原子验身删除(防误删)+ 看门狗续期(防提前过期);强烈建议直接用 Redisson,别自己造轮子。

第三件事:分布式锁的几个深层问题

排查后我意识到,分布式锁的"坑"比表面看到的还要深。我把几个深层问题梳理了一遍。

分布式锁的深层问题

# 一、锁提前过期的"根本困境"(即使有续期也要想清楚)
#   - 续期(看门狗)能缓解, 但若"持锁进程卡死/网络分区(GC停顿、Full GC、
#     线程卡住)", 续期线程也可能没及时续上 → 锁还是可能过期、被别人拿到。
#   - 极端情况下, "两个进程都以为自己持有锁"是分布式锁理论上难以100%杜绝的。
#   - 应对: 关键业务"加锁 + 业务本身也要幂等"(双保险, 见幂等篇)!
#     即: 别把"安全"完全押在锁上, 业务逻辑本身也要能容忍"万一锁失效了"。

# 二、单点 Redis 的可靠性
#   - 单 Redis 挂了 → 锁服务不可用。
#   - Redis 主从: 主挂了切到从, 但锁可能还没同步到从 → 锁"丢失"。
#   - 应对: Redis 高可用(哨兵/集群); 极致一致性用 RedLock 或 ZooKeeper/etcd
#     (基于一致性协议的锁, 更可靠但更重)。

# 三、锁的粒度
#   - 锁太粗(一把大锁锁住所有): 并发度低, 性能差。
#   - 锁太细(每个小资源一把锁): 复杂、易死锁。
#   - 按"实际需要互斥的最小资源"加锁(如按订单ID加锁, 而非全局一把锁)。

# 四、要不要用分布式锁? —— 先想有没有更简单的方案
#   - 很多"需要分布式锁"的场景, 其实可以用其他方式避免:
#     * 唯一约束/幂等: 用数据库唯一索引防重复(比锁简单可靠)。
#     * 单点执行: 定时任务用分布式调度框架(如XXL-JOB)保证只一个实例跑。
#     * 乐观锁: 用版本号 CAS, 避免悲观锁。
#   - 分布式锁是"重武器", 能不用就不用(它有上述这么多坑)。

# 核心: 锁提前过期理论上难100%杜绝, 关键业务要"锁+幂等"双保险; 注意单点Redis可靠性
#   (高可用/RedLock)、锁粒度; 且优先考虑唯一约束/幂等/调度框架等更简单的替代方案。

排查让我看到分布式锁更深的几个问题。一、锁提前过期的"根本困境"——即使有续期,若持锁进程卡死/网络分区(GC 停顿、线程卡住),续期线程也可能没及时续上、锁还是可能被别人拿到;"两个进程都以为持有锁"在理论上难 100% 杜绝;应对是关键业务"加锁 + 业务本身也幂等"双保险,别把安全完全押在锁上二、单点 Redis 的可靠性——单 Redis 挂了锁服务不可用,主从切换时锁可能丢失;应对是 Redis 高可用,极致一致性用 RedLock 或 ZooKeeper/etcd三、锁的粒度——太粗并发低、太细易死锁,按"实际需要互斥的最小资源"加锁(如按订单 ID 而非全局一把锁)四、要不要用分布式锁?先想有没有更简单的方案——很多场景可用数据库唯一约束/幂等防重复、用分布式调度框架(XXL-JOB)保证单点执行、用乐观锁;分布式锁是重武器、有这么多坑,能不用就不用下面这张图,是这次分布式锁没锁住的成因与解法:

第四件事:分布式锁错误写法 vs 正确写法速查

这次踩坑后,我把分布式锁的常见错误和对应的正确做法整理成一张表。

环节 错误写法 正确写法
加锁 SETNX 不设过期 SET key token EX ttl NX(原子)
加锁+过期 SETNX 再 EXPIRE(两步) 一条 SET NX EX 命令
锁标识 value 固定值"1" value 用唯一 token(UUID)
释放 直接 DEL Lua 验身后再删
验身+删除 get 判断后再 del(两步) Lua 脚本原子完成
过期时间 固定值(易提前过期/太长) 看门狗自动续期
整体 自己手写(易错) 用 Redisson 等成熟库

这张表,把分布式锁的"错与对"逐环列了出来。它清楚地显示:分布式锁的每一个环节(加锁、设过期、标识、释放、验身),都有"看起来对、实则错"的写法和"真正正确"的写法,而错与对的差别,常常就在"是不是原子的""有没有唯一标识"这些细节上它给我的最大启发是:分布式锁是一个"看起来简单、做对极难"的典型——它的"简单"(抢锁-执行-释放)只是表象,它的""藏在分布式环境带来的一堆边界里(崩溃、超时、并发、网络分区)而这些边界,恰恰是"偶发的、难复现的、平时测不出来的"——你本地测、小流量测,锁都"工作得好好的",直到生产上某次"业务恰好超时"或"实例恰好崩溃",问题才暴露。这让我领悟到一个关于"分布式原语"的认知:分布式锁、分布式事务、分布式 ID 这类"分布式原语",看起来都是"很基础的小功能",但它们的正确实现,凝结了大量对分布式环境复杂性的考量;自己手搓一个"看起来能用"的版本很容易,但要"在各种边界下都正确"极难所以,对这类"简单但极易错"的分布式原语,最明智的选择是站在巨人的肩膀上——用经过大规模生产验证的成熟库(Redisson),而不是自己造一个充满隐患的轮子

第五件事:什么时候真的需要分布式锁

这次也让我重新思考"到底要不要用分布式锁"。我把它和替代方案整理了一下。

场景 能否用更简单的替代 说明
防止重复插入 ✓ 数据库唯一索引 比锁简单可靠
防止重复处理(消息/请求) ✓ 幂等设计(去重表) 见幂等篇
定时任务只一个实例跑 ✓ 分布式调度框架 XXL-JOB等天然支持
更新冲突 ✓ 乐观锁(版本号CAS) 无需悲观锁
限流/计数 ✓ Redis 原子操作(INCR) 原子命令即可
复杂的临界区互斥 △ 可能确实需要分布式锁 且要用成熟库

这张表,让我看到"需要分布式锁"的场景其实比想象的少。很多看似"非锁不可"的需求,都有更简单可靠的替代:防重复插入用数据库唯一索引、防重复处理用幂等设计、定时任务单实例用调度框架、更新冲突用乐观锁、限流计数用 Redis 原子操作;真正需要分布式锁的,往往只剩"复杂的临界区互斥"这类场景(且要用成熟库)。它给我的最大启发是:面对一个问题,在选用一个"复杂、重、易错"的方案(分布式锁)之前,先问一句"有没有更简单的方案能解决它?"我之前的思路是"要保证互斥 → 上分布式锁",这个条件反射让我直接跳到了一个最复杂的方案,而忽略了"这个具体场景,其实用数据库唯一约束/幂等就能解决",根本不需要扛出分布式锁这个重武器这让我领悟到一个工程决策的智慧:"能用简单方案解决的,就别用复杂方案"——复杂的方案(分布式锁、分布式事务)不仅实现难、易错,还引入了新的故障点和维护成本;每多引入一份复杂度,就多一份出错的可能所以,"克制地选择最简单够用的方案",本身就是一种重要的架构能力;追求"用对的、够用的、最简单的工具",而不是"用最高级、最复杂的工具"——这,是衡量一个工程师是否成熟的标志之一。

第六件事:要用分布式锁时,我现在的决策习惯

现在每当我想到"加个分布式锁",我都会先按这张图想清楚:

这张图的精髓,是"先找更简单的替代,确实需要才用锁,且用成熟库 + 幂等双保险"第一问 "有没有更简单的替代":防重复插入用唯一索引、防重复处理用幂等、定时单实例用调度框架、更新冲突用乐观锁;只有"确实需要互斥临界区"才用分布式锁。用锁时:优先用 Redisson 等成熟库(别手写);非要手写则原子加锁 + 唯一 token + Lua 释放 + 看门狗续期而最重要的兜底:关键业务"锁 + 业务幂等"双保险,别把安全全押在锁上(因为锁理论上难 100% 可靠)。这套决策,让我面对互斥需求时,从"条件反射上分布式锁"变成了"先找更简单方案、非用不可才慎重地用"——核心始终是:分布式锁是易错的重武器,优先用更简单的替代,非用不可则用成熟库并配业务幂等双保险。

我立下的几条规矩

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

  1. 加锁必须原子地设过期。用 SET key token EX ttl NX,别 SETNX 后再 EXPIRE,更别不设过期。
  2. 锁要带唯一标识。value 存 UUID,释放时验身,杜绝误删别人的锁。
  3. 释放锁用 Lua 原子"验身+删除"。别 get 判断后再 del(两步之间锁可能易主)。
  4. 处理锁提前过期。用看门狗自动续期,别让业务没执行完锁就过期。
  5. 别自己造轮子。用 Redisson 等成熟库,它处理好了所有边界。
  6. 关键业务锁+幂等双保险。锁理论上难 100% 可靠,业务本身也要能容忍锁失效。
  7. 优先找更简单的替代。唯一索引/幂等/调度框架/乐观锁能解决的,别上分布式锁。

附:一个相对完整的 Redis 分布式锁实现(含续期)

口说无凭。下面给一个把"原子加锁 + 唯一标识 + Lua 释放 + 看门狗续期"都实现了的分布式锁类:

import redis, uuid, threading, contextlib

class RedisLock:
    # Lua: 验身后才删除(原子)
    _RELEASE = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else return 0 end"""
    # Lua: 验身后才续期(原子)
    _RENEW = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('expire', KEYS[1], ARGV[2])
    else return 0 end"""

    def __init__(self, client, key, ttl=30):
        self.r = client
        self.key = key
        self.ttl = ttl
        self.token = str(uuid.uuid4())     # 唯一标识(本次持锁的凭证)
        self._stop = threading.Event()
        self._wd = None

    def acquire(self, blocking=True, retry_interval=0.1, timeout=10):
        import time
        deadline = time.monotonic() + timeout
        while True:
            # 原子: 抢锁 + 设过期
            if self.r.set(self.key, self.token, ex=self.ttl, nx=True):
                self._start_watchdog()      # 抢到锁, 启动续期
                return True
            if not blocking or time.monotonic() > deadline:
                return False
            time.sleep(retry_interval)       # 没抢到, 等一会儿重试

    def _start_watchdog(self):
        def renew():
            # 每 ttl/3 续期一次, 直到 release 停止
            while not self._stop.wait(self.ttl / 3):
                self.r.eval(self._RENEW, 1, self.key, self.token, self.ttl)
        self._wd = threading.Thread(target=renew, daemon=True)
        self._wd.start()

    def release(self):
        self._stop.set()                     # 停止续期
        # 原子: 验身后才删(防误删别人的锁)
        self.r.eval(self._RELEASE, 1, self.key, self.token)

    # 支持 with 语法, 自动 acquire/release
    @contextlib.contextmanager
    def __call__(self):
        if not self.acquire():
            raise TimeoutError(f"获取锁失败: {self.key}")
        try:
            yield
        finally:
            self.release()

# ====== 用法 ======
# r = redis.Redis()
# lock = RedisLock(r, "task_lock", ttl=30)
# with lock():                  # 自动加锁, 块结束自动释放
#     run_business()            # 期间看门狗自动续期, 不怕业务超时
# # 出了 with, 锁自动释放(验身删除)
#
# ★ 但生产强烈建议直接用 Redisson(Java)等成熟库, 这个仅供理解原理!

# 核心: 这个类实现了 原子加锁(set nx ex)+唯一token+Lua验身释放+看门狗续期+with语法;
#   展示了一个"正确"的分布式锁要处理多少细节 —— 也正因如此, 生产更该用成熟库。

这个 RedisLock 类,把前面讲的所有要点,落成了一个相对完整的实现。它涵盖了:原子加锁(set nx ex)、唯一 token 标识、Lua 脚本原子"验身释放"和"验身续期"、看门狗自动续期、以及方便的 with 语法(自动加锁/释放)但我想用它传递的,恰恰是一个略带反讽的结论:看看这个类有多少代码、处理了多少细节(原子性、唯一标识、Lua 脚本、后台续期线程、停止信号……),你就明白"正确地实现一个分布式锁"有多不容易——而这还只是单节点 Redis 锁,没考虑主从切换、RedLock 这些更复杂的情况。所以这个实现最大的价值,不是"拿去用",而是"帮你理解原理、并由此领悟到:你真的不应该自己手写它"。这,正是我想用这段代码,留给每个做分布式系统的人的最后一课:当你"亲手实现一遍"一个东西、真切地体会到它有多少隐藏的复杂细节时,你才会从心底里认同"用成熟的轮子"的价值"不要重复造轮子"这句话,很多人都听过,但只有当你真正尝试去造一遍、被它的复杂性折磨过之后,你才会真正理解这句话的分量、才会对那些已有的、经过千锤百炼的轮子(Redisson、各种成熟库)心怀感激所以,我建议每个工程师都可以"为了理解而亲手实现一遍"这类经典原语(分布式锁、连接池、限流器),但实现完之后,请把你的实现锁进抽屉、在生产里老老实实用成熟库——前者让你"懂原理",后者让你"不踩坑";懂原理是为了用好轮子、并在轮子出问题时能排查,而不是为了自己造一个充满隐患的轮子上生产。这,是我从这场"分布式锁"事故里,带走的、关于"理解与复用"的、最实在的领悟。

写在最后

回头看,这场由"分布式锁没锁住"引发的、并发防不住还互删锁的事故,真正教给我的,远不止"分布式锁要怎么正确实现"这一套技巧。它让我对"简单"和"复杂"的关系,有了更深刻的体会。分布式锁,是一个绝佳的"看起来简单、实则复杂"的例子:它的接口简单得不能再简单(加锁、解锁),简单到让我掉以轻心、随手就写了一个"看起来对"的版本;可它的正确实现,却暗藏着崩溃、超时、并发、网络分区等一系列分布式环境特有的、极其棘手的边界我栽就栽在,被它"简单的表象"骗了,用"写单机锁的随意",去对待一个"本质上极其复杂的分布式问题"这让我领悟到一个深刻的工程道理:一个东西"用起来简单"(接口简单),和它"实现起来简单 / 正确地用很简单",完全是两码事;恰恰是那些"接口简单、人人都觉得自己会"的东西(分布式锁、缓存、时间日期、字符编码……),最容易让人因为"轻视"而栽进它底层的复杂性里这也呼应了那句名言:"简单是表象,复杂是内核"。所以,这件事给我的最大警示是:对那些"接口看起来简单"的东西,不要因为它的简单外表就轻视它;尤其当它身处一个复杂的环境(分布式、并发)时,要对它表象之下可能隐藏的复杂性保持敬畏;要么花时间真正理解并正确处理它的所有边界,要么(更明智地)站在巨人的肩膀上,用经过千锤百炼的成熟方案,而不是凭着"这不挺简单"的错觉自己硬来对简单表象之下的复杂保持敬畏——这,是我用一次"分布式锁没锁住"的事故,换来的、关于架构、也关于"简单与复杂"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次想加分布式锁时,先想想"有没有更简单的替代"、并优先选用 Redisson,那我对着那把形同虚设的锁熬的这大半天,就值了。

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

我的 RAG 知识库问答总是答非所问、要么答不全要么牛头不对马嘴,模型和向量库都没问题,我对着文档切分的 chunking 排查了大半天的复盘

2026-6-2 7:57:37

技术教程

我的 C# 异步方法明明用 try-catch 包住了,里面抛的异常却直接崩了整个进程、catch 根本没拦住,我对着 async void 排查了大半天的复盘

2026-6-2 8:09:23

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