临时顺序节点、分布式锁与选型
本册对应知识图谱
2.4.5。学习目标不是背会“创建临时顺序节点”,而是能够证明锁的所有权来自哪里、失败时谁可能继续执行、未知创建结果如何收敛,以及为什么真正资源端仍需 Fencing Token(栅栏令牌)、幂等键和状态机。

正式图解释: 图中的节点、会话、监听和业务令牌属于四种不同状态。正常路径是 A 获得最小序号,B 只监听 A,A 会话过期后 B 重新读取并获得协调资格;失败路径是旧 A 恢复后仍尝试写业务数据;最终由权威数据库拒绝低 Fencing Token(栅栏令牌)。因此 Zookeeper(分布式协调服务)锁只提供协调条件,不保证外部副作用恰好一次。
1. 简历关联点与面试主线
- WMS(仓储管理系统)库存正确性首先依赖数据库条件更新、唯一约束和库存流水,不能用远程锁替代业务不变量。
- Runner(执行器)主任务、低频控制面任务和公平排队更适合讨论 Zookeeper(分布式协调服务)锁,但结果提交必须携带业务代次。
- 支付资金正确性依赖支付状态机、渠道流水唯一键、验签、主动查单、对账和差错处理,任何远程锁都只能降低并发冲突。
- Redis(远程字典服务)锁适合低延迟短临界区;数据库锁适合权威数据短事务;Zookeeper(分布式协调服务)锁适合低频、强协调、需要顺序排队的控制面场景。
2. 锁机制、失败模型与业务边界
2.4.5.1 临时节点、顺序后缀、会话与锁所有权
Zookeeper(分布式协调服务)互斥锁通常在资源目录下创建临时顺序节点。临时属性把节点生命周期绑定到服务端会话,顺序属性让服务端为同一父目录下的创建请求追加单调递增后缀。客户端不是“创建成功就持锁”,而是创建后读取子节点、按序号排序,只有自身节点序号最小时才取得当前协调资格。这个资格以“当前会话仍有效、节点仍存在、节点仍是最小序号”为共同前提;本地布尔变量、线程仍运行或 TCP(传输控制协议)连接刚恢复都不能单独证明仍持锁。
flowchart TD
R["资源 /orders/42"] --> D["锁目录 /locks/orders/42"]
D --> A["req-A-00000017 临时顺序节点"]
D --> B["req-B-00000018 临时顺序节点"]
D --> C["req-C-00000019 临时顺序节点"]
A --> O["最小序号且会话有效:当前协调资格"]
B --> W1["只监听前驱 17"]
C --> W2["只监听前驱 18"]
O --> F["业务写仍需令牌、幂等和状态机"]图 1 解释: 节点之间的顺序决定等待关系,箭头不表示业务数据所有权。正常路径由最小节点进入临界区;失败路径是客户端只记住“我曾经最小”而忽略会话已经过期;业务结论是每次外部写还要接受权威存储裁决。
| 要素 | 服务端事实 | 客户端可见信号 | 不能推出的结论 |
|---|---|---|---|
| 临时节点 | 归属于指定会话 | 创建返回真实路径 | 进程永远拥有资源 |
| 顺序后缀 | 同一父目录下递增 | 节点名尾部序号 | 跨父目录全局单调 |
| 最小节点 | 当前排队首位 | 读取子节点后计算 | 外部数据库已授予写权 |
| 会话有效 | 服务端仍保留会话 | 连接状态与心跳事件 | 本地未收到过期事件就一定有效 |
数据演绎 1:所有权的三个同时条件。 输入节点 A=17、B=18、C=19,A 会话 S-A 有效。T0 A 读取列表后确认自己最小;T1 A 的网络断开,本地仍保存 locked=true,但无法证明服务端状态;T2 服务端确认 S-A 过期并删除节点 17;T3 B 收到前驱删除事件,重新读取并确认 18 最小。输出是 B 取得新的协调资格。失败分支若 A 在 T3 恢复后只检查本地变量,会与 B 并发写;验证必须让数据库拒绝 A 的旧业务代次。
热门面试题
问题(基础题):为什么要同时使用临时节点和顺序节点?
- 考点:自动释放与确定排队顺序。
- 回答思路:分别解释生命周期和排队,再说明组合后的持锁判定。
- 详细答案:临时节点在服务端确认所属会话过期后自动删除,解决持有者崩溃而永久占锁的问题;顺序节点提供同一父目录下可比较的递增后缀,客户端能够判断谁排在前面。二者组合后,最小且会话有效的节点获得协调资格,其他节点只等待自己的前驱。单独使用临时节点会让多个参与者都创建成功,单独使用持久顺序节点又会留下需要人工清理的残留。
- 进阶追问:临时节点删除是否意味着旧进程已经停止?
- 进阶回答:不意味着。旧进程可能经历长暂停或网络分区,恢复后仍能调用数据库或第三方,因此真正资源端必须校验业务版本或 Fencing Token(栅栏令牌)。
问题(原理题):为什么“创建节点成功”不等于“加锁成功”?
- 考点:参与排队与取得所有权的区别。
- 回答思路:指出所有客户端都能创建,只有最小序号满足持锁条件。
- 详细答案:创建临时顺序节点只是把客户端加入等待队列。服务端可能已经存在序号更小的节点,因此客户端必须读取同一锁目录、排序并定位自身节点。只有自身最小才进入临界区;否则监听紧邻前驱并等待。收到删除事件后还要重新读取,因为通知可能合并、连接可能重建,不能把一次事件直接解释为锁已经转移给自己。
- 进阶追问:顺序后缀能直接作为业务全局令牌吗?
- 进阶回答:不能直接泛化。后缀只在父目录范围内有顺序语义,目录重建、计数边界和业务长期契约都要考虑;应把成功授权转换为业务资源维度的单调代次并持久化。
问题(项目追问题):WMS(仓储管理系统)库存扣减是否应先拿这种锁?
- 考点:协调锁与权威不变量的选型。
- 回答思路:先判断数据库原子条件能否直接守住库存不负,再评估锁的性能作用。
- 详细答案:库存扣减的硬约束是可用量不能小于零,最直接方案是数据库条件更新或库存预占状态机,例如
available >= quantity才更新,并写唯一预占流水。Zookeeper(分布式协调服务)锁会增加网络往返、会话和热点目录压力,却仍不能阻止旧持有者迟到写。它最多用于低频批量调整或控制面串行,不能成为库存正确性的唯一地基。 - 进阶追问:热点商品数据库冲突很高怎么办?
- 进阶回答:可按商品和仓分片、消息排队、令牌桶削峰或预分配库存段降低冲突,最终仍由数据库条件和流水守恒验收。
2.4.5.2 A/B/C 加锁、前驱监听与公平边界
A、B、C 依次创建 17、18、19。A 最小并进入临界区,B 监听 17,C 监听 18。A 主动删除或因会话过期被删除后,只唤醒 B;B 重新读取列表并确认自己最小后才持锁。这样把“所有等待者监听父目录”的羊群效应缩小为链式唤醒。所谓公平,是排队节点通常按服务端生成的序号推进;它不保证线程调度、网络到达、创建重试或业务完成顺序绝对公平,也不保证等待者永不超时。
sequenceDiagram
participant A as A:节点17
participant B as B:节点18
participant C as C:节点19
participant Z as Zookeeper(分布式协调服务)
A->>Z: 确认17最小并执行
B->>Z: 监听前驱17
C->>Z: 监听前驱18
A->>Z: 删除17或会话过期
Z-->>B: 前驱17删除
B->>Z: 重读列表并确认18最小
B->>Z: 执行后删除18
Z-->>C: 前驱18删除
C->>Z: 重读列表并确认19最小图 2 解释: 链式监听只唤醒直接后继。失败路径包括 B 事件处理前会话过期、B 主动取消或连接重建;此时 C 仍需通过重新读取当前列表发现真实前驱,不能依赖“每个事件必达”。
| 公平维度 | 能提供的性质 | 破坏或模糊因素 | 工程结论 |
|---|---|---|---|
| 排队顺序 | 同一目录已成功创建节点的序号顺序 | 请求到达顺序、创建响应丢失 | 是近似先到先服务,不是调用发起时间绝对公平 |
| 唤醒范围 | 通常只唤醒紧邻后继 | 连接重建、监听遗漏 | 事件后必须重读 |
| 执行顺序 | 最小节点先取得资格 | 线程暂停、业务超时 | 不等于先完成业务 |
| 饥饿控制 | 正常队列推进可降低插队 | 反复取消重建、会话抖动 | 需超时、取消和监控 |
数据演绎 2:链式推进。 T0 子节点为 [17,18,19],B 的前驱是 17,C 的前驱是 18;T1 A 删除 17,只有 B 收到事件;T2 B 重读 [18,19] 后获得资格;T3 C 的事件尚未发生。若 B 在 T2 前会话过期,节点 18 也被删除,C 可能只收到一次或连续事件,但正确动作始终是重读 [19] 并重新计算。输出是状态读取决定资格,事件只负责唤醒。
热门面试题
问题(基础题):为什么等待者只监听前驱节点?
- 考点:避免羊群效应。
- 回答思路:比较监听父目录、监听最小节点和监听直接前驱的唤醒数量。
- 详细答案:若所有等待者都监听父目录或当前持有者,任何释放都会同时唤醒大量客户端,它们一起读取、排序并再次注册,形成突发流量。每个等待者只监听紧邻前驱后,节点删除通常只唤醒一个直接后继,队列沿链推进。这个优化降低读取和线程调度峰值,但前驱事件后仍必须重新读取,因为通知不是业务状态本身。
- 进阶追问:监听前驱能完全消除羊群效应吗?
- 进阶回答:不能。大量客户端同时首次创建、连接重建或父目录异常时仍会形成峰值,需要限流、退避、分层目录和客户端连接治理。
问题(原理题):Zookeeper(分布式协调服务)锁是严格公平锁吗?
- 考点:序号公平与端到端公平的边界。
- 回答思路:说明已创建节点的排序性质,再列出到达、重试、调度和取消因素。
- 详细答案:对同一父目录中已经成功创建的节点,序号给出明确排队次序,因此正常情况下不会让后创建的节点越过更小节点。但调用开始时间不等于请求到达服务端时间,创建响应丢失后的重复节点会改变队列,客户端暂停也会延迟实际执行。因此它提供的是协调队列层的顺序公平,不是业务完成时间、线程 CPU(中央处理器)调度或跨目录全局公平。
- 进阶追问:业务必须严格按订单创建时间执行怎么办?
- 进阶回答:应把业务序号写入权威任务队列,由数据库或 MQ(消息队列)按分区顺序消费;锁序号不能替代业务排序契约。
问题(故障题):收到前驱删除事件后能否直接进入临界区?
- 考点:监听只提供变化提示。
- 回答思路:强调重读、自身节点存在性和会话状态三项复核。
- 详细答案:不能直接进入。事件处理可能延迟,连接期间可能发生多个节点变化,自身会话也可能已经过期。客户端应先确认连接状态,重新读取子节点,找到自己的真实节点并计算是否最小;若自身节点不存在,应按失锁处理而不是重新创建后继续原临界区。只有新的持锁判定通过,才开始新的受保护工作。
- 进阶追问:事件丢失是否会永久阻塞?
- 进阶回答:成熟客户端会在重连、超时或状态变化时重建监听并重读;还应设置有界等待和取消,避免无限挂起。
2.4.5.3 创建响应丢失、未知节点名与幂等清理
最棘手的边界不是创建明确失败,而是服务端已经创建 req-7f3a-00000021,成功响应却在网络中丢失。客户端只知道结果未知,不知道真实顺序后缀;若直接再创建,会得到 req-7f3a-00000022,同一请求出现两个节点。生产实现应在节点名前加入每次加锁尝试唯一且可识别的请求前缀,超时后扫描锁目录,只认属于本会话和该请求标识的节点;发现一个则恢复其真实路径,发现多个则保留最小候选并幂等删除其余节点。扫描期间会话若已过期,全部旧节点都不再代表资格,必须结束本次业务尝试。
flowchart TD
A["创建 req-7f3a- 临时顺序节点"] --> B{"收到明确结果?"}
B -->|成功| C["保存真实路径 21"]
B -->|明确失败| D["按错误类型退避或结束"]
B -->|响应丢失| E["结果未知:扫描父目录"]
E --> F["按请求前缀、会话和节点拥有者匹配"]
F --> G{"匹配数量"}
G -->|0| H["确认会话后决定是否重试创建"]
G -->|1| C
G -->|多个| I["保留最小候选并幂等删除重复节点"]
I --> C图 3 解释: 未知结果不能按失败处理。正常路径从扫描结果恢复真实节点名;失败路径是盲重试制造重复排队节点,甚至让客户端错误监听自己的另一个节点;业务结论是请求标识和清理动作必须可重试。
| 创建结果 | 服务端可能状态 | 客户端动作 | 禁止动作 |
|---|---|---|---|
| 明确成功 | 一个节点已创建 | 保存完整路径并参与排序 | 只保存前缀 |
| 明确业务失败 | 通常未创建 | 按错误结束或重试 | 把权限失败当网络抖动无限重试 |
| 超时或断连 | 已创建或未创建 | 扫描唯一前缀并核对会话 | 直接创建第二个节点 |
| 会话过期 | 旧临时节点会被清理 | 终止旧尝试,建立新会话后新申请 | 沿用旧节点名继续业务 |
数据演绎 3:重复创建与清理。 请求标识为 7f3a。T0 服务端创建节点 21;T1 响应丢失;T2 错误客户端盲重试并创建 22;T3 扫描得到 [req-7f3a-21, req-7f3a-22],两者都属于会话 S-9;T4 保留更小的 21,条件删除 22;T5 重新计算 21 的前驱。失败分支若扫描时 S-9 已过期,则两个节点都不再有效,本次申请必须失败,不能借新会话认领旧节点。
热门面试题
问题(基础题):为什么创建超时不能简单重试?
- 考点:分布式调用未知结果。
- 回答思路:说明服务端提交与客户端收响应是两个时刻。
- 详细答案:请求可能已经被服务端法定提交并创建节点,只是响应在返回途中丢失。客户端看到超时只能判断“我不知道结果”,不能判断“服务端没执行”。直接重试会再创建一个顺序节点,导致重复排队、错误前驱和清理困难。正确方案是使用唯一请求前缀扫描并恢复真实节点路径,或由成熟客户端配方处理保护模式。
- 进阶追问:随机节点名能解决吗?
- 进阶回答:随机性只能降低不同请求重名,不能让客户端从未知结果中找到自己的节点;还要有稳定请求标识、会话归属和扫描规则。
问题(原理题):扫描出多个同前缀节点时为什么保留最小节点?
- 考点:重复申请的排队位置和幂等收敛。
- 回答思路:同一尝试只应有一个队列身份,更小节点代表最早被服务端排序的申请。
- 详细答案:同一请求产生多个节点是重试副作用,业务只应保留一个参与者。最小序号通常对应最早成功的创建,也最接近原申请的排队位置;删除其他重复节点能恢复单一身份。删除必须核对请求前缀、会话拥有者和完整路径,防止误删其他客户端;若无法确认归属,应停止而不是冒险清理。
- 进阶追问:可以使用持久顺序节点避免会话过期吗?
- 进阶回答:不适合普通锁。持久节点不会随会话清理,崩溃后更容易永久阻塞,除非另有可靠租约和回收协议。
问题(线上题):锁目录出现大量残留节点如何处理?
- 考点:残留分类、止血和证据保留。
- 回答思路:先区分临时节点、持久节点、活跃会话和重复前缀,再分批清理。
- 详细答案:先暂停新增申请或限流,采集节点名称、创建时间、临时拥有者、会话状态和客户端版本。临时节点有活跃会话时不能按“看起来很旧”删除;没有临时拥有者的持久节点要追查是否为错误实现。对同请求前缀的重复节点,由拥有者或运维脚本按完整证据分批条件删除,并观察等待队列与业务错误率。清理后修复盲重试逻辑。
- 进阶追问:为什么不能直接删除整个锁目录?
- 进阶回答:会同时释放真实持有者和全部等待者,制造大规模并发进入,还会丢失事故证据;应先隔离流量并按节点归属处理。
2.4.5.4 崩溃、连接抖动、会话过期与旧进程恢复
进程崩溃通常最终导致会话过期和临时节点删除,但网络断开、长时间 Stop-The-World(全停顿)或主机暂停会形成危险窗口:客户端无法与服务端通信,本地业务线程却可能继续;服务端稍后确认会话过期并让新客户端持锁;旧进程恢复后若继续执行,就出现业务双写。连接断开应进入“所有权不确定”状态,暂停新的副作用;会话过期是不可恢复的旧所有权终止,必须放弃旧临界区。重新连上同一集群也不能把新会话当成旧锁续接。
stateDiagram-v2
[*] --> Waiting: 已创建等待节点
Waiting --> Held: 自身最小且会话有效
Held --> Suspended: 连接断开或进程暂停
Suspended --> Held: 原会话恢复且重新验证
Suspended --> Lost: 服务端确认会话过期
Held --> Released: 主动删除自身节点
Lost --> [*]: 放弃旧业务尝试
Released --> [*]
Lost --> Reapply: 新会话发起全新申请
Reapply --> Waiting图 4 解释: Suspended(暂停)不是继续安全执行的依据,也不是立即判定节点已删除;它代表结果不确定。失败路径是从 Lost(已失去)直接跳回 Held(持有);业务结论是新会话只能开始新的业务尝试,不能恢复旧授权。
| 事件 | 锁节点 | 客户端判断 | 业务动作 |
|---|---|---|---|
| 短暂断连 | 可能仍存在 | 所有权不确定 | 暂停新副作用,等待恢复并重验 |
| 进程崩溃 | 暂时仍存在,过期后删除 | 本地不可执行 | 由新持有者接管,靠幂等恢复 |
| 会话明确过期 | 临时节点将被删除或已删除 | 永久失去旧锁 | 中止旧流程,拒绝旧代次 |
| 新会话建立 | 创建新的节点 | 新申请身份 | 不得认领旧会话节点 |
数据演绎 4:长暂停形成双持有者窗口。 T0 A 以节点 31 和令牌 501 持锁;T1 A 发生 40 秒进程暂停,协商会话超时为 20 秒;T2=20s 服务端使 S-A 过期并删除 31;T3=22s B 创建 32,获得令牌 502;T4=40s A 恢复并发送令牌 501 的库存调整。输出应是权威数据库接受 B 的 502,拒绝 A 的 501。若下游只检查“订单还没完成”而不比较代次,旧 A 仍可能覆盖新结果。
热门面试题
问题(基础题):连接断开和会话过期有什么区别?
- 考点:本地网络状态与服务端所有权状态。
- 回答思路:断开可在超时内恢复旧会话,过期则旧会话不可复活。
- 详细答案:连接断开只表示当前连接不可用,服务端在协商超时内可能仍保留会话和临时节点,客户端也可能连接其他服务端继续同一会话。会话过期表示服务端已经终止该会话,临时节点被删除,任何重连都只能建立新会话。断开期间所有权不确定,应暂停副作用;收到过期后必须认定旧锁永久失效。
- 进阶追问:断开期间继续做本地计算可以吗?
- 进阶回答:可做可丢弃、无外部副作用的计算,但提交前必须重新验证会话、业务代次和当前状态;不可继续不可逆写入。
问题(原理题):会话过期自动删节点为什么仍不能保证互斥?
- 考点:协调服务与外部进程不可强制停止。
- 回答思路:服务端只能撤销节点,无法撤回已发送请求或停止旧线程。
- 详细答案:Zookeeper(分布式协调服务)可以决定旧会话不再拥有协调节点,并让后继参与者继续,但它不能杀死因暂停而失联的进程,也不能撤销旧进程已经发给数据库、文件系统或第三方渠道的请求。旧进程恢复后仍可能带着本地状态执行,因此需要真正资源端保存最大 Fencing Token(栅栏令牌)、使用状态条件和幂等键拒绝陈旧副作用。
- 进阶追问:把会话超时调得很长是否更安全?
- 进阶回答:只会减少误过期但延长崩溃后的阻塞和恢复时间,无法消除旧请求;应按网络与暂停分布设置并保留业务栅栏。
问题(故障题):客户端恢复后发现会话已过期,应该怎么做?
- 考点:失锁状态机与补偿。
- 回答思路:停止、查证、放弃旧授权,再决定是否新申请。
- 详细答案:立即把本地锁状态改为失效,停止领取新任务和可取消步骤;对已经发出的外部调用按幂等键查询真实结果,不能凭超时重做。旧业务提交必须携带原代次并接受权威端拒绝。只有确认旧尝试已结束或能够安全幂等接续后,才用新会话、新请求标识和新代次重新申请,并记录一次接管事件用于审计。
- 进阶追问:释放旧节点失败要不要重试?
- 进阶回答:会话已过期时旧临时节点由服务端清理,无需用新会话删除;若状态不确定,先查归属,禁止删除名称相似但属于其他会话的节点。
2.4.5.5 Fencing Token(栅栏令牌)与外部副作用裁决
Fencing Token(栅栏令牌)是每次成功授权时递增的资源维度代次,不是随机持有者标识。持有者把令牌随每次业务写传给真正资源端,资源端在同一原子操作中比较 incoming_token >= stored_token 并推进最大值;低代次迟到写被拒绝。随机请求标识解决“这是哪个调用”和幂等去重,Fencing Token(栅栏令牌)解决“谁更新”;两者不能互相替代。令牌必须有明确生成和持久化边界,不能把可能重建的节点名字符串直接当永久资金协议。
sequenceDiagram
participant A as 旧持有者A:token=701
participant B as 新持有者B:token=702
participant DB as 权威数据库
B->>DB: UPDATE ... WHERE token < 702
DB-->>B: 成功,保存702
A->>DB: 迟到写 token=701
DB-->>A: 拒绝,701小于702
A->>A: 转入查证或补偿,不覆盖新状态图 5 解释: 栅栏发生在资源端而不是锁客户端。正常路径推进最大令牌;失败路径是只在应用内比较、随后仍无条件写数据库,检查与写之间会再次竞态。
| 机制 | 解决的问题 | 需要保存的值 | 无法独立解决 |
|---|---|---|---|
| 随机请求标识 | 重试去重、定位调用 | 唯一业务键 | 新旧持有者顺序 |
| 持有者标识 | 防止误删他人节点 | 会话和完整节点路径 | 旧持有者外部写 |
| Fencing Token(栅栏令牌) | 拒绝低代次迟到写 | 资源维度最大代次 | 同代次重复请求 |
| 业务状态机 | 约束合法状态迁移 | 当前状态和版本 | 选举与排队 |
数据演绎 5:令牌和幂等同时工作。 任务 export-88 的 A 获得 token=701,请求键为 export-88:publish;A 失锁后 B 获得 702,先发布结果并保存最大令牌 702。A 恢复后重复提交原请求:幂等表能识别同一请求,令牌比较还能在请求标识错误或不同的情况下拒绝 701。若 B 因响应丢失重试相同请求,幂等键返回既有结果而不重复发布。输出是一个有效正式指针,旧产物进入清理。
热门面试题
问题(基础题):Fencing Token(栅栏令牌)和随机锁令牌有什么区别?
- 考点:唯一性与单调性的不同职责。
- 回答思路:随机令牌识别所有者,栅栏令牌表达授权新旧。
- 详细答案:随机令牌只需高概率唯一,常用于释放时确认删除的是自己的锁;不同随机值没有大小关系。Fencing Token(栅栏令牌)必须对同一资源单调递增,资源端保存已经接受的最大值,并拒绝更小值。前者防误删,后者防旧持有者迟到写;关键系统通常还要同时使用幂等请求键和状态机。
- 进阶追问:可以用顺序节点后缀当令牌吗?
- 进阶回答:可作为同一稳定父目录下的候选来源,但要评估目录生命周期和业务契约,通常转换并持久化为业务代次更清晰。
问题(原理题):为什么令牌校验必须在真正资源端原子执行?
- 考点:检查与使用之间的竞态。
- 回答思路:应用先查后写会留下时间窗口,资源端条件更新才能闭合。
- 详细答案:若应用先查询当前最大令牌,判断自己较新,再执行无条件写,两个持有者可能都在检查时通过,之后旧请求覆盖新请求。资源端必须把“比较令牌、验证业务状态、写入结果、推进最大令牌”放在同一事务或原子条件更新中。第三方系统不支持令牌时,应通过自有提交代理、唯一键、查询确认和补偿缩小风险,不能宣称已经实现栅栏。
- 进阶追问:对象存储没有条件更新怎么办?
- 进阶回答:先上传带代次的临时对象,正式对象指针在可条件更新的数据库中发布;低代次只能留下可清理的孤儿对象。
问题(项目追问题):支付回调处理需要 Fencing Token(栅栏令牌)吗?
- 考点:资金状态机优先于锁令牌。
- 回答思路:以渠道流水唯一键和合法状态迁移为主,令牌只用于特定调度所有权。
- 详细答案:支付回调的核心是验签、渠道交易号唯一、商户订单映射、金额币种校验和状态机条件更新;重复回调应返回同一处理结果。若有主动查单任务与回调并发,二者都依据渠道事实推进同一状态,而不是谁拿锁谁覆盖。Fencing Token(栅栏令牌)可保护对账批次或主任务所有权,但不能替代支付流水与状态机。
- 进阶追问:锁住订单号是否就不会重复入账?
- 进阶回答:不能。锁可能失效或重复执行,真正防重入账的是账务流水唯一键、原子状态迁移和对账。
2.4.5.6 可重入锁、线程所有权与 Apache Curator(Apache 协调客户端框架)
可重入表示同一逻辑所有者已经持锁时再次获取不会自我阻塞,而是增加本地重入计数;释放次数与获取次数相等时才真正删除远端节点。难点在“同一所有者”的定义:通常要同时绑定进程内锁对象、线程和当前会话,不能让同进程另一线程借用。Apache Curator(Apache 协调客户端框架)的 InterProcessMutex 提供成熟的可重入跨进程互斥配方,处理节点创建、前驱等待和连接状态;但它仍不能消除会话过期、旧线程恢复和外部副作用未知结果,业务必须在连接丢失与过期回调中停止或围栏写入。
stateDiagram-v2
[*] --> None: 未持锁
None --> Count1: 线程T首次获得远端节点
Count1 --> Count2: 同线程再次获取,计数加一
Count2 --> Count1: 第一次释放,仅计数减一
Count1 --> None: 最后一次释放,删除远端节点
Count1 --> Lost: 会话过期
Count2 --> Lost: 会话过期
Lost --> [*]: 计数作废并拒绝继续释放或写入图 6 解释: 重入计数是本地优化,远端会话过期会让全部本地计数立即失去授权意义。失败路径是计数仍大于零就继续业务;业务结论是连接状态必须能使整个持锁上下文失效。
| 维度 | 手写配方风险 | Apache Curator(Apache 协调客户端框架)价值 | 仍需业务处理 |
|---|---|---|---|
| 可重入 | 线程身份、计数和异常释放易错 | 封装重入和释放路径 | 失锁后中止业务 |
| 节点保护 | 创建结果未知易产生重复节点 | 提供保护创建思路与成熟实现 | 监控残留和版本兼容 |
| 等待 | 监听竞态与取消复杂 | 封装前驱等待与超时 | 设定截止时间和退避 |
| 外部副作用 | 无法撤回旧请求 | 不负责业务事务 | 幂等、状态机、栅栏和对账 |
数据演绎 6:重入计数失效。 线程 T-9 第一次获取后计数为 1,第二次调用后计数为 2;完成内层操作释放一次,计数回到 1,远端节点仍存在。随后会话过期,远端节点被删除,本地计数即使仍为 1 也必须清零并把上下文标记为 LOST。失败分支若外层 finally(最终清理)继续以旧上下文提交,会产生陈旧写;正确做法是释放动作容忍节点已不存在,而提交由业务代次拒绝。
热门面试题
问题(基础题):什么是分布式可重入锁?
- 考点:逻辑所有者与重入计数。
- 回答思路:同线程重复获取只增加计数,最后一次释放才删除节点。
- 详细答案:分布式可重入锁允许同一线程在同一锁实例和有效会话上下文中重复获取,而不会排队等待自己的远端节点。第一次获取创建并取得节点,后续获取只增加本地计数;释放逐次减一,计数归零才删除远端节点。它解决嵌套方法调用的自锁问题,但会话过期会让所有计数同时失效,不能靠本地计数恢复授权。
- 进阶追问:同进程不同线程能否重入?
- 进阶回答:通常不能,所有权应包含线程身份;若业务需要跨线程传递,应显式设计任务所有权而不是偷偷共享锁对象。
问题(原理题):为什么优先用 Apache Curator(Apache 协调客户端框架)而不是手写?
- 考点:配方中的竞态和连接状态复杂度。
- 回答思路:列出创建未知、监听竞态、取消、重入和连接恢复。
- 详细答案:一个看似十几行的锁算法要处理创建响应丢失、节点重复、监听注册与删除并发、连接重建、会话过期、超时取消、异常释放和线程中断。Apache Curator(Apache 协调客户端框架)经过长期使用,提供互斥、读写、信号量和选主等配方,并统一重试和连接状态。使用成熟配方能减少协议错误,但仍需锁定版本、理解状态回调并完成业务围栏。
- 进阶追问:使用成熟框架是否就不需要读原理?
- 进阶回答:仍需理解,否则无法正确处理
SUSPENDED(连接暂停)与LOST(会话失效)、设置超时或解释线上重复执行。
问题(故障题):可重入锁释放次数少一次会怎样?
- 考点:本地计数泄漏与远端占锁。
- 回答思路:计数不归零,节点持续存在直到会话结束。
- 详细答案:少释放一次会让本地重入计数保持大于零,远端节点不会主动删除,其他客户端持续等待;如果进程长期存活,会形成逻辑死锁。应保证每次成功获取都在同一控制流中
finally(最终清理)释放,记录持锁年龄、重入深度和等待时长,并设置业务截止时间。强制运维删除前必须核对会话和业务副作用状态。 - 进阶追问:多释放一次呢?
- 进阶回答:成熟实现通常抛出所有权异常;业务不能吞掉异常继续运行,因为这说明调用配对或线程上下文已经破坏。
2.4.5.7 读写锁、共享锁、信号量与容量配方
互斥锁只允许一个持有者;读写锁允许多个读者并发,但写者必须排在所有先前读写节点之后,并阻止后续读者越过;共享锁或信号量允许最多 N 个租约同时存在。它们都建立在顺序节点和会话上,区别是资格判定函数不同。Apache Curator(Apache 协调客户端框架)的 InterProcessReadWriteLock、InterProcessSemaphoreV2 等成熟配方更适合生产。公平读写锁仍需考虑写者饥饿、升级死锁、许可泄漏和会话过期,不能把“最多 N 个参与者”当成下游容量永远安全。
flowchart TD
Q["顺序队列"] --> R1["R-41 读"]
Q --> R2["R-42 读"]
Q --> W["W-43 写"]
Q --> R3["R-44 读"]
R1 --> A["41与42可并发读"]
R2 --> A
W --> B["等待41与42释放后独占"]
R3 --> C["不能越过已排队写者43"]
S["信号量 permits=3"] --> P["最小三个租约获得许可"]图 7 解释: 读者并发不代表后续读者可以绕过写者,否则写者可能永久饥饿。信号量按最小若干节点授予资格;失败路径是下游真实容量下降而许可数未调整,协调层仍会放入过多请求。
| 配方 | 资格规则 | 典型场景 | 主要风险 |
|---|---|---|---|
| 互斥锁 | 自身为最小节点 | 单主控制操作 | 旧持有者和长临界区 |
| 读写锁 | 读可共享,写按序独占 | 低频配置发布 | 写饥饿、升级死锁 |
| 共享锁 | 前 K 个节点可进入 | 固定并发批处理 | K 与真实容量漂移 |
| 信号量 | 成功持有许可租约 | 第三方接口并发控制 | 许可泄漏、限额非全局业务事实 |
数据演绎 7:读写顺序与信号量。 队列为 R41,R42,W43,R44。T0 R41 和 R42 同时获得读资格;W43 等待两个前驱释放;R44 虽是读者也必须等待 W43,避免写者饥饿。另一个信号量目录许可数为 3,节点 51,52,53 获得许可,54 等待 51。若 52 会话过期,其许可释放,54 重读后进入。输出是资格由当前队列计算;下游若把并发上限从 3 降为 1,还需配置治理和应用限流同步收敛。
热门面试题
问题(基础题):读写锁如何判断读者和写者能否进入?
- 考点:顺序队列中的不同资格函数。
- 回答思路:读者查看前面是否有写者,写者要求自己是最早未完成节点。
- 详细答案:读节点可以与排在自己之前且仍有效的读节点并发,但如果前面存在写节点就必须等待该写节点;写节点需要等待所有序号更小的读写节点释放。实现通常让读者监听前面最近的写节点,写者监听直接前驱,从而减少通知。收到事件后仍要重读和重算资格。
- 进阶追问:读锁能否直接升级为写锁?
- 进阶回答:多个读者同时升级会互相等待,容易死锁;应释放读锁后按新顺序申请写锁,并用业务版本防止间隙状态变化。
问题(原理题):分布式信号量是否等同于限流器?
- 考点:并发数与速率的区别。
- 回答思路:信号量控制同时占用数量,限流器控制时间窗口内速率。
- 详细答案:信号量通过最多 N 个有效租约限制并发中的任务数,任务完成或会话过期后释放许可;它不直接限制每秒请求数,也无法表达突发桶、平滑速率和调用成本差异。第三方渠道保护通常同时需要本地速率限流、超时、熔断和分布式并发许可,且许可获取失败要快速降级而非无限排队。
- 进阶追问:许可数越大吞吐越高吗?
- 进阶回答:不一定,超过数据库连接、线程或第三方容量后会增加排队和超时;应根据端到端延迟和错误率闭环调整。
问题(项目追问题):异步导出适合哪种配方?
- 考点:任务幂等、并发容量与主任务协调。
- 回答思路:信号量控制重任务并发,任务表和检查点保证正确性。
- 详细答案:异步导出可用信号量限制同一租户或节点上的重任务并发,避免 OOM(内存溢出)和磁盘竞争;若需要唯一合并者,再用领导者配方选出汇总角色。但任务状态、分片检查点、对象存储临时文件和最终指针仍由数据库持久化,重复执行按任务键幂等,旧持有者发布由代次拒绝。协调配方只治理容量和角色。
- 进阶追问:会话过期会丢失已经生成的分片吗?
- 进阶回答:不应丢失,分片结果和校验摘要要持久化;新执行者从检查点接续,孤儿文件由生命周期任务清理。
2.4.5.8 领导者锁、选主配方与任务所有权
领导者选举可以视为长期持有的协调资格:参与者创建临时顺序节点,最小节点成为当前 Leader(领导者),后继等待前驱。Apache Curator(Apache 协调客户端框架)的 LeaderLatch 或 LeaderSelector 封装参与、失去领导权和重新排队。领导者身份适合决定“谁负责扫描、分片或发布配置”,不适合直接证明任务只执行一次。每次领导权必须映射到单调 leader_epoch,任务领取和结果提交都比较该代次;断连时停止领取,明确会话失效时主动退位。
sequenceDiagram
participant A as Runner(执行器)A epoch=81
participant Z as Zookeeper(分布式协调服务)
participant B as Runner(执行器)B epoch=82
participant T as 任务表
A->>Z: 节点最小,成为Leader(领导者)
A->>T: 领取任务,写epoch=81
Z-->>A: 会话失效
Z-->>B: 前驱删除,B成为Leader(领导者)
B->>T: 条件接管,写epoch=82
A->>T: 旧结果提交epoch=81
T-->>A: 拒绝低代次图 8 解释: 选主决定当前协调者,任务表决定哪个结果有效。失败路径是 A 已失去领导权却继续运行;业务结论是每次任务状态迁移都必须比较任务版本和领导代次。
| 领导者事件 | 协调动作 | 任务动作 | 观测指标 |
|---|---|---|---|
| 获得领导权 | 记录新代次 | 开始领取新任务 | 选主耗时、代次 |
| 连接暂停 | 暂停受保护工作 | 停止领取,保存检查点 | 暂停时长、在途任务 |
| 会话失效 | 永久退位 | 旧代次提交应被拒绝 | 失租次数、围栏拒绝 |
| 新领导者接管 | 重建队列状态 | 扫描超时任务并条件接管 | 接管数、任务年龄 |
数据演绎 8:主任务接管。 A 的 leader_epoch=81,任务 J7 状态为 RUNNING/A/81/version=5。A 暂停后会话过期,B 获得 82,通过条件 version=5 AND epoch=81 AND lease_expired 把任务更新为 RUNNING/B/82/version=6。A 恢复并以 81/version=5 提交,影响行数为 0;B 以 82/version=6 完成。输出只有 B 的正式结果生效,A 的临时产物可追踪清理。
热门面试题
问题(基础题):分布式锁和领导者选举有什么关系?
- 考点:短临界区与长期角色资格。
- 回答思路:都可由临时顺序节点构建,但生命周期和业务动作不同。
- 详细答案:两者都可以让最小临时顺序节点获得资格,后继监听前驱。互斥锁通常保护有界临界区,执行后主动释放;领导者资格持续更久,用于决定哪个实例负责调度、扫描或协调。长期资格更容易遇到连接暂停、会话过期和旧领导者恢复,因此必须设计明确退位回调、业务代次和任务接管状态机。
- 进阶追问:领导者能把任务都放在内存吗?
- 进阶回答:不能,切主会丢失内存状态;任务、检查点和结果必须在权威存储中可恢复。
问题(原理题):为什么选主不能保证任务恰好一次?
- 考点:领导权切换与在途副作用。
- 回答思路:旧领导者可能已发送请求但未知结果,新领导者必须重试。
- 详细答案:A 在失去会话前可能已经调用下游,响应却丢失;B 接管后看到任务未完成会再次调用。反过来,A 长暂停后恢复也可能继续提交。协调服务只能确定当前谁满足领导条件,不能撤回外部请求。任务必须有稳定业务键、幂等步骤、超时查证、检查点和 Fencing Token(栅栏令牌),以“允许重复尝试但只接受一个合法结果”实现可靠性。
- 进阶追问:让任务执行时间小于会话超时可以吗?
- 进阶回答:只能降低概率,尾延迟和暂停不可完全界定;正确性不能建立在平均耗时上。
问题(项目追问题):Runner(执行器)如何优雅退位?
- 考点:停止领取、在途处置和节点释放顺序。
- 回答思路:先关闭入口,保存状态,再释放资格。
- 详细答案:收到发布下线或连接暂停信号后,先停止领取新任务并标记实例排空;可中断任务写检查点后退出,不可中断外部调用记录幂等键并转入查证。等待有限时间后释放领导节点,未完成任务由租约超时和条件接管恢复。不能先删除节点再继续领取,否则新旧领导者会并发派发;也不能无限等待导致发布卡死。
- 进阶追问:强制终止时如何恢复?
- 进阶回答:依靠持久任务状态、租约、代次和检查点扫描,不依赖旧进程执行清理回调。
2.4.5.9 Zookeeper(分布式协调服务)、Redis(远程字典服务)与数据库锁选型
三类锁的关键差异不在 API(应用程序接口)写法,而在权威资源和失败语义。数据库锁与业务数据处在同一事务引擎,最适合短事务内守住行级不变量;Redis(远程字典服务)锁延迟低、吞吐高,常用租约与续期保护短临界区,但复制切换和暂停窗口必须评估;Zookeeper(分布式协调服务)锁依靠会话、临时顺序节点和法定历史,适合低频控制面、公平排队与选主,代价是更高延迟、目录热点和运维复杂度。三者都不能单独保证外部副作用恰好一次。
flowchart LR
Q{"权威不变量在哪里?"} -->|"数据库一行或少量行"| DB["条件更新或短事务锁"]
Q -->|"低延迟短临界区"| R["评估 Redis(远程字典服务)锁"]
Q -->|"低频公平协调或选主"| Z["评估 Zookeeper(分布式协调服务)锁"]
DB --> B["幂等、状态机和流水"]
R --> B
Z --> B
B --> F["必要时使用 Fencing Token(栅栏令牌)"]图 9 解释: 选型从权威不变量和业务失败预算出发,而不是从团队熟悉的中间件出发。失败路径是给资金、库存全部套远程锁,却没有数据库条件与对账。
| 维度 | 数据库锁/条件更新 | Redis(远程字典服务)锁 | Zookeeper(分布式协调服务)锁 |
|---|---|---|---|
| 权威资源 | 数据库行、索引和事务 | 缓存键 | 会话与数据节点 |
| 所有权依据 | 事务和锁管理器 | 令牌值与租约 | 会话、临时顺序节点、最小序号 |
| 自动释放 | 事务结束 | 到期或比较删除 | 会话过期或主动删除 |
| 排队公平 | 通常不作为稳定契约 | 普通实现不保证 | 顺序队列提供较清晰公平性 |
| 故障切换 | 依赖数据库复制与事务恢复 | 异步复制可能丢锁 | 法定历史保护协调状态 |
| 旧持有者 | 事务结束后外部调用仍可能迟到 | 租约过期后可能继续 | 会话过期后可能继续 |
| 延迟/吞吐 | 延迟中等,受事务竞争影响 | 通常低延迟高吞吐 | 写入和监听成本较高 |
| 锁等待/死锁 | 有锁等待和事务死锁 | 需客户端超时与顺序治理 | 有队列等待和会话阻塞 |
| 运维 | 复用业务数据库但要控长事务 | 管理复制、内存和热键 | 管理法定人数、会话、目录和监听 |
| 典型场景 | 库存条件扣减、状态迁移 | 缓存重建、短期去重协调 | 控制面选主、公平任务协调 |
数据演绎 9:相同库存问题的三种实现。 商品可用量为 5,订单 A 要 4、B 要 3。数据库方案用 available >= qty 条件更新,A 成功后余 1,B 影响 0 行;Redis(远程字典服务)锁可让 A/B 串行,但若 A 失租后迟到写,仍要数据库条件拒绝;Zookeeper(分布式协调服务)锁按节点 61/62 排队,A 会话过期后 B 接管,同样不能跳过库存条件。输出是三种协调方式都应以数据库不负数为验收,远程锁只改变竞争路径。
热门面试题
问题(基础题):三类锁如何快速选型?
- 考点:权威资源、临界区和失败预算。
- 回答思路:数据库不变量优先数据库,短缓存协调评估 Redis(远程字典服务),公平控制面评估 Zookeeper(分布式协调服务)。
- 详细答案:先问不加分布式锁能否用唯一键、条件更新或分区串行守住正确性。业务数据就在数据库且临界区很短时,数据库方案边界最清楚;缓存重建等低延迟短操作可评估 Redis(远程字典服务)锁;低频主任务、公平排队和控制面协调可评估 Zookeeper(分布式协调服务)。最后统一补齐超时、取消、幂等、栅栏、监控与降级。
- 进阶追问:哪一种锁最安全?
- 进阶回答:没有脱离业务资源的绝对答案。安全性取决于权威端能否拒绝非法状态和旧持有者,而不是中间件名字。
问题(原理题):Zookeeper(分布式协调服务)锁为何通常比 Redis(远程字典服务)锁慢?
- 考点:法定复制、节点创建和监听交互。
- 回答思路:比较一次内存命令与协调写、复制、排序和通知。
- 详细答案:Zookeeper(分布式协调服务)创建节点属于有序写,需要通过领导者和法定副本形成可恢复历史,客户端还要读取子节点、注册前驱监听并处理会话。Redis(远程字典服务)普通锁通常是单键原子命令和租约,路径更短。前者用更高协调成本换取会话和顺序队列语义,不能用在每个高频请求上而不做容量评估。
- 进阶追问:慢是否意味着不能用于生产?
- 进阶回答:不是。低频控制面更关注正确协调和可观察顺序,吞吐不是第一目标;要通过压测验证等待和故障恢复。
问题(项目追问题):缓存重建锁应该怎么选?
- 考点:效率锁和正确性锁的区别。
- 回答思路:缓存重建重复通常只浪费资源,可选低延迟方案并允许降级。
- 详细答案:同一热点键的缓存重建通常是效率问题,多个实例重复加载会冲击数据库但不应破坏业务权威数据。可用 Redis(远程字典服务)短租约锁、单飞请求或逻辑过期,等待者读取旧值并随机退避;重建失败允许降级和重试。没有必要为每次缓存未命中创建 Zookeeper(分布式协调服务)节点,除非重建属于极低频且要求严格顺序的控制任务。
- 进阶追问:锁服务不可用时怎么办?
- 进阶回答:限流回源、使用旧缓存、请求合并或直接失败,不能让所有实例无保护击穿数据库。
2.4.5.10 WMS(仓储管理系统)库存、履约与跨境物流选型
WMS(仓储管理系统)库存要区分可售、预占、拣货、扣减、释放和盘点修正,真正不变量是数量守恒、状态迁移合法和每条业务流水唯一。高频下单不应让请求在 Zookeeper(分布式协调服务)热点目录排队,而应由数据库条件更新或按仓库与商品分区串行。Zookeeper(分布式协调服务)更适合低频盘点批次主控、仓库波次发布或全局任务选主;跨境物流面单和轨迹同步面对第三方未知结果,应使用稳定请求号、主动查询和状态机,而不是长时间持锁等待网络。
flowchart TD
O["订单请求"] --> I["库存预占唯一流水"]
I --> C{"数据库条件 available >= qty"}
C -->|成功| S["预占状态成功并发布事件"]
C -->|失败| F["返回库存不足"]
P["低频盘点或波次任务"] --> Z["Zookeeper(分布式协调服务)选主或公平协调"]
Z --> E["携带任务代次写检查点"]
L["物流第三方调用"] --> U["结果未知时按请求号查证"]
S --> V["拣货、扣减、释放按状态机推进"]图 10 解释: 高频库存硬约束与低频控制任务走不同路径。失败路径是拿到远程锁后无条件扣减,或在第三方调用期间长时间占锁;业务结论是锁只协调角色,库存和履约仍以流水与状态机收敛。
| 场景 | 推荐主方案 | 锁的作用 | 必要兜底 |
|---|---|---|---|
| 下单预占库存 | 数据库条件更新、唯一预占流水 | 通常不需要远程锁 | 释放幂等、守恒对账 |
| 波次发布 | 任务版本和状态机 | 可用公平协调避免同时发布 | Fencing Token(栅栏令牌)、回滚版本 |
| 盘点批次 | 批次状态和仓库范围隔离 | 可选主控实例 | 人工复核、差异流水 |
| 面单购买 | 稳定请求号和供应商查单 | 仅防本地并发提交 | 未知结果查证、费用对账 |
| 轨迹拉取 | 分片调度和游标 | 控制主任务或分片归属 | 去重、乱序合并、补拉 |
热门面试题
问题(基础题):库存防超卖为什么优先条件更新?
- 考点:不变量与权威存储同源。
- 回答思路:把检查和扣减放在同一数据库原子语句中。
- 详细答案:条件更新把“可用量足够”和“扣减数量”放在同一原子操作中,影响行数直接表示成功或失败,权威库存不会因远程锁过期而无条件变负。再配合订单与预占唯一键、状态机和库存流水,重试也不会重复扣减。远程锁可以降低热点竞争,但不能替代这条数据库底线。
- 进阶追问:多仓分配需要锁所有仓吗?
- 进阶回答:优先按仓逐项预占并用补偿释放,或先计算候选再条件提交;跨仓大锁会放大等待和死锁。
问题(原理题):为什么第三方物流调用不应持有长锁?
- 考点:不可控尾延迟和未知结果。
- 回答思路:网络调用可能超时但已成功,长锁既阻塞又不能解决重复。
- 详细答案:面单供应商响应时间和可用性不受本系统控制,超时可能是请求未到、已处理但响应丢失或仍在执行。持有远程锁等待只会让队列和会话压力上升,锁失效后旧调用仍可能成功。应先写本地请求状态和稳定请求号,调用后成功则推进,超时进入未知并主动查单,重复回调按供应商单号和请求号幂等。
- 进阶追问:供应商不支持幂等键怎么办?
- 进阶回答:缩小自动重试,按业务字段查单,建立人工差错队列和账单对账,不能靠锁伪造供应商幂等。
问题(项目追问题):波次发布为何可以考虑 Zookeeper(分布式协调服务)锁?
- 考点:低频控制面与公平排队。
- 回答思路:发布频率低、竞争者少、需要明确顺序,但仍以任务版本为权威。
- 详细答案:仓库波次发布通常是低频控制操作,同一仓和批次不希望多个调度器同时生成任务,顺序等待和会话失效有价值。可以用成熟配方协调发布者,但发布任务要写唯一批次号、版本和状态,生成拣货任务使用唯一约束;失锁后旧发布者的低代次写被拒绝。这样协调故障只影响发布进度,不破坏仓内任务一致性。
- 进阶追问:如何降级?
- 进阶回答:锁服务异常时暂停自动发布、保留人工审核和单仓限流入口,恢复后按批次状态扫描,不允许无锁并发发布。
2.4.5.11 支付资金正确性、排障与设计红线
支付创建、回调、主动查单、退款和对账都可能并发,但资金正确性不能建立在 Zookeeper(分布式协调服务)、Redis(远程字典服务)或数据库悲观锁的单点假设上。权威模型应包含商户请求号、渠道交易号、金额币种、支付状态机、账务流水唯一键和对账差异。远程锁可以减少同一订单的并发处理或选举对账主任务,却不能证明渠道只扣一次、回调只到一次或旧进程已经停止。排障时先冻结自动补偿,按渠道事实、内部订单、账务和锁代次建立证据链。
flowchart TD
A["支付回调或主动查单"] --> V["验签、金额币种与商户校验"]
V --> K["渠道流水唯一键"]
K --> S["状态机条件迁移"]
S --> L["账务流水原子落账"]
L --> R["返回幂等结果"]
Z["远程锁或对账主任务选举"] -. "只减少并发或决定角色" .-> A
E["异常:未知、重复、旧代次"] --> Q["查单、对账、补偿或人工差错"]
Q --> S图 11 解释: 虚线表示锁不是资金事实链的一部分。正常路径由渠道事实和状态机推进;失败路径进入查证与对账,禁止因拿到锁就强行改成成功或失败。
| 红线 | 错误做法 | 正确边界 | 关键证据 |
|---|---|---|---|
| 重复入账 | 只锁订单号 | 账务流水唯一键和状态机 | 渠道号、账务流水号 |
| 未知结果 | 超时后直接重发扣款 | 主动查单或稳定请求号重试 | 请求日志、渠道查询结果 |
| 旧持有者 | 认为会话过期后线程已停 | 权威端拒绝低代次 | 最大令牌、拒绝日志 |
| 对账任务 | 只允许一个进程运行 | 选主加批次唯一、检查点和代次 | 批次号、文件摘要、差异单 |
| 人工修复 | 直接改支付状态 | 生成可审计修正流水 | 审批、原始差异、修正记录 |
热门面试题
问题(基础题):为什么支付不能只靠分布式锁防重?
- 考点:外部渠道、未知结果和持久幂等。
- 回答思路:锁可能失效且渠道调用不可撤回,必须让流水唯一和状态机落地。
- 详细答案:锁只能在一段时间内减少本系统并发处理者,进程暂停、会话过期或故障切换后仍可能出现旧请求;渠道也可能已扣款但响应丢失。账务防重必须依赖渠道交易号或稳定请求号唯一约束,订单状态只能按合法前置状态迁移,重复回调返回既有结果。最后通过渠道账单和内部流水对账发现遗漏或差错。
- 进阶追问:数据库行锁能否完全解决?
- 进阶回答:行锁只能保护本地事务,无法覆盖事务外渠道调用和响应丢失;应避免持锁调用第三方并使用状态机查证。
问题(原理题):锁只提供协调条件是什么意思?
- 考点:协调层与业务权威层分离。
- 回答思路:锁能选出当前资格,不能证明外部副作用恰好一次。
- 详细答案:协调条件回答“根据锁服务当前可见状态,哪个参与者可以继续”,不回答“数据库、支付渠道或对象存储最终接受了几次操作”。外部请求可能在失锁前已发出,旧进程也可能在失锁后恢复。因此每个资源端都要有可验证的不变量:数据库条件、唯一键、状态机、Fencing Token(栅栏令牌)、查询确认和对账。
- 进阶追问:那分布式锁还有什么价值?
- 进阶回答:它能减少竞争、串行低频控制操作、组织公平等待和选主,从而简化流程与降低资源压力,但不承担终局事实。
问题(事故题):发现新旧两个对账任务都在写结果,如何处理?
- 考点:止血、证据、围栏和重算。
- 回答思路:先停止低代次写,再核对批次、渠道文件和内部差异。
- 详细答案:立即暂停自动发布和补偿,保留两个实例的会话、节点路径、任务代次、文件摘要与写入日志;由数据库条件拒绝低代次后,确认当前合法批次所有者。按渠道原始账单、内部支付流水和已生成差异单重算,重复差异使用批次加业务键唯一约束合并,任何资金修正生成新流水而非覆盖原记录。最后注入会话过期复现实验。
- 进阶追问:能直接删除旧任务节点吗?
- 进阶回答:删除节点只能停止后续协调,不能撤回已写结果;必须先围栏业务写并保存证据,再清理节点。
知识节收口
以上 11 个知识小节形成一条完整因果链:临时顺序节点负责组织资格,前驱监听负责低成本推进,会话负责回收失联参与者,Fencing Token(栅栏令牌)和业务权威存储负责拒绝旧持有者。任何一层缺失,都不能把锁描述为生产级正确性方案。
3. 项目落地话术
“我不会把分布式锁描述成资金或库存正确性的最后防线。WMS(仓储管理系统)下单预占用数据库条件更新和唯一流水守住库存不负,Zookeeper(分布式协调服务)只用于低频波次、盘点或 Runner(执行器)选主。锁实现优先采用 Apache Curator(Apache 协调客户端框架)的成熟配方,等待者只监听前驱;创建结果未知时按唯一请求前缀扫描并清理重复节点。会话断开立即暂停副作用,会话过期永久放弃旧授权;所有任务结果携带 Fencing Token(栅栏令牌),权威数据库拒绝低代次。支付则以渠道流水唯一键、状态机、主动查单和对账为核心,锁只降低并发,不宣称恰好一次。”
4. 高频综合面试题与追问
问题(综合题):请完整讲解 Zookeeper(分布式协调服务)分布式锁如何工作。
- 考点:临时顺序节点、前驱监听、会话和业务边界。
- 回答思路:按加入队列、判断资格、等待、释放、失效与业务栅栏回答。
- 口述答案:结论是,Zookeeper(分布式协调服务)锁用临时顺序节点建立一个可恢复的协调队列,但真正安全的业务系统还必须有幂等、状态机和 Fencing Token(栅栏令牌)。客户端先在资源对应目录创建带唯一请求前缀的临时顺序节点,保存服务端返回的完整路径,然后读取同目录子节点并按顺序后缀排序。自身最小时才取得当前协调资格;否则找到紧邻前驱,只监听该节点,避免所有等待者监听父目录造成羊群效应。前驱删除后不能直接进入临界区,而要重新读取、确认自身节点仍存在、会话仍有效且当前确实最小。正常释放只删除自己的完整节点;进程崩溃后,服务端确认会话过期会删除临时节点并推动后继。失败边界包括创建成功但响应丢失、重复节点、网络断开、长暂停和旧进程恢复。断开期间所有权不确定,应暂停新的外部副作用;会话过期后旧授权永久失效,新会话只能重新排队。因为协调服务无法停止旧线程或撤销已发出的数据库、支付、文件请求,所以每次成功授权还要映射为单调业务代次,真正资源端在同一原子更新中拒绝低代次。项目中我只把它用于低频控制面、公平任务协调和 Runner(执行器)选主,库存仍由数据库条件更新,支付仍由流水唯一键、状态机、查单和对账保证。验证时会让 A 持锁后暂停,B 接管,再恢复 A,确认 A 的旧代次写被拒绝,而不是只看锁节点是否删除。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:为什么只监听前驱?直接回答:把一次释放的唤醒范围缩小到直接后继,减少读取和线程调度峰值;事件后仍要重读。
- 追问 2:创建节点后为什么不能立即执行?直接回答:创建只是加入队列,只有当前最小节点才有协调资格。
- 追问 3:锁节点删除能否证明旧进程停止?直接回答:不能,必须由权威资源通过业务代次或 Fencing Token(栅栏令牌)拒绝旧写。
- 关联专题:临时节点、顺序后缀、会话与锁所有权
问题(综合题):为什么说这种锁只能提供协调条件,不能保证外部副作用恰好一次?
- 考点:控制面与业务权威层分离。
- 回答思路:从不可撤回请求、未知结果、旧持有者和资源端裁决说明。
- 口述答案:结论是,锁只能依据锁服务当前状态决定谁满足“可以继续”的条件,无法对数据库、支付渠道、对象存储或物流供应商承诺一次且仅一次。A 持锁时可能已经发出扣款、库存调整或文件发布请求,随后网络分区或进程暂停;服务端在会话超时后删除 A 的临时节点,B 成为新持有者。此时 A 的外部请求可能已经成功但响应丢失,也可能在 A 恢复后才迟到到达。Zookeeper(分布式协调服务)既不能杀死 A,也不能撤销外部系统已接受的动作,所以“同一时刻锁目录只有一个最小节点”不等于“业务世界只有一个副作用”。工程上要把终局正确性放到真正资源端:库存用
available >= quantity条件更新和预占流水唯一键;支付用渠道交易号、稳定请求号、验签、金额币种校验和状态机;任务结果用任务键、步骤键、检查点和单调 Fencing Token(栅栏令牌);第三方超时进入未知状态,通过主动查单、回调和对账收敛。恰好一次通常不是传输层事实,而是“允许至少一次尝试,通过幂等和状态条件只接受一个有效结果”。面试表达时我会明确锁的价值仍然很大:它减少竞争、组织公平等待、控制低频角色和降低资源压力,但不能越权成为资金账本或库存账本。验收要注入会话过期、响应丢失、旧进程恢复和重复请求,检查权威状态只有一个合法结果,同时保留拒绝与补偿记录。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:唯一键和锁谁更重要?直接回答:涉及持久业务事实时唯一键和状态条件是底线,锁主要优化并发路径。
- 追问 2:是否存在真正的恰好一次调用?直接回答:跨不受控系统通常只能通过幂等、查证和对账形成业务上的单次有效结果。
- 追问 3:锁还有必要吗?直接回答:有,适合降低冲突、串行控制操作和选主,但要明确它不是终局正确性来源。
- 关联专题:Fencing Token(栅栏令牌)与外部副作用裁决
问题(综合题):请演绎 A、B、C 三个客户端如何公平加锁、等待和释放。
- 考点:具体节点序号、前驱关系、事件与状态重读。
- 回答思路:用
17/18/19的时间线讲清每一步和异常分支。 - 口述答案:假设 A、B、C 在同一资源目录依次创建临时顺序节点,得到
req-A-00000017、req-B-00000018、req-C-00000019。A 读取子节点后发现17最小,在确认会话有效后进入临界区;B 不是最小,计算直接前驱为17并只监听 A;C 的直接前驱是18,只监听 B。A 正常完成后条件删除自己的完整节点,B 收到前驱删除事件。B 不能仅凭事件就认定获锁,而要重新读取列表,确认自己的18仍存在且已经最小,然后进入;B 删除后 C 同样重读并进入。这种链式唤醒使已经成功加入队列的参与者通常按服务端序号推进,避免每次释放唤醒所有人。公平边界也要讲清:客户端调用发起时间不等于请求到达服务端时间,创建响应丢失后的重试可能制造重复节点,线程暂停会让更早节点很晚才实际执行,因此它是协调队列层的顺序公平,不是业务完成时间绝对公平。异常情况下,如果 B 在 A 删除前会话过期,18也会消失,C 可能连续收到变化或在重连后发现前驱已经不存在;正确算法始终是重读当前列表并重新计算,而不是依赖每个通知都精确到达。业务提交还要带代次,因为 A 失锁后仍可能恢复。验证时记录节点序号、会话、监听目标、事件时间和业务令牌,证明队列推进和业务裁决是两层机制。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:C 能直接监听 A 吗?直接回答:不应,监听直接前驱能保持链式唤醒,A 释放时只需唤醒 B。
- 追问 2:B 取消等待后 C 怎么办?直接回答:B 删除自身节点,C 收到事件后重读并计算新的前驱或最小资格。
- 追问 3:公平是否包含线程调度公平?直接回答:不包含,顺序节点只约束协调队列,不控制操作系统调度和业务完成时间。
- 关联专题:A/B/C 加锁、前驱监听与公平边界
问题(综合题):创建临时顺序节点时响应丢失,为什么危险,如何恢复?
- 考点:未知结果、唯一请求前缀和重复节点清理。
- 回答思路:区分失败与未知,用具体节点
21/22演绎扫描收敛。 - 口述答案:危险点在于客户端超时并不说明创建失败。服务端可能已经把
req-7f3a-00000021创建并提交,只是返回完整路径的响应在网络中丢失;客户端如果把超时当失败并直接重试,就会再创建req-7f3a-00000022。同一个业务尝试拥有两个队列身份,可能错误监听自己的另一个节点、重复等待或释放不完整。恢复方案是在每次加锁尝试的节点名前加入稳定且唯一的请求前缀,同时仍使用临时顺序创建。发生未知结果时先检查会话状态,再扫描锁父目录,按请求前缀、临时拥有者和当前会话筛选候选。找到一个就恢复其服务端完整路径;找到多个则在能够证明同属本次请求和会话的前提下保留最小序号21,幂等删除22等重复节点,然后重新计算前驱。一个也没找到时,只有确认会话仍有效且扫描结果可信,才决定是否重新创建;若会话已经过期,本次尝试必须失败,新会话只能发起新请求,不能认领旧节点。主动删除也要核对完整路径和拥有者,不能按模糊前缀批量删。生产中优先使用 Apache Curator(Apache 协调客户端框架)成熟配方来处理保护创建和连接状态,自己仍需监控未知创建次数、重复节点数、清理失败和最长节点年龄。验证时故意在服务端创建成功后丢弃响应,确认客户端能恢复同一节点,而不是产生无限重复节点。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:为什么保留最小重复节点?直接回答:它通常是最早成功创建的队列身份,保留它能维持原申请位置。
- 追问 2:只按前缀扫描安全吗?直接回答:不够,还要核对会话拥有者和完整路径,避免误认其他请求。
- 追问 3:会话过期后还能扫描认领吗?直接回答:不能,旧节点不再代表有效授权,新会话必须重新申请。
- 关联专题:创建响应丢失、未知节点名与幂等清理
问题(综合题):连接抖动、断网和会话过期时,客户端应该如何处理锁状态?
- 考点:暂停、不确定、失锁和新申请状态机。
- 回答思路:区分连接状态与会话状态,说明每个状态允许的业务动作。
- 口述答案:我会把锁客户端至少建模为等待、持有、连接暂停、明确失锁和已释放五种状态。短暂断连只表示当前连接不可用,服务端在协商超时内可能仍保留原会话和临时节点,所以既不能立即认定锁已经释放,也不能继续把本地
locked=true当作授权。进入暂停状态后停止领取新任务、暂停可中断的外部副作用,只允许做可丢弃的本地计算;恢复原会话后重新读取自身节点和队列位置,验证通过才继续。若收到会话过期,旧会话不可复活,临时节点已经或将被服务端删除,本地重入计数和锁标记全部作废,旧临界区必须结束。已经发出的数据库或第三方请求按稳定请求号查询真实结果,不能盲重试;后续若要继续,必须建立新会话、使用新请求标识、重新排队并获得更高业务代次。最危险场景是长 Stop-The-World(全停顿):A 暂停超过会话超时,B 已接管,而 A 恢复后业务线程并不知道。对此不能只依赖连接回调,还要让任务表或数据库在提交时比较 Fencing Token(栅栏令牌)与状态版本。参数上,会话超时过短会因抖动频繁误过期,过长会延迟崩溃接管;应根据真实网络尾延迟和暂停分布设置,并用演练验证。监控要区分连接暂停次数、会话失效、接管、旧代次拒绝和业务状态年龄。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:断连后能继续本地计算吗?直接回答:可做可丢弃计算,但提交前必须重验会话和业务代次,不能执行不可逆副作用。
- 追问 2:会话超时越长越安全吗?直接回答:不是,会延长故障接管;正确性仍要依靠业务围栏。
- 追问 3:新连接能恢复旧锁吗?直接回答:只有原会话在超时内恢复才可能继续;会话过期后的新会话不能认领旧授权。
- 关联专题:崩溃、连接抖动、会话过期与旧进程恢复
问题(综合题):请用具体数据解释旧持有者如何被 Fencing Token(栅栏令牌)拒绝。
- 考点:单调授权、原子比较与幂等职责。
- 回答思路:用
501/502和任务版本逐时刻演绎。 - 口述答案:假设任务
J7的 A 获得锁后,权威任务表为它分配token=501,状态是RUNNING/A/version=8。A 执行到一半发生 40 秒暂停,而会话超时是 20 秒;服务端删除 A 的临时节点,B 排队成为最小节点并获得新代次502。B 用条件version=8 AND token=501 AND lease_expired接管任务,把状态改为RUNNING/B/token=502/version=9,随后完成结果并在同一事务中检查当前令牌仍是502。A 恢复后带着旧内存状态提交token=501/version=8,数据库执行UPDATE ... WHERE token <= 501 AND version=8,由于当前最大令牌是502且版本已是 9,影响行数为 0,A 必须读取新状态并丢弃或清理自己的临时结果。这里 Fencing Token(栅栏令牌)表达授权新旧,随机请求标识则负责同一持有者重试去重;如果 B 的成功响应丢失,稳定请求键让 B 重试返回同一结果。如果外部对象存储不支持令牌条件,就先上传J7/501.tmp和J7/502.tmp,只允许数据库中的正式指针由当前代次发布,旧文件不能成为用户可见结果。令牌校验必须在真正资源端与写入原子完成,应用层先查再无条件写会留下竞态。验证用暂停 A、让 B 完成、再恢复 A 的故障注入,观察旧代次拒绝指标和唯一正式结果。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:令牌必须全局递增吗?直接回答:通常只需对同一受保护资源可比较且单调,范围越大争用和持久化成本越高。
- 追问 2:随机 UUID(通用唯一标识符)能做栅栏吗?直接回答:不能,它只有唯一性没有新旧顺序。
- 追问 3:资源端不支持条件写怎么办?直接回答:通过可围栏的提交代理或数据库正式指针间接保护,并保留查证和补偿。
- 关联专题:Fencing Token(栅栏令牌)与外部副作用裁决
问题(综合题):可重入分布式锁如何实现,最容易出现哪些问题?
- 考点:线程身份、计数、会话失效和异常释放。
- 回答思路:从首次远端获取、重入计数、最终释放和失锁作废说明。
- 口述答案:可重入锁的核心不是再创建一个节点,而是识别同一逻辑所有者。第一次由线程 T 在当前锁对象和有效会话下创建临时顺序节点,排到最小后把本地重入计数设为 1;同一线程再次进入嵌套方法时,确认锁上下文相同,只把计数加到 2,不重新排队。每次成功获取都必须对应一次释放,第一次释放把 2 减到 1,最后一次释放才删除远端节点并清理上下文。所有者通常包含线程身份,不能让同进程另一线程借用,否则调用栈和释放责任会失控。最常见问题一是少释放导致计数长期不归零,其他实例一直等待;二是异常路径多释放,引发所有权错误;三是会话已经过期但本地计数仍大于零,旧线程误以为持锁继续写;四是跨线程异步回调丢失原所有权。会话过期时不论计数多大,都要把上下文标记为
LOST(已失去)并停止业务,后续finally(最终清理)只做幂等清理,不能恢复授权。生产优先使用 Apache Curator(Apache 协调客户端框架)的InterProcessMutex,但仍需理解连接状态、设置有界等待、记录重入深度和持锁年龄。业务层结果照样使用状态条件和 Fencing Token(栅栏令牌)。验证应覆盖两层嵌套正常释放、内层异常、线程中断、会话过期和错误跨线程释放。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:同进程不同线程算重入吗?直接回答:通常不算,所有权应包含线程,除非显式设计可转移任务所有权。
- 追问 2:重入计数保存在 Zookeeper(分布式协调服务)吗?直接回答:常见实现保存在客户端本地,远端仍是一个节点,因此会话失效会让本地计数全部作废。
- 追问 3:如何发现少释放?直接回答:监控持锁年龄、重入深度和等待时间,并保证获取释放在结构化控制流中配对。
- 关联专题:可重入锁、线程所有权与 Apache Curator(Apache 协调客户端框架)
问题(综合题):读写锁和信号量如何基于顺序节点实现?
- 考点:资格函数、写者公平与许可失效。
- 回答思路:分别给出读、写、前 K 个节点的判定规则。
- 口述答案:它们共享“临时顺序节点形成队列”的骨架,区别是资格判定。读写锁为节点标记读或写:写节点只有在自己是最早未完成节点时才能进入,需要等待所有更小序号的读写节点;读节点可以与前面的读节点并发,但若前面存在写节点,就监听最近的前置写节点,不能越过它,否则持续到来的读者会让写者饥饿。例如队列
R41,R42,W43,R44,前两个读者并发,W43 等待两者释放,R44 也必须等 W43。读锁直接升级为写锁很危险,多个读者同时升级会互相等待,应释放后重新申请并用版本检查间隙变化。信号量则设许可数 N,通常当前最小的 N 个有效租约获得资格,其余节点监听影响自己排名的前驱;会话过期会释放对应许可。它控制的是同时占用数量,不是每秒请求速率,也不能保证下游真实容量永远等于 N。生产应使用 Apache Curator(Apache 协调客户端框架)的成熟读写锁和信号量配方,设置等待截止时间、取消和连接状态处理。异步导出可以用信号量限制重任务并发,避免 OOM(内存溢出),但任务检查点、分片摘要和最终发布仍需持久化和代次围栏。验证要覆盖写者等待、读者不插队、许可拥有者崩溃、会话抖动和容量动态下调。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:读锁为什么不能越过已排队写锁?直接回答:否则不断到来的读者会让写者长期饥饿,破坏队列公平。
- 追问 2:信号量等同于每秒限流吗?直接回答:不等同,它控制并发占用数,速率还要用令牌桶等机制。
- 追问 3:许可泄漏如何恢复?直接回答:临时租约随会话过期释放,主动路径仍要结构化归还并监控许可年龄。
- 关联专题:读写锁、共享锁、信号量与容量配方
问题(综合题):Apache Curator(Apache 协调客户端框架)解决了什么,又没有解决什么?
- 考点:成熟配方价值与业务正确性边界。
- 回答思路:列配方、竞态封装、连接状态,再列外部副作用边界。
- 口述答案:Apache Curator(Apache 协调客户端框架)的价值是把容易写错的客户端协调配方封装成经过长期验证的组件,包括可重入互斥锁、读写锁、信号量、领导者选举、重试策略和连接状态监听。手写锁不仅要创建节点和排序,还要处理创建响应丢失、节点保护、前驱在注册监听前已经删除、线程中断、超时取消、重入计数、异常释放、重连和会话过期;任何一个竞态遗漏都可能永久等待或重复进入。成熟框架能显著降低协议实现错误,并提供统一的生命周期和可观测入口。它没有解决三类问题:第一,网络分区和长暂停后旧线程仍可能继续执行,框架无法撤销外部副作用;第二,数据库、支付渠道和对象存储的幂等与状态机不属于锁客户端职责;第三,错误的场景选型、无限等待、过大热点目录和错误会话参数仍会导致系统故障。因此使用时要锁定客户端和服务端兼容版本,理解
SUSPENDED(连接暂停)与LOST(会话失效)的业务含义,为获取设置截止时间,在失锁回调中停止领取和提交,并让权威端校验 Fencing Token(栅栏令牌)。项目评审还要问是否能用数据库条件更新或天然分区串行替代。验证既要跑正常互斥,也要注入未知创建、断连、会话过期、重复释放和旧持有者恢复,不能以“用了框架”作为正确性证明。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:用了框架还需要看源码吗?直接回答:至少要理解配方状态机、连接回调和版本边界,排障时才能解释行为。
- 追问 2:是否应该统一无限等待获取?直接回答:不应,业务必须有截止时间、取消和降级,防止线程与连接池耗尽。
- 追问 3:框架能自动生成业务栅栏吗?直接回答:不能,业务代次要与真正资源端的条件写契约结合。
- 关联专题:可重入锁、线程所有权与 Apache Curator(Apache 协调客户端框架)
问题(综合题):如何设计一个安全的 Runner(执行器)领导者选举和任务接管流程?
- 考点:领导代次、任务租约、检查点、围栏和优雅退位。
- 回答思路:把选主、任务表和结果提交分层说明。
- 口述答案:我会把 Zookeeper(分布式协调服务)选主只作为“谁负责调度”的控制面,把任务表作为执行状态权威。各 Runner(执行器)通过 Apache Curator(Apache 协调客户端框架)的领导者配方参与,获得资格时生成或持久化新的
leader_epoch,先重建任务视图,再开始领取。领取任务使用唯一任务键和条件更新,写入所有者、任务版本、领导代次、租约到期时间和检查点;执行步骤按任务键加步骤号幂等,长任务定期保存进度。连接进入暂停时立刻停止领取新任务,可中断步骤保存检查点;会话明确失效时永久退位,旧代次不再允许提交。新领导者获得更高代次后,只接管租约已过期且状态允许的任务,使用版本条件把所有者和代次推进。旧领导者恢复后,即使仍拿着内存结果,数据库也会因代次和版本较低拒绝;对象存储等外部结果先写带代次临时路径,只有任务表中的当前代次能发布正式指针。优雅下线顺序是先关闭领取入口、等待有限时间或保存检查点、再释放领导资格,不能先释放后继续派发。消息与任务允许重复,结果只接受一次合法状态迁移。监控包括选主耗时、领导代次、连接暂停、失租、接管次数、任务年龄、旧代次拒绝和孤儿产物。演练时暂停旧领导者超过会话超时,让新领导者接管并完成,再恢复旧进程,确认唯一正式结果和可审计拒绝记录。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:选主后任务可以只放内存吗?直接回答:不能,切主和崩溃会丢状态,任务与检查点必须持久化。
- 追问 2:为什么还要任务租约?直接回答:领导者可能存活但单个工作线程卡死,任务租约支持细粒度接管。
- 追问 3:优雅下线先删节点吗?直接回答:不应,先停止领取并处理在途,再释放资格,避免新旧领导者并发派发。
- 关联专题:领导者锁、选主配方与任务所有权
- 问题(综合题):Zookeeper(分布式协调服务)锁、Redis(远程字典服务)锁和数据库锁如何强制比较?
- 考点:权威资源、所有权、故障切换、公平、性能和业务围栏。
- 回答思路:先说没有绝对最安全,再按失败语义和场景选型。
- 口述答案:我不会先问哪种锁“最强”,而会先问受保护的不变量在哪里、临界区多长、故障时允许什么。数据库锁或条件更新直接作用于业务权威行,事务结束自动释放,最适合库存扣减、订单状态迁移等短事务;它的风险是长事务占连接、锁等待、死锁和把外部调用包进事务。Redis(远程字典服务)锁通常用唯一持有者值和租约,获取释放延迟低、吞吐高,适合缓存重建、短时去重等效率锁;风险是租约过期、续期与业务线程脱节、主从异步复制和故障切换可能丢失锁状态。Zookeeper(分布式协调服务)锁以会话、临时顺序节点和法定历史组织等待,公平队列、失效清理和选主语义更清楚,适合低频控制面和公平任务协调;代价是写入、读取、监听和会话运维成本更高,热点目录也可能形成压力。三者的共同边界是旧持有者:事务外调用、租约过期或会话过期后,旧线程仍可能迟到写,因此资金、库存和任务结果都要在真正资源端用唯一键、状态机、版本或 Fencing Token(栅栏令牌)裁决。选型还要比较锁等待上限、死锁、故障域、降级和团队运维。验证不只跑并发互斥,还要注入主从切换、法定人数丢失、长暂停、响应丢失和旧进程恢复,证明业务不变量不因锁服务变化而破坏。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:数据库锁一定最安全吗?直接回答:对同库短事务边界清楚,但事务外副作用仍需幂等和查证,长事务也会放大风险。
- 追问 2:Redis(远程字典服务)锁能做资金锁吗?直接回答:不能作为唯一地基,资金必须由账务流水、状态机和对账保证。
- 追问 3:Zookeeper(分布式协调服务)锁最适合什么?直接回答:低频控制面、公平排队、主任务选举和可接受更高协调成本的场景。
- 关联专题:三类锁选型
- 问题(综合题):WMS(仓储管理系统)库存防超卖为什么不应依赖 Zookeeper(分布式协调服务)锁?
- 考点:库存不变量、热点性能和失锁旧写。
- 回答思路:用可用量 5、订单 4 和 3 演绎数据库条件,再说明锁的辅助位置。
- 口述答案:库存防超卖的终局条件是权威库存不为负,并且每个订单预占、扣减、释放和修正都有唯一流水。假设商品可用量是 5,订单 A 要 4,订单 B 要 3,数据库可用
UPDATE stock SET available=available-? WHERE sku=? AND available>=?。A 成功后余量为 1,B 影响行数为 0,从原子条件上守住不变量;订单预占号加唯一约束,重试不会扣两次。若改成 Zookeeper(分布式协调服务)锁,A、B 虽然按节点排队,但 A 可能会话过期后恢复并执行无条件扣减,B 也已经接管,锁本身不能阻止库存变负。高频下单还会让同商品目录产生大量创建、读取、监听和删除,延迟与运维成本高。正确架构是以数据库条件或按商品分区串行为底线,热点时通过请求削峰、库存分段、按仓分片和异步预占降低冲突。远程锁只在极低频的库存批量调整、盘点冻结或波次控制中作为协调工具,并且每次调整仍比较库存版本和任务代次。异常恢复以库存流水守恒、订单状态和仓内实物为证据,不以“锁日志显示成功”作为库存事实。验证同时压测 A/B 并发、重复消息、释放迟到和旧进程恢复,检查库存不负、预占唯一、释放不多加,并对账初始量、入库、预占、扣减、释放和修正。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:数据库冲突高怎么办?直接回答:分片、排队、削峰或库存段预分配降低冲突,仍保留权威条件更新。
- 追问 2:批量盘点能用锁吗?直接回答:可用于低频任务协调,但盘点差异和修正必须写版本化流水并可审计。
- 追问 3:多仓库存如何处理?直接回答:按仓与商品建立独立不变量,跨仓用预占加补偿,避免全局大锁。
- 关联专题:WMS(仓储管理系统)库存、履约与跨境物流选型
- 问题(综合题):支付项目中分布式锁应该放在哪里,绝不能替代什么?
- 考点:支付状态机、渠道未知结果、账务防重与对账。
- 回答思路:以创建、回调、查单、退款和对账分层回答。
- 口述答案:支付正确性的主线不是“同一订单只能有一个线程”,而是任何并发和重试下资金状态都可证明。创建支付使用商户请求号作为稳定幂等键,渠道请求保存请求与响应;回调先验签,再校验商户、订单、金额、币种和渠道交易号,用渠道流水唯一键吸收重复,按合法前置状态推进订单并在同一事务落账务流水。主动查单与回调可以并发,但都依据渠道事实和状态机更新,不能谁拿锁谁覆盖。超时是未知结果,优先查单或使用渠道支持的幂等键重试,不能因为锁过期就再次扣款。退款也要有独立退款请求号、渠道退款号、累计金额约束和对账。Zookeeper(分布式协调服务)锁可以用于低频对账主任务选举、文件批次解析协调,或减少同订单短时间内的重复处理;Redis(远程字典服务)锁也可作为效率优化。但任何远程锁都不能替代渠道流水唯一约束、支付状态机、账务借贷守恒、主动查单、渠道账单对账和人工差错处理。对账任务即使选出一个主节点,仍要用批次号、文件摘要、行唯一键、检查点和 Fencing Token(栅栏令牌)防止旧主恢复写入。事故中先停止自动补偿,按渠道账单、内部订单、账务流水和差异单取证,所有修复生成新流水,不直接改余额。故障演练要覆盖重复回调、查单与回调竞态、渠道成功响应丢失、选主切换和旧批次提交。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:锁住订单号能防重复入账吗?直接回答:不能完全防,最终要靠账务流水唯一键和状态迁移条件。
- 追问 2:回调和查单谁优先?直接回答:都只是渠道事实来源,按签名、交易号和状态机收敛,不按线程先后决定。
- 追问 3:远程锁可用于对账吗?直接回答:可用于选主和降低并发,但批次幂等、检查点、围栏和差异复核仍不可少。
- 关联专题:支付资金正确性、排障与设计红线
- 问题(综合题):跨境物流面单购买和轨迹同步为什么不能长时间持有分布式锁?
- 考点:第三方尾延迟、未知结果、幂等与状态重建。
- 回答思路:区分本地并发协调和供应商终局事实。
- 口述答案:面单供应商和轨迹平台是外部系统,响应时间、重试语义和可用性不受本系统控制。若在调用前拿分布式锁并一直持有到第三方返回,慢请求会占用锁和工作线程,后续订单排队;网络超时仍无法判断供应商是否已经购买成功,锁过期后旧调用还可能返回,新持有者又发起第二次购买。正确流程是先在数据库建立稳定业务请求号和状态机,例如
INIT -> REQUESTING -> SUCCESS/UNKNOWN/FAILED,短事务内用唯一键防本地重复,然后在事务外调用供应商。成功后按供应商面单号和请求号条件推进;超时进入UNKNOWN(未知),停止无条件重试,通过供应商查询接口、回调或账单查证。若供应商支持幂等键,重复调用复用同一键;不支持时限制自动重试并进入人工差错。分布式锁最多减少同一业务号的瞬时并发,不承担供应商防重。轨迹同步同样以运单号、供应商事件号、节点代码和发生时间去重,保留原始事件后按业务规则构建当前视图;Runner(执行器)可用 Zookeeper(分布式协调服务)选主或分片协调,但游标、检查点和事件都要持久化,失锁后新实例从检查点继续。验证要在请求到达供应商后丢弃响应、重复回调、乱序轨迹和选主切换,确认只产生一张有效面单、费用可对账、轨迹不倒退且任务可接管。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:供应商无查询接口怎么办?直接回答:减少自动重试,依赖稳定业务字段、账单对账和人工差错,不能用锁推断结果。
- 追问 2:轨迹乱序如何处理?直接回答:保留原始事件,按来源序号、业务阶段和规则重建视图,不简单按到达时间覆盖。
- 追问 3:锁能防两次购买吗?直接回答:只能降低本系统并发,供应商是否重复处理仍需幂等键、查单和对账。
- 关联专题:WMS(仓储管理系统)库存、履约与跨境物流选型
- 问题(综合题):锁目录出现大量节点、等待时间飙升,如何排查和止血?
- 考点:容量、残留、会话、热点和业务降级。
- 回答思路:按影响、限流、证据、分类、清理、修复和演练闭环。
- 口述答案:先确认影响是单资源热点、整个集群写延迟还是客户端连接风暴,并暂停或限流新增锁申请,避免继续放大。业务侧对非关键效率锁可降级为旧缓存、请求合并或快速失败;对波次、对账等正确性敏感控制任务暂停自动执行,保留人工安全入口,不能无锁并发运行。随后采集锁目录子节点数量、创建速率、最老节点年龄、临时拥有者、活跃会话、等待分位、连接数、未完成请求、服务端角色、磁盘同步和选举频率。把节点分类为活跃临时节点、未知创建产生的同前缀重复节点、异常持久节点和已经无拥有者的可疑残留。活跃会话的节点不能按年龄直接删除;重复节点要核对请求前缀、会话和完整路径,由拥有者或受控脚本保留最小候选并条件清理;错误的持久顺序节点要先确认业务不再依赖。若大量等待来自一个热点资源,评估是否把高频业务错误地放在 Zookeeper(分布式协调服务)锁上,改为数据库条件更新、分区排队或本地合并。还要检查客户端是否监听父目录造成羊群效应、无限重试、会话超时过短或连接池复用错误。恢复时分批放量,观察节点增长与清理速率是否平衡。复盘需加入目录基数阈值、等待超时、重复前缀计数、客户端版本治理和故障演练,禁止直接删除整个目录,因为这会同时释放真实持有者并让等待者并发进入。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:最老节点能直接删除吗?直接回答:不能,先核对临时拥有者、会话和业务状态,年龄不是所有权证据。
- 追问 2:如何快速止住羊群效应?直接回答:限流新申请,改为前驱监听,客户端重连退避并对热点目录分层。
- 追问 3:为什么不删除父目录重建?直接回答:会释放所有真实持有者、制造并发进入并丢失事故证据。
- 关联专题:创建响应丢失、未知节点名与幂等清理
- 问题(综合题):如何设计锁的超时、取消、重试和降级策略?
- 考点:有界等待、重试预算、业务截止时间和失败语义。
- 回答思路:从端到端截止时间分配到获取、执行、提交和清理。
- 口述答案:锁不能无限等待,策略应从业务端到端截止时间反推。假设接口总预算 3 秒,锁获取可分配 300 毫秒到 500 毫秒,临界区必须有明确上限,提交和清理保留余量;超过获取预算就取消自己的等待节点并返回可识别结果,而不是占住线程池。取消时要处理创建结果未知:先恢复真实节点路径再删除,不能只丢本地 Future(未来结果)让远端节点继续排队。重试只针对明确的可恢复错误,并使用指数退避、随机抖动和总次数预算;连接断开或会话状态未知时不立即高频重建节点。不同场景降级不同:缓存重建属于效率锁,可读取旧值、单飞合并或受控回源;波次发布和支付对账属于控制面任务,锁不可用时暂停自动执行并保留人工审批;库存扣减本来就应由数据库条件保证,不因锁不可用而破坏不变量。临界区内的外部调用要尽量移出,必须调用时使用稳定幂等键和查证,超时后进入未知状态。取消或会话过期不能撤回已发请求,所以结果提交继续比较任务版本和 Fencing Token(栅栏令牌)。监控等待时间、取消率、重试次数、会话暂停、节点残留和降级使用量。验证通过服务端慢写、网络抖动、线程池拥塞和批量取消,确认没有重试风暴、远端孤儿和无锁越权执行。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:获取超时后能立即重试吗?直接回答:先确认未知创建结果并清理,再按退避和总预算决定,不能盲重试。
- 追问 2:所有锁失败都应快速失败吗?直接回答:取决于业务;效率锁可降级,关键控制任务通常暂停并报警。
- 追问 3:为什么要端到端截止时间?直接回答:避免各层独立超时叠加,让请求早已失去业务价值却仍占资源。
- 关联专题:A/B/C 加锁、前驱监听与公平边界
- 问题(综合题):如何证明你的 Zookeeper(分布式协调服务)锁实现没有羊群效应和监听竞态?
- 考点:前驱监听、先查后监听竞态和状态重读。
- 回答思路:说明正确注册模式、连接重建和指标验证。
- 口述答案:算法层面,每个非最小节点只监听紧邻前驱,而不是监听锁父目录或统一持有者节点。关键竞态是客户端先读取到前驱
17,准备注册监听时17已经删除;若注册与检查分成不可校验的两步,客户端可能永远等不到事件。成熟配方会使用带监听的存在性检查,并根据返回结果决定:前驱仍存在则等待,已不存在则立即重新读取队列。即使收到事件,也只把它当唤醒信号,再次确认自身节点、会话和最小资格;即使没有收到某个事件,连接恢复、等待超时或状态回调也会触发重建读取。连接断开期间可能错过多个变化,恢复后不能补演每个历史事件,而应读取当前事实并注册新的前驱。验证不能只看代码,我会用 A/B/C 到数百客户端压测,记录每次节点删除触发的客户端唤醒数量、父目录读取次数、监听注册量、线程池队列和服务端通知速率。理想情况下单次正常释放只唤醒一个直接后继,批量会话过期可能有有限连锁重算,但不应全体同时读取。再故意在读取前驱和注册监听之间删除节点、在事件回调前断网、让等待者取消和会话过期,确认所有客户端最终要么获得资格、要么超时退出,没有永久阻塞和多持有者。若仍有重连风暴,应增加指数退避、连接共享、目录分层和申请限流。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:事件顺序能直接决定锁顺序吗?直接回答:不能,事件只唤醒,当前节点列表和会话状态才决定资格。
- 追问 2:监听父目录有什么问题?直接回答:任一变化会唤醒全部等待者,形成读取和调度峰值。
- 追问 3:如何测试监听注册竞态?直接回答:在读取前驱与注册之间注入删除,验证客户端能立即重读而不是永久等待。
- 关联专题:A/B/C 加锁、前驱监听与公平边界
- 问题(综合题):锁节点主动释放、会话过期释放和运维强制释放有什么不同?
- 考点:释放证据、未知结果和风险分级。
- 回答思路:比较触发者、可确认性和业务前置条件。
- 口述答案:主动释放发生在持有者正常结束临界区后,客户端按保存的完整路径删除自己的节点;删除也可能响应丢失,所以释放方要把业务提交和锁清理分开理解,不能因删除超时就重复业务。会话过期释放由服务端终止会话后删除全部临时节点,适用于进程崩溃或失联,但只撤销协调资格,不能证明旧业务线程和外部请求停止。运维强制释放风险最高,它绕过正常所有者和会话语义,可能在真实持有者仍执行时唤醒后继,制造并发写。强制处理前应先从业务入口止血,让真正资源端启用或确认 Fencing Token(栅栏令牌)与状态条件,采集节点完整路径、临时拥有者、会话、创建时间、业务请求和当前任务代次,再判断是重复节点、错误持久节点还是卡住的活跃节点。若必须释放,先隔离旧实例或撤销其写权限,再条件删除特定节点,并观察后继接管;不能删除整个父目录。主动释放代码应在
finally(最终清理)执行,但只有成功获取后才释放,且所有权异常不能被吞掉。对于外部调用结果未知的临界区,先把业务状态写为待查证,再释放锁,让后续处理根据幂等键和状态机接续,而不是持锁无限等待。验收分别注入正常完成、删除响应丢失、进程崩溃和人工释放,检查业务只接受合法代次,锁队列最终推进,并保留完整审计记录。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:释放超时能否认为节点还在?直接回答:不能,结果未知,应读取完整路径或等待会话状态确认,业务不能重复执行。
- 追问 2:强制删除前最重要的动作是什么?直接回答:先阻断旧持有者的业务写,让权威端具备围栏,再处理协调节点。
- 追问 3:为什么不能删父目录?直接回答:会同时影响真实持有者和所有等待者,扩大并发与证据损失。
- 关联专题:崩溃、连接抖动、会话过期与旧进程恢复
- 问题(综合题):Zookeeper(分布式协调服务)锁的公平性有哪些严格边界?
- 考点:服务端顺序、请求到达、重试、调度和业务顺序。
- 回答思路:定义公平对象,再逐层排除不保证的公平。
- 口述答案:它能提供的公平性是:在同一父目录下已经成功创建的顺序节点具有明确序号,正常算法让最小节点先取得资格,后继只在更小节点消失后推进,因此较晚的已入队节点通常不能越过较早节点。这个结论有多个边界。第一,客户端本地调用开始时间不等于请求到达服务端的顺序,网络延迟不同会改变谁先获得序号;第二,创建成功响应丢失时,错误重试会产生多个节点,同一业务尝试可能占多个位置;第三,前驱获得资格后可能线程暂停或业务执行很慢,序号只决定进入次序,不保证完成时间;第四,等待者可能超时取消、会话过期或重连,队列会动态收缩;第五,不同资源父目录的序号没有全局可比性。读写锁还要防止后续读者越过已排队写者,否则写者会饥饿。业务若要求严格按订单创建时间、会员等级或仓库优先级处理,应把该优先级写入权威任务队列,并由单分区消费者、数据库排序领取或专门调度器执行,不能把锁节点后缀当业务排序协议。工程上要给等待设置截止时间和取消路径,记录创建时间、节点序号、获得资格时间与完成时间,区分“协调队列公平”和“业务服务水平”。验证时用不同网络延迟、响应丢失、取消和暂停注入,确认已入队顺序不被随意越过,同时承认业务完成顺序可以不同。面试中把它称为相对清晰的先到先服务协调队列更准确,而不是绝对公平锁。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:顺序后缀能代表请求发起时间吗?直接回答:不能,只代表服务端对成功创建请求的排序。
- 追问 2:业务优先级队列怎么做?直接回答:把优先级持久化到权威任务队列,由调度规则选取,锁只保护领取过程。
- 追问 3:取消等待会破坏公平吗?直接回答:会改变当前队列,但属于显式退出;后继重读后按剩余序号继续。
- 关联专题:A/B/C 加锁、前驱监听与公平边界
- 问题(综合题):如何处理读锁升级、写锁降级和写者饥饿?
- 考点:读写锁队列、升级死锁和版本复核。
- 回答思路:禁止隐式升级,按队列重新申请,用状态版本保护窗口。
- 口述答案:读锁升级成写锁不能简单地“持有读锁再等待写锁”。假设 A、B 都持有读锁并同时申请升级,写锁要求前面的所有读者释放,而 A、B 又都等待自己升级成功后才释放,就会形成分布式死锁。更稳妥的流程是读取所需状态并记录业务版本,释放读锁,然后作为新的写请求按顺序队列申请;获得写锁后重新读取并比较版本,若期间数据已变化就重新计算或放弃,不能沿用旧读结果。写锁降级为读锁也要谨慎:如果配方明确支持,可在仍持写锁时先建立读资格,再释放写锁,避免中间完全无锁窗口;如果框架不保证原子降级,就释放后重新申请并接受其他写者可能插入。写者饥饿的防线是读节点不能越过已经排队的更小写节点,例如
R41,R42,W43,R44中 R44 必须等待 W43,而不是因为自己也是读者就立即进入。客户端还要设置有界等待和取消,避免失联节点长期阻塞。即使读写锁协调正确,业务数据仍要以版本或条件更新验收,因为会话过期后的旧读者可能迟到写缓存或派生结果。生产优先使用 Apache Curator(Apache 协调客户端框架)的成熟读写锁配方,并验证其可重入和升级边界,不自行拼装。测试覆盖双读者同时升级、写者排队期间持续新读、会话过期、超时取消和版本变化,观察没有永久等待、写者能够推进且旧版本提交被拒绝。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。 - 追问 1:为什么释放读锁再申请写锁不安全?直接回答:中间状态可能变化,所以必须记录并重验业务版本,而不是假设连续性。
- 追问 2:写锁一定可以降级吗?直接回答:取决于具体配方契约,不应自行假设原子性;不支持时重新申请并复核。
- 追问 3:如何防写者饥饿?直接回答:后续读者不得越过已排队写者,按顺序节点和最近前置写者等待。
- 关联专题:读写锁、共享锁、信号量与容量配方
- 问题(综合题):分布式信号量如何用于异步导出,怎样避免 OOM(内存溢出)与任务丢失?
- 考点:容量控制、持久检查点、接管和结果发布。
- 回答思路:协调并发不等于任务可靠,分别设计许可和任务状态。
- 口述答案:异步导出的风险是大查询、内存聚合、文件合并和对象上传同时发生,单纯增加线程会把吞吐问题变成 OOM(内存溢出)。我会先按租户或节点测算数据库、堆内存、磁盘和对象存储容量,再用 Apache Curator(Apache 协调客户端框架)的信号量配方限制重任务并发,例如最多 3 个有效许可;只有排在许可范围内且会话有效的执行者开始重步骤,其他任务在持久任务表中等待,不占用业务线程无限阻塞。许可只控制并发容量,任务可靠性由任务表负责:任务键唯一,状态包含分片、检查点、所有者、版本、代次、重试次数和下次执行时间;每个分片流式读取并写临时文件,保存行数、摘要和游标,避免整批数据进入堆。执行者崩溃或会话过期后许可释放,新执行者通过条件接管任务,从最后检查点继续;旧执行者恢复后,低 Fencing Token(栅栏令牌)不能发布正式文件。对象先上传带任务号和代次的临时路径,最终文件指针在数据库中原子发布,重复上传只产生可清理孤儿。许可数还要与本地线程池隔离、查询超时和背压联动,不把信号量当每秒速率限制。监控任务年龄、许可占用、内存、分片耗时、接管、围栏拒绝和孤儿文件。故障演练在查询、合并、上传和发布四个阶段分别终止进程,确认任务不丢、正式结果唯一且内存受控。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:许可数如何确定?直接回答:根据数据库连接、堆内存、磁盘和对象存储压测结果设置,并保留动态降级。
- 追问 2:会话过期后文件怎么办?直接回答:临时文件按任务号和代次标识,新执行者接管,旧文件不可发布并由清理任务回收。
- 追问 3:信号量能替代线程池吗?直接回答:不能,信号量做跨实例容量协调,本地线程池仍负责执行隔离和背压。
- 关联专题:读写锁、共享锁、信号量与容量配方
- 问题(综合题):如果会话频繁过期导致锁抖动,你会如何定位?
- 考点:客户端、网络、JVM(Java 虚拟机)、服务端和参数证据链。
- 回答思路:先止血,再按时间对齐连接、暂停、法定人数和磁盘指标。
- 口述答案:先判断是单个客户端、某个机房还是整个集群,并暂停受影响的关键控制任务,非关键锁申请限流和退避,避免过期后集体重连与重建节点形成风暴。证据要按同一时间线收集:客户端连接状态、协商会话超时、心跳、请求延迟、重试次数、线程池是否饥饿;JVM(Java 虚拟机)侧查看 GC(垃圾回收)暂停、CPU(中央处理器)饱和、线程阻塞和宿主机暂停;网络侧看双向丢包、抖动、连接追踪和跨机房路径;服务端看 Leader(领导者)角色、法定人数、选举频率、未完成请求、磁盘同步延迟、连接数和会话过期速率。若只有应用实例异常,重点查 Stop-The-World(全停顿)、事件线程被业务阻塞或容器 CPU(中央处理器)限额;若全体同时异常,查服务端磁盘、法定人数和网络。会话超时不能靠盲目调大解决:过短会误过期,过长会让真实崩溃迟迟不接管,应根据网络和暂停的高分位数据设置,并保证客户端事件线程不执行重业务。恢复时分批重连和放量,确认节点创建与删除速率平衡。业务层必须把连接暂停视为所有权不确定,把明确过期视为永久失锁,任务提交由代次拒绝,因此即使抖动也不破坏资金和库存。复盘加入会话暂停、过期、选主切换、锁等待和旧代次拒绝告警,并定期做网络抖动与长暂停演练。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:直接调大会话超时可以吗?直接回答:只能缓解误过期,可能延长真实故障恢复,必须先定位暂停和网络根因。
- 追问 2:为什么客户端事件线程不能做业务?直接回答:阻塞事件处理会延迟心跳、状态回调和监听处理,放大会话抖动。
- 追问 3:如何避免恢复风暴?直接回答:客户端指数退避、随机抖动、共享连接、申请限流和分批放量。
- 关联专题:崩溃、连接抖动、会话过期与旧进程恢复
- 问题(综合题):如何给分布式锁建立可观测性和告警?
- 考点:协调指标、业务指标、关联标识与告警分级。
- 回答思路:按客户端、锁目录、服务端和业务权威四层建设。
- 口述答案:只监控“获取成功率”不够,我会建立四层证据。客户端层记录资源键哈希、请求标识、完整节点路径、会话标识、前驱、创建耗时、等待时间、持锁年龄、重入深度、取消、连接暂停和会话失效;日志避免直接暴露支付或租户敏感键。目录层统计子节点数量、创建删除速率、同前缀重复节点、最老节点、父目录读取和通知数量,用于发现热点、未知创建和羊群效应。服务端层关注角色、法定人数、请求延迟、未完成请求、连接、会话、Watcher(监听器)、磁盘同步、快照和选举频率。业务层最关键,记录任务代次、Fencing Token(栅栏令牌)拒绝、幂等命中、状态条件失败、接管次数、业务状态年龄、库存差异和支付对账差异。所有指标通过请求号、任务号、会话和节点路径关联,事故时才能从锁等待追到业务结果。告警按风险分级:节点数持续增长、会话大面积过期和法定人数丢失是平台告警;关键任务长期无进展、旧代次写被突然大量拒绝是业务告警;单次锁竞争只做指标不轰炸值班。阈值应基于基线和高分位,不用固定平均值。看板同时展示正常路径和降级状态,发布新客户端按版本比较重复节点与等待分位。定期用创建响应丢失、长暂停、网络抖动和强制接管演练,验证日志能回答谁创建、谁等待、谁失锁、谁的业务写最终被接受。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:为什么旧代次拒绝是好指标?直接回答:它证明围栏在发挥作用,同时异常上升也提示会话抖动或旧进程恢复。
- 追问 2:锁键能直接写日志吗?直接回答:需做脱敏或哈希,尤其支付、租户和订单标识,保留可关联但不泄露。
- 追问 3:节点数告警如何设?直接回答:结合正常并发、创建删除速率和最长等待基线,关注持续增长而非瞬时峰值。
- 关联专题:创建响应丢失、未知节点名与幂等清理
- 问题(综合题):如何测试一个分布式锁方案达到生产要求?
- 考点:机制、失败注入、业务不变量和图形化证据。
- 回答思路:从正常并发到未知结果、会话失效和旧持有者,最后验证业务终局。
- 口述答案:验收不能停在一百个线程最终计数正确。第一组验证基本配方:A/B/C 得到递增节点,只有最小节点进入,后继只监听前驱,正常释放后队列按序推进;获取超时和取消能删除自己的真实节点。第二组验证创建未知:服务端创建成功后丢弃响应,客户端按唯一请求前缀找回节点;重复创建时只保留最小候选,清理不会误删他人节点。第三组验证连接和会话:短断连后原会话恢复要重新验证,长暂停超过会话超时后新客户端接管,旧客户端恢复不得继续有效提交。第四组验证服务端故障:领导者切换、法定人数丢失、磁盘变慢和批量重连期间不会产生无界重试、永久等待或全体羊群。第五组验证业务不变量:WMS(仓储管理系统)库存并发扣减始终不负,支付重复回调只产生一条有效账务流水,Runner(执行器)旧代次结果被 Fencing Token(栅栏令牌)拒绝,对象存储只有一个正式指针。测试记录节点序号、会话、事件、业务版本和最终权威状态,不能只看客户端日志。容量测试覆盖峰值创建删除率、目录基数、等待高分位、连接数、服务端写延迟和恢复时间。最后做降级演练:锁不可用时效率锁能读旧值或限流,关键控制任务暂停而不是无锁运行。通过标准是重复尝试允许发生但业务只接受合法结果,节点最终可清理、队列可推进、告警可定位,而不是宣称所有失败场景都绝无并发。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:为什么线程并发测试不够?直接回答:它覆盖不了网络分区、创建未知、会话过期和外部副作用迟到。
- 追问 2:最关键的故障注入是什么?直接回答:旧持有者暂停超过会话超时,新持有者完成后再恢复旧者,验证围栏拒绝。
- 追问 3:如何验收降级?直接回答:锁服务不可用时关键不变量仍成立,非关键功能按预案限流、旧值或暂停。
- 关联专题:Fencing Token(栅栏令牌)与外部副作用裁决
- 问题(综合题):面试中如何回答“你会在什么场景选择 Zookeeper(分布式协调服务)锁”?
- 考点:决策框架、项目证据、替代方案和风险意识。
- 回答思路:先否定默认使用,再给适用场景、实现和业务兜底。
- 口述答案:我的结论是不会把 Zookeeper(分布式协调服务)锁作为所有分布式并发的默认方案。先判断能否不用锁:库存、订单和支付状态若能由数据库唯一键、条件更新、短事务或按键分区串行守住,就优先让正确性与权威数据同源。只有在低频控制面、参与者数量可控、希望有清晰顺序排队和会话失效语义时,我才会评估它,例如 Runner(执行器)主节点选举、仓库波次发布、盘点批次协调或对账主任务。实现上不手写简化算法,而使用 Apache Curator(Apache 协调客户端框架)成熟配方;每次申请带唯一请求前缀,创建结果未知时扫描恢复;等待者只监听前驱;获取设置截止时间和取消;连接暂停时停止新副作用,会话过期时永久放弃旧授权。最重要的是,锁只提供当前协调资格,任务结果还要携带 Fencing Token(栅栏令牌),数据库以版本条件拒绝旧持有者;业务动作有幂等键、状态机、检查点和对账。对缓存重建这类低延迟效率锁,我更可能选 Redis(远程字典服务)或请求合并;对库存扣减选数据库条件;对支付资金绝不让任何远程锁替代流水和账本。上线前会压测创建删除和等待高分位,演练未知创建、长暂停、网络分区、选主切换和降级;线上观察会话失效、目录基数、锁等待、接管与围栏拒绝。这样的回答既说明我理解配方,也表明我会从业务不变量、性能和失败预算做选型,而不是因为简历里写过组件就到处使用。 评审时我还会把资源权威、所有权证据、失效信号、未知结果、旧持有者拒绝、降级入口和人工恢复逐项写进故障矩阵;上线前用请求丢失、长暂停、网络分区和重复执行演练,验收协调状态最终收敛、业务不变量不被破坏、每个异常结果都有可查询的证据。
- 追问 1:最典型的不选场景是什么?直接回答:高频库存扣减和支付资金入账,优先数据库不变量、流水与状态机。
- 追问 2:最典型的选择场景是什么?直接回答:低频 Runner(执行器)选主、公平控制任务和可接受较高协调成本的场景。
- 追问 3:上线前必须做什么演练?直接回答:创建未知、会话过期、旧持有者恢复、法定人数故障和业务降级。
- 关联专题:三类锁选型
5. 复习清单
- 能从临时节点、顺序后缀、最小节点和会话四个条件解释锁所有权。
- 能用 A=
17、B=18、C=19演绎前驱监听、释放和异常跳过。 - 能解释创建成功响应丢失为什么是未知结果,以及唯一请求前缀如何恢复节点名。
- 能区分连接暂停、会话过期、新会话重建和旧进程恢复的业务动作。
- 能说明公平只是已入队节点的协调顺序,不是业务发起或完成时间绝对公平。
- 能解释可重入、读写、共享锁、信号量和领导者配方的资格判定差异。
- 能说明 Apache Curator(Apache 协调客户端框架)减少配方错误但不替业务保证终局正确。
- 能用具体
501/502数据说明 Fencing Token(栅栏令牌)必须由真正资源端原子校验。 - 能强制比较数据库锁、Redis(远程字典服务)锁和 Zookeeper(分布式协调服务)锁。
- 能把 WMS(仓储管理系统)库存、跨境物流、支付和 Runner(执行器)场景分别选到正确机制。
- 能设计未知创建、长暂停、旧持有者恢复、残留节点和羊群效应的故障演练。
- 始终记住:锁只提供协调条件,不保证外部副作用恰好一次。
