<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>分布式锁 on 吾爱主机</title><link>https://blog.waihost.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/</link><description>Recent content in 分布式锁 on 吾爱主机</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Thu, 30 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://blog.waihost.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/index.xml" rel="self" type="application/rss+xml"/><item><title>Redis 分布式锁：互斥、安全释放与续期工程实践</title><link>https://blog.waihost.com/posts/redis-distributed-lock-mutex-renewal-practice/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/redis-distributed-lock-mutex-renewal-practice/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/redis-distributed-lock-mutex-renewal-practice.svg" alt="Featured image of post Redis 分布式锁：互斥、安全释放与续期工程实践" /&gt;&lt;p&gt;下单扣库存、定时任务防重、接口幂等窗口、分片任务抢占——多进程/多实例同时触达同一共享资源时，都需要&lt;strong&gt;互斥&lt;/strong&gt;。本地 &lt;code&gt;synchronized&lt;/code&gt; 或 &lt;code&gt;Mutex&lt;/code&gt; 只覆盖本机；跨进程时，工程上常见做法是把锁状态放到 &lt;strong&gt;Redis&lt;/strong&gt; 这类高可用共享存储里。&lt;/p&gt;
&lt;p&gt;本文依据 Redis 官方 &lt;em&gt;Distributed Locks with Redis&lt;/em&gt;、&lt;code&gt;SET&lt;/code&gt; 命令文档（含 Patterns）、&lt;em&gt;Scripting with Lua&lt;/em&gt; 与 &lt;code&gt;GETDEL&lt;/code&gt; 说明，把&lt;strong&gt;单实例正确加解锁、误删他人锁、主从异步复制下的安全边界、锁续期，以及 Redlock/fencing token 的工程取舍&lt;/strong&gt;讲清楚，并给出可复现命令实验与排错清单。它和本站「缓存穿透/击穿/雪崩」互补：那篇讲缓存一致性与流量保护，本篇讲&lt;strong&gt;跨实例互斥原语本身&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="一问题背景分布式互斥要什么"&gt;&lt;a href="#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af%e5%88%86%e5%b8%83%e5%bc%8f%e4%ba%92%e6%96%a5%e8%a6%81%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;一、问题背景：分布式互斥要什么
&lt;/h2&gt;&lt;p&gt;官方把有效的分布式锁最小保证概括为三条（Safety / Liveness）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;属性&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;工程直觉&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;互斥（Safety）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;任意时刻最多一个客户端持有锁&lt;/td&gt;
					&lt;td&gt;两台机器不能同时“以为自己锁住了”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;无死锁（Liveness A）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;持锁客户端崩溃或分区后，锁最终可被再次获取&lt;/td&gt;
					&lt;td&gt;必须有 TTL / 自动释放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;容错（Liveness B）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;多数 Redis 节点存活时仍可加解锁（面向 Redlock 场景）&lt;/td&gt;
					&lt;td&gt;单点故障与多数派策略&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;只满足“看起来能 SET 成功”不够。真正会在生产踩雷的，通常是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;加锁非原子&lt;/strong&gt;：&lt;code&gt;SETNX&lt;/code&gt; 成功后再 &lt;code&gt;EXPIRE&lt;/code&gt;，进程在中间崩溃 → 永久锁死；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解锁不安全&lt;/strong&gt;：业务超时后锁已过期被别人抢走，本客户端仍 &lt;code&gt;DEL&lt;/code&gt; → &lt;strong&gt;删掉别人的锁&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无 token 签名&lt;/strong&gt;：所有客户端写同一个固定 value，无法判断“这把锁还是不是我的”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把主从 failover 当强一致&lt;/strong&gt;：异步复制下会出现双持锁窗口。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="二核心机制单实例正确加解锁"&gt;&lt;a href="#%e4%ba%8c%e6%a0%b8%e5%bf%83%e6%9c%ba%e5%88%b6%e5%8d%95%e5%ae%9e%e4%be%8b%e6%ad%a3%e7%a1%ae%e5%8a%a0%e8%a7%a3%e9%94%81" class="header-anchor"&gt;&lt;/a&gt;二、核心机制：单实例正确加解锁
&lt;/h2&gt;&lt;h3 id="21-原子加锁set-key-token-nx-px-ttl"&gt;&lt;a href="#21-%e5%8e%9f%e5%ad%90%e5%8a%a0%e9%94%81set-key-token-nx-px-ttl" class="header-anchor"&gt;&lt;/a&gt;2.1 原子加锁：&lt;code&gt;SET key token NX PX ttl&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;官方单实例推荐写法（毫秒 TTL 示例）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET resource_name my_random_value NX PX 30000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对应语义（见 &lt;code&gt;SET&lt;/code&gt; 文档）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;选项&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;NX&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;仅当 key 不存在&lt;/strong&gt;时才设置&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;PX milliseconds&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;以毫秒设置过期（也可用 &lt;code&gt;EX seconds&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;value = 随机 token&lt;/td&gt;
					&lt;td&gt;标识“这把锁属于哪个客户端/哪次加锁”&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;SET&lt;/code&gt; 成功返回 &lt;code&gt;OK&lt;/code&gt; 表示抢到锁；key 已存在时（&lt;code&gt;NX&lt;/code&gt; 条件不满足）返回空，表示未抢到。&lt;code&gt;NX&lt;/code&gt; 与过期时间在&lt;strong&gt;同一条命令&lt;/strong&gt;里完成，避免“加锁成功但没 TTL”的经典坑。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SET&lt;/code&gt; 文档的 Patterns 小节也写了同类形态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET resource-name anystring NX EX max-lock-time
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并强调：简单 &lt;code&gt;DEL&lt;/code&gt; 解锁可工作，但更稳健的做法是 &lt;strong&gt;token + 条件删除&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="22-为什么-value-必须是随机-token"&gt;&lt;a href="#22-%e4%b8%ba%e4%bb%80%e4%b9%88-value-%e5%bf%85%e9%a1%bb%e6%98%af%e9%9a%8f%e6%9c%ba-token" class="header-anchor"&gt;&lt;/a&gt;2.2 为什么 value 必须是随机 token
&lt;/h3&gt;&lt;p&gt;官方要求 value &lt;strong&gt;在所有客户端、所有加锁请求间唯一&lt;/strong&gt;。建议量级约 20 字节随机（如 &lt;code&gt;/dev/urandom&lt;/code&gt;）；也可用“微秒时间戳 + 客户端 ID”等折中方案（安全性略弱，但多数场景够用）。&lt;/p&gt;
&lt;p&gt;token 的唯一用途：解锁时证明“我仍是当前持有者”，避免过期后误删他人锁。&lt;/p&gt;
&lt;h3 id="23-安全解锁条件删除而不是裸-del"&gt;&lt;a href="#23-%e5%ae%89%e5%85%a8%e8%a7%a3%e9%94%81%e6%9d%a1%e4%bb%b6%e5%88%a0%e9%99%a4%e8%80%8c%e4%b8%8d%e6%98%af%e8%a3%b8-del" class="header-anchor"&gt;&lt;/a&gt;2.3 安全解锁：条件删除，而不是裸 &lt;code&gt;DEL&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;错误写法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DEL resource_name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;时序灾难：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端 A 加锁，TTL = 30s；&lt;/li&gt;
&lt;li&gt;A 业务卡住超过 30s，key 过期；&lt;/li&gt;
&lt;li&gt;客户端 B 成功 &lt;code&gt;SET NX&lt;/code&gt; 拿到同一资源；&lt;/li&gt;
&lt;li&gt;A 恢复后执行 &lt;code&gt;DEL&lt;/code&gt; → &lt;strong&gt;B 的锁被删掉&lt;/strong&gt;，C 又能进入临界区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官方正确路径有两层：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（1）Redis 8.4+：&lt;code&gt;DELEX&lt;/code&gt; 条件删除（文档写法）&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DELEX key IFEQ my_random_value
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;（2）更通用：Lua 脚本（8.4 之前也适用）&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;get&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redis.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;del&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用形态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EVAL &amp;#34;&amp;lt;script&amp;gt;&amp;#34; 1 resource_name token-value
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;GET&lt;/code&gt; 比较与 &lt;code&gt;DEL&lt;/code&gt; 在脚本内完成。官方 &lt;em&gt;Scripting with Lua&lt;/em&gt; 明确：脚本在服务器侧&lt;strong&gt;原子执行&lt;/strong&gt;，执行期间阻塞其他活动——这对“校验 token 再删除”至关重要，客户端侧的 &lt;code&gt;GET&lt;/code&gt; + &lt;code&gt;DEL&lt;/code&gt; 两步命令&lt;strong&gt;不是&lt;/strong&gt;安全等价物。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;注意：&lt;code&gt;GETDEL&lt;/code&gt; 会“读出并删除”，但它&lt;strong&gt;不校验 token&lt;/strong&gt;，不能单独当作安全解锁命令。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="24-锁有效期lock-validity-time的双重含义"&gt;&lt;a href="#24-%e9%94%81%e6%9c%89%e6%95%88%e6%9c%9flock-validity-time%e7%9a%84%e5%8f%8c%e9%87%8d%e5%90%ab%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;2.4 锁有效期（lock validity time）的双重含义
&lt;/h3&gt;&lt;p&gt;TTL 既是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动释放时间&lt;/strong&gt;（持锁者崩溃后的死锁自愈）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持锁者必须在此窗口内完成工作&lt;/strong&gt;的时间预算。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;互斥保证是&lt;strong&gt;时间窗口内的互斥&lt;/strong&gt;，不是“进程活着就永远持锁”。业务若可能超过 TTL，要么加大合理 TTL，要么做&lt;strong&gt;续期&lt;/strong&gt;，并在业务层准备好&lt;strong&gt;超时后的幂等/回滚&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="三主从复制为什么不够天然安全"&gt;&lt;a href="#%e4%b8%89%e4%b8%bb%e4%bb%8e%e5%a4%8d%e5%88%b6%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e5%a4%9f%e5%a4%a9%e7%84%b6%e5%ae%89%e5%85%a8" class="header-anchor"&gt;&lt;/a&gt;三、主从复制为什么不够“天然安全”
&lt;/h2&gt;&lt;p&gt;单实例方案在“偶尔双持可接受”的业务里很实用；但若把 Redis 主从 + 自动 failover 当成强一致锁，会踩官方点名的竞态：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端 A 在 master 上拿到锁；&lt;/li&gt;
&lt;li&gt;master 在把该写复制到 replica 前崩溃；&lt;/li&gt;
&lt;li&gt;replica 升主；&lt;/li&gt;
&lt;li&gt;客户端 B 在新 master 上再次 &lt;code&gt;SET NX&lt;/code&gt; 成功 → &lt;strong&gt;Safety 被破坏&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原因：Redis 复制默认是&lt;strong&gt;异步&lt;/strong&gt;的。官方态度很务实：若故障场景下短暂双持可接受，复制方案可用；否则应认真评估 &lt;strong&gt;Redlock&lt;/strong&gt; 或更强的一致性组件，并在业务侧使用 &lt;strong&gt;fencing token&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="四redlock-概要多数派--耗时校验"&gt;&lt;a href="#%e5%9b%9bredlock-%e6%a6%82%e8%a6%81%e5%a4%9a%e6%95%b0%e6%b4%be--%e8%80%97%e6%97%b6%e6%a0%a1%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;四、Redlock 概要：多数派 + 耗时校验
&lt;/h2&gt;&lt;p&gt;Redlock 假设有 &lt;strong&gt;N 个独立 Redis master&lt;/strong&gt;（示例 N=5，互不依赖复制协调）。客户端流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;记录当前毫秒时间戳 &lt;code&gt;T1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;用&lt;strong&gt;相同 key 与相同随机 value&lt;/strong&gt;，在 N 个实例上并行 &lt;code&gt;SET ... NX PX&lt;/code&gt;；对每个实例使用&lt;strong&gt;远小于锁 TTL&lt;/strong&gt; 的连接/命令超时（例如 TTL=10s 时超时约 5–50ms），避免卡在不可用节点上；&lt;/li&gt;
&lt;li&gt;计算耗时 &lt;code&gt;elapsed = now - T1&lt;/code&gt;；&lt;strong&gt;仅当&lt;/strong&gt;在多数实例（至少 &lt;code&gt;N/2+1&lt;/code&gt;）加锁成功，且 &lt;code&gt;elapsed &amp;lt; TTL&lt;/code&gt; 时，才认为获得锁；&lt;/li&gt;
&lt;li&gt;有效剩余时间约为 &lt;code&gt;TTL - elapsed&lt;/code&gt;（再扣一点时钟漂移余量）；&lt;/li&gt;
&lt;li&gt;若失败：对&lt;strong&gt;所有&lt;/strong&gt;实例尝试解锁（包括“以为没锁上”的），尽快释放部分锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;失败重试应加&lt;strong&gt;随机退避&lt;/strong&gt;，降低多客户端同时抢锁导致的“脑裂式空转”。释放时无论是否自认成功，都应尝试释放，降低依赖 TTL 的可用性惩罚。&lt;/p&gt;
&lt;h3 id="41-续期lock-extension"&gt;&lt;a href="#41-%e7%bb%ad%e6%9c%9flock-extension" class="header-anchor"&gt;&lt;/a&gt;4.1 续期（lock extension）
&lt;/h3&gt;&lt;p&gt;若工作可拆成小步骤，可用较短默认 TTL，并在接近过期时向多数实例发送 Lua：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅当 key 存在且 value 仍是自己的 token 时，延长 TTL；&lt;/li&gt;
&lt;li&gt;仅当多数实例续期成功且仍在有效时间内，才认为续期成功；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必须限制最大续期次数&lt;/strong&gt;，否则可能破坏 liveness（锁永远被同一客户端拖住）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这与许多客户端库里的 “watchdog / lock watchdog” 思路一致：续期是&lt;strong&gt;持锁期间的重新确认&lt;/strong&gt;，不是“进程活着就一定安全”。&lt;/p&gt;
&lt;h3 id="42-时钟与一致性免责声明官方-disclaimer"&gt;&lt;a href="#42-%e6%97%b6%e9%92%9f%e4%b8%8e%e4%b8%80%e8%87%b4%e6%80%a7%e5%85%8d%e8%b4%a3%e5%a3%b0%e6%98%8e%e5%ae%98%e6%96%b9-disclaimer" class="header-anchor"&gt;&lt;/a&gt;4.2 时钟与一致性免责声明（官方 disclaimer）
&lt;/h3&gt;&lt;p&gt;官方在文档末尾明确提醒：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若关心正确性，应实现 &lt;strong&gt;fencing tokens&lt;/strong&gt;；不要假设“进程还活着 = 锁一定还在”；&lt;/li&gt;
&lt;li&gt;Redis TTL 不依赖单调时钟，&lt;strong&gt;墙钟跳变&lt;/strong&gt;理论上可能导致异常；应规范运维（避免手工乱改时间、正确配置 NTP），但仍不能把时钟理想化。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Martin Kleppmann 对 Redlock 的分析与 antirez 的回应均被官方列入阅读清单——工程选型时请当作&lt;strong&gt;必读辩论&lt;/strong&gt;，而不是站队口号。&lt;/p&gt;
&lt;h2 id="五工程增强fencing-token-与临界区设计"&gt;&lt;a href="#%e4%ba%94%e5%b7%a5%e7%a8%8b%e5%a2%9e%e5%bc%bafencing-token-%e4%b8%8e%e4%b8%b4%e7%95%8c%e5%8c%ba%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;五、工程增强：Fencing Token 与临界区设计
&lt;/h2&gt;&lt;p&gt;即使锁服务偶发双持，业务仍可把破坏降到最低：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fencing token&lt;/strong&gt;：每次成功加锁从&lt;strong&gt;单调递增&lt;/strong&gt;源（DB 序列、强一致元数据服务等）取一个 token；写共享资源时带上该 token；资源端&lt;strong&gt;拒绝&lt;/strong&gt;小于已接受 token 的写。这样后拿到“旧锁”的客户端写不进去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临界区尽量短&lt;/strong&gt;：锁内只做“决策 + 提交意图”，重 IO 放到锁外并用幂等键保护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务幂等&lt;/strong&gt;：以订单号、任务 ID、版本号做天然去重，锁失败/超时可安全重试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可观测性&lt;/strong&gt;：记录加锁耗时、失败原因、续期次数、解锁脚本返回 0 的次数（token 不匹配）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="六可复现实验单实例"&gt;&lt;a href="#%e5%85%ad%e5%8f%af%e5%a4%8d%e7%8e%b0%e5%ae%9e%e9%aa%8c%e5%8d%95%e5%ae%9e%e4%be%8b" class="header-anchor"&gt;&lt;/a&gt;六、可复现实验（单实例）
&lt;/h2&gt;&lt;p&gt;下面用 &lt;code&gt;redis-cli&lt;/code&gt; 演示互斥、过期与&lt;strong&gt;误删防护&lt;/strong&gt;。假设本地 &lt;code&gt;6379&lt;/code&gt; 有 Redis。&lt;/p&gt;
&lt;h3 id="61-互斥第二个客户端抢不到"&gt;&lt;a href="#61-%e4%ba%92%e6%96%a5%e7%ac%ac%e4%ba%8c%e4%b8%aa%e5%ae%a2%e6%88%b7%e7%ab%af%e6%8a%a2%e4%b8%8d%e5%88%b0" class="header-anchor"&gt;&lt;/a&gt;6.1 互斥：第二个客户端抢不到
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 终端 A：抢锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:order:1001 &lt;span class="s2"&gt;&amp;#34;token-A-111&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;15000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 终端 B：同 key 再抢&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:order:1001 &lt;span class="s2"&gt;&amp;#34;token-B-222&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;15000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：(nil) —— NX 失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli GET lock:order:1001
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：token-A-111&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli PTTL lock:order:1001
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：正数毫秒&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="62-安全解锁-vs-危险-del"&gt;&lt;a href="#62-%e5%ae%89%e5%85%a8%e8%a7%a3%e9%94%81-vs-%e5%8d%b1%e9%99%a9-del" class="header-anchor"&gt;&lt;/a&gt;6.2 安全解锁 vs 危险 &lt;code&gt;DEL&lt;/code&gt;
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 正确：仅 token 匹配才删&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;del&amp;#34;, KEYS[1]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:order:1001 token-A-111
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 再抢，模拟 B 持锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:order:1001 &lt;span class="s2"&gt;&amp;#34;token-B-222&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;15000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# A 拿着过期旧 token 误删：应失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;del&amp;#34;, KEYS[1]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:order:1001 token-A-111
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：0 —— 不会删掉 B 的锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli GET lock:order:1001
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：仍是 token-B-222&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="63-续期仅持有者可延长"&gt;&lt;a href="#63-%e7%bb%ad%e6%9c%9f%e4%bb%85%e6%8c%81%e6%9c%89%e8%80%85%e5%8f%af%e5%bb%b6%e9%95%bf" class="header-anchor"&gt;&lt;/a&gt;6.3 续期：仅持有者可延长
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:job:9 &lt;span class="s2"&gt;&amp;#34;token-C&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;5000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 错误 token 续期（应不改 TTL / 不改 value）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;pexpire&amp;#34;, KEYS[1], ARGV[2]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:job:9 wrong-token &lt;span class="m"&gt;20000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 正确 token 续期&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;pexpire&amp;#34;, KEYS[1], ARGV[2]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:job:9 token-C &lt;span class="m"&gt;20000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli PTTL lock:job:9
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：接近 20000ms&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="64-伪代码骨架语言无关"&gt;&lt;a href="#64-%e4%bc%aa%e4%bb%a3%e7%a0%81%e9%aa%a8%e6%9e%b6%e8%af%ad%e8%a8%80%e6%97%a0%e5%85%b3" class="header-anchor"&gt;&lt;/a&gt;6.4 伪代码骨架（语言无关）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UNLOCK_LUA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return redis.call(&amp;#34;del&amp;#34;, KEYS[1])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UNLOCK_LUA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;lock not acquired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 可把 token 当作 fencing 的一部分上送&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;生产中请再补：续期协程/线程、最大持锁时间、指标与超时取消。&lt;/p&gt;
&lt;h2 id="七常见坑与排查清单"&gt;&lt;a href="#%e4%b8%83%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;七、常见坑与排查清单
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;现象&lt;/th&gt;
					&lt;th&gt;可能原因&lt;/th&gt;
					&lt;th&gt;排查/修复&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;锁永远解不开&lt;/td&gt;
					&lt;td&gt;只 &lt;code&gt;SETNX&lt;/code&gt; 未设过期；或 TTL 极大&lt;/td&gt;
					&lt;td&gt;统一 &lt;code&gt;SET NX PX&lt;/code&gt;；审计历史 key&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;偶发并发进入临界区&lt;/td&gt;
					&lt;td&gt;解锁用了裸 &lt;code&gt;DEL&lt;/code&gt;；或业务超时后无 fencing&lt;/td&gt;
					&lt;td&gt;Lua/&lt;code&gt;DELEX IFEQ&lt;/code&gt;；加 fencing token&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;故障切换后双写&lt;/td&gt;
					&lt;td&gt;依赖异步主从当强一致锁&lt;/td&gt;
					&lt;td&gt;评估是否可接受；否则 Redlock/外部强一致 + fencing&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;加锁抖动高&lt;/td&gt;
					&lt;td&gt;固定重试无退避；热点锁竞争&lt;/td&gt;
					&lt;td&gt;随机退避 + 缩短临界区 + 锁粒度拆分&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;续期把锁续成“永不释放”&lt;/td&gt;
					&lt;td&gt;无限 watchdog&lt;/td&gt;
					&lt;td&gt;上限次数/墙钟最大持锁时间&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;集群下 Lua 访问了未声明的 key&lt;/td&gt;
					&lt;td&gt;违反脚本 key 规则&lt;/td&gt;
					&lt;td&gt;所有 key 经 &lt;code&gt;KEYS&lt;/code&gt; 传入；遵循 Cluster 槽约束&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;以为 &lt;code&gt;GETDEL&lt;/code&gt; 能安全解锁&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;GETDEL&lt;/code&gt; 不校验 owner&lt;/td&gt;
					&lt;td&gt;必须条件删除&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="八选型建议务实版"&gt;&lt;a href="#%e5%85%ab%e9%80%89%e5%9e%8b%e5%bb%ba%e8%ae%ae%e5%8a%a1%e5%ae%9e%e7%89%88" class="header-anchor"&gt;&lt;/a&gt;八、选型建议（务实版）
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单机房、可接受极低概率异常双持、业务幂等完善&lt;/strong&gt;：单 Redis + &lt;code&gt;SET NX PX&lt;/code&gt; + token Lua 解锁通常足够，实现简单、延迟低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对互斥更敏感、可运维多实例独立 Redis&lt;/strong&gt;：评估 Redlock 及成熟库（官方文档列出 Redisson、Redsync、node-redlock 等），并读完 Kleppmann/antirez 讨论。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金融级强一致临界区&lt;/strong&gt;：不要只靠 Redis TTL 语义；优先考虑带 fencing 的共识/事务系统，Redis 锁最多做“快速失败的互斥提示”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任何方案&lt;/strong&gt;：临界区短、幂等、可观测、可回滚，比纠结算法名词更重要。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="九总结"&gt;&lt;a href="#%e4%b9%9d%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;九、总结
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;分布式锁的底线是 &lt;strong&gt;互斥 + 可自动释放 + 安全解锁&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;单实例正确骨架：&lt;code&gt;SET key token NX PX ttl&lt;/code&gt; + &lt;strong&gt;token 条件删除&lt;/strong&gt;（Lua 或 Redis 8.4 &lt;code&gt;DELEX IFEQ&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;裸 &lt;code&gt;DEL&lt;/code&gt;、拆分的 &lt;code&gt;SETNX&lt;/code&gt;+&lt;code&gt;EXPIRE&lt;/code&gt;、固定 value，是三大经典错误。&lt;/li&gt;
&lt;li&gt;异步主从 failover 不能保证互斥；是否可接受取决于业务与是否有 fencing。&lt;/li&gt;
&lt;li&gt;Redlock 用多数派与耗时校验提升容错，但涉及时钟漂移、持久化/延迟重启与社区争议——&lt;strong&gt;官方自己要求你读分析文章&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;工程上把锁当作&lt;strong&gt;辅助互斥&lt;/strong&gt;，用 fencing token、幂等与短临界区兜底正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Redis 官方文档：&lt;em&gt;Distributed Locks with Redis&lt;/em&gt;（含单实例实现、Redlock、续期、disclaimer）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 命令：&lt;code&gt;SET&lt;/code&gt;（&lt;code&gt;NX&lt;/code&gt; / &lt;code&gt;PX&lt;/code&gt; / &lt;code&gt;EX&lt;/code&gt; 与 Patterns 中的加解锁说明）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/commands/set/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/commands/set/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 官方文档：&lt;em&gt;Scripting with Lua&lt;/em&gt;（脚本原子执行、&lt;code&gt;EVAL&lt;/code&gt;/&lt;code&gt;KEYS&lt;/code&gt;/&lt;code&gt;ARGV&lt;/code&gt;）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/develop/programmability/eval-intro/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/develop/programmability/eval-intro/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 命令：&lt;code&gt;GETDEL&lt;/code&gt;（读删一体，但&lt;strong&gt;不&lt;/strong&gt;替代 token 校验解锁）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/commands/getdel/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/commands/getdel/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 文档源码（可检索原文）：distributed-locks markdown&lt;br&gt;
&lt;a class="link" href="https://raw.githubusercontent.com/redis/docs/main/content/develop/clients/patterns/distributed-locks.md" target="_blank" rel="noopener"
 &gt;https://raw.githubusercontent.com/redis/docs/main/content/develop/clients/patterns/distributed-locks.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Martin Kleppmann：&lt;em&gt;How to do distributed locking&lt;/em&gt;（Redlock 分析，官方文末引用）&lt;br&gt;
&lt;a class="link" href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" target="_blank" rel="noopener"
 &gt;https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;antirez：&lt;em&gt;Is Redlock safe?&lt;/em&gt;（对上述分析的回应，官方文末引用）&lt;br&gt;
&lt;a class="link" href="http://antirez.com/news/101" target="_blank" rel="noopener"
 &gt;http://antirez.com/news/101&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>