分布式锁、Session 与调度:租约失效后谁还有资格写
从租约、fencing token、锁续期、会话归属和调度唯一执行拆解共享状态协调,防止旧持有者恢复后继续写。
Redis 官方 Distributed Locks 要求唯一随机值与 compare-delete;它仍不替代目标资源对 fencing token 的校验。
拿到锁不等于整个临界区都拥有资格
客户端 A 获得 10 秒租约,随后发生长 GC 或网络暂停;租约到期后 B 获得锁并完成写入;A 恢复时仍以为自己在临界区,如果目标数据库不区分世代,A 会覆盖 B。锁服务无法撤回已经运行的线程,也不能让外部文件、数据库和第三方 API 自动知道租约失效。
锁必须有唯一 owner token,释放时 compare-delete 防止 A 删除 B 的新锁;但这只保护锁记录。每次成功获取还应得到单调 fencing token,目标资源保存已接受最大 token 并拒绝更小写入。若目标资源无法校验 token,只能把锁当降低冲突概率的优化,不能作为安全不变量唯一证据。
续期把失败检测问题带进客户端
看门狗续期要求客户端进程、事件循环、网络与锁服务在期限内工作。续期失败可能是锁已丢,也可能只是响应丢失;继续执行业务风险过期写,立即终止又可能中断已提交动作。临界区要短、可取消、写入带 token,并在接近租约截止时停止开始新步骤。
锁粒度过粗降低并发,过细增加 key、续期与死锁复杂度。多锁顺序必须固定,避免循环等待;超时不是自动解锁业务状态,已修改资源仍需事务或补偿。
Session 是有版本的安全状态
集中式 Session 允许任意实例读取,但每次请求增加远程依赖;粘性 Session 降低读取成本却让实例排空和故障转移复杂;自包含 token 减少服务端读取,却难以及时撤销。无论形态,都要定义 absolute expiry、idle expiry、security epoch 与并发设备策略。
用户修改密码或被禁用后,旧 token 不能只等自然过期。权威用户状态保存 securityVersion,敏感请求比较 token 版本;缓存失效存在陈旧窗口时,高风险操作直接读权威源或默认拒绝。Session 锁不能跨浏览器标签无限持有,也不能替代业务幂等。
调度的 exactly-once 常常只是唯一提交
多节点调度通过租约选 leader 或为每个 job 抢占执行权。节点在执行成功后、记录完成前崩溃会重跑;先记录完成再执行业务会丢任务。因此可靠调度选择至少一次触发,并让业务以 jobId + scheduledTime 幂等提交。
长任务租约需要续期和 epoch,旧 worker 恢复后提交必须被拒绝。misfire 策略要明确补跑一次、逐次补齐或跳过,避免停机恢复后瞬间执行数万历史任务。调度表记录 owner、epoch、attempt、deadline、result 与人工终止,才能解释唯一执行。
用证据边界拆开“不知道”和“做不到”
分布式进程只能看到本地状态、收到的消息与超时。超时说明在截止前没有证据,不说明对端未执行;租约说明在某个时钟假设下资格可能仍有效,不说明旧执行线程已停止;副本响应说明该副本拥有某版本,不说明全局没有更新。工程设计先列出每个结论需要什么证据,再决定证据不足时拒绝、等待、返回陈旧值还是进入补偿。
结果未知必须持久化 operationId、目标实体、预期版本和查询入口。自动重试只有在动作尚未开始或目标以相同 id 幂等时安全。用异常类名直接判断“肯定没执行”会在网络断点后制造重复提交。
状态机必须拥有终态、租约和人工出口
每个非终态都要有下一次唤醒来源:消息重投、租约到期、协调器扫描、对账任务或人工队列。只有状态而没有唤醒,流程会永久卡住;只有重试而没有终态,会永久自旋。
锁正确性和业务幂等必须同时存在
即使 fencing 阻止旧 worker,当前 worker 也可能在提交成功后丢响应并重试。目标表既要按 resource 保存 maxFence,又要按 operationId 保存结果;前者阻止旧世代,后者吸收同世代重复。两个约束解决不同失败窗口,不能互相替代。
锁服务切换、Session 存储故障和调度 leader 变更都要注入。验收不是同时只有一个线程打印日志,而是权威业务表只有一次有效状态迁移,所有旧 epoch 写有拒绝计数,过期 Session 在安全版本变化后无法继续高风险操作。
用两个 Java 状态模型固定分布式不变量
下面的 Java 17 模型不模拟真实网络或共识实现,而是把本篇关键版本、租约、状态和容量关系变成确定输出。随后用真实数据库、Broker、锁服务或多进程环境注入延迟、重复、分区与崩溃。
javac --release 17 -Xlint:all -Werror examples/backend-development/distributed-systems/locks-sessions-scheduling/FencingTokenDemo.java examples/backend-development/distributed-systems/locks-sessions-scheduling/LeaseSchedulingDemo.java
java -cp examples/backend-development/distributed-systems/locks-sessions-scheduling FencingTokenDemo
java -cp examples/backend-development/distributed-systems/locks-sessions-scheduling LeaseSchedulingDemoworkerA=41 workerB=42 staleWriteToken=41 authorityAccepted=false currentValue=from-B
job=settlement leaseOwner=node-B epoch=9 nodeAResumed=true nodeACommitAccepted=false executionsRecorded=1输出变化时必须解释哪条一致性、epoch、幂等或容量不变量被修改。真实实验还要验证结果未知、旧所有者恢复、状态清理和人工修复路径。
容量、观测与安全要围绕协调成本
同步协调增加往返、日志刷盘、锁持有和 quorum 等待;异步收敛增加积压、版本、去重和对账存储。容量模型至少包含峰值操作率、参与者数、每次尝试、超时窗口、重试放大、状态保留、复制倍数与恢复净速率。平均值无法覆盖分区热点和长尾暂停。
协调元数据同样是敏感控制面。锁 key、分片映射、事务状态、Session 与路由 epoch 需要最小权限、传输保护、审计和保留期。来自客户端的版本、token 或 shard hint 都不可信,权威服务必须验证签名、范围和当前状态。默认拒绝比在证据不足时猜测成功更适合余额、权限和唯一性。
反向实验要打在决定落盘的前后
故障注入必须有范围、自动停止和数据清理。生产演练从只读、单租户、单分片和低比例开始,保留旁路与回滚。完成标准不是组件重新可达,而是业务不变量保持、未知结果已对账、积压按预测下降且旧所有者无法继续写。
演进从缩小协调域开始
小规模优先单库本地事务、模块化单体和单一调度 owner。增长后先按业务冲突域分片、用批量与读模型减少跨域协调,再引入 Outbox、租约或 Saga。不要为了“分布式化”把一个本地不变量拆成跨服务事务。
任何新协调机制都写决策记录:解决什么不变量、网络分区时拒绝什么、权威证据在哪、超时后如何查询、状态保留多久、容量上限、故障演练与移除路径。系统复杂度只有在可验证收益覆盖恢复成本时才值得增加。
数据模型要为并发和恢复保留足够字段
替代方案要比较协调域而不是比较组件列表
团队门禁要阻止不可恢复的捷径
代码审查要求状态转换与 SQL 条件同时出现,集成测试要求在决定落盘前后杀进程,发布检查要求新旧版本互读,运行看板要求展示最老非终态而不只展示总数。分布式正确性不是某次设计评审的结论,而是持续被反例验证的工程属性。
