S52

分布式事务

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

进程把文件写好了,配额服务却没有记账;远端已经执行了扣减,本机等到的却是超时。这些问题有共同点:完成目标需要改变多个独立资源,而某个进程的退出码无法代表所有资源的最终状态。本课从文件发布、容量预留和网络调用出发,建立事务边界、失败恢复与补偿的基本判断方法。

1 分布式事务要解决什么

设想有个文件发布工具:用户在 shell 中提交归档包,工具先向配额服务预留 20 MiB,再把临时文件发布到目标目录,最后把任务标为可下载。配额记录在数据库里,文件由另一个进程管理。我们希望成功时文件可读且配额已扣,失败时文件不可见且配额归还。这里的困难不在于命令能否依次运行,而在于任何相邻动作之间都可能发生进程退出、主机重启或连接中断。

本地事务由单个事务管理器控制,例如数据库把配额扣减与预留记录放进同个事务,提交后同时生效。文件系统的 rename 在满足条件时可以原子地改变名称,但它不会顺便提交数据库事务。把数据库命令和 mv 放进同个 shell 脚本,也没有扩大原子性边界。set -e 可以让脚本遇到部分错误时停止,却无法撤销此前已经提交的数据库更新。

分布式事务关心的是:跨越多个独立提交边界后,如何使这些动作符合事先约定的整体结果。有的系统要求参与资源共同提交或共同放弃;有的系统允许暂时出现中间状态,再通过重试或补偿收敛。讨论方案前必须先说明不变量,也就是无论如何恢复都要守住的条件。例如同个任务最多持有 1 份有效预留,已经允许下载的文件必须有有效配额记录,归还量不能超过原来占用量。

进程、配额库与文件服务之间的事务边界
进程、配额库与文件服务之间的事务边界

网络增加了第 3 种结果:结果未知。调用成功表示收到了成功答复;明确拒绝表示对方报告没有执行或已经放弃;超时只表示期限内没收到答复。请求可能根本没到,也可能已经提交而答复丢失。此时直接换个任务号再试,会把原本的恢复问题变成重复执行问题。因此每次逻辑操作应有稳定的操作标识,重试时沿用它,并提供按该标识查询结果的接口。

事务也不会自动替我们定义外部可见性。临时文件可以先存在,但下载接口只承认状态为 READY 的任务;配额可能已经预留,但界面应标成处理中。若用户能绕过接口直接遍历目标目录,这个设计的可见性假设就不成立。所谓整体正确,必须同时覆盖数据库记录、实际资源和访问入口,而不能只检查某张表里的成功标志。

这类问题不要求参与者位于不同城市。即便所有进程都在同台机器上,只要数据库与文件系统分别提交,也会出现类似窗口。反过来,跨进程调用若最终只修改同个数据库,并能由同个本地事务完成,就未必需要引入分布式提交。先画出谁能原子提交什么,再选择机制,通常比先给架构贴名称更有效。

2 两阶段提交与超时边界

两阶段提交常记为 2PC。它安排协调者询问所有参与者能否提交,再统一决定提交还是放弃。第 1 阶段是准备:参与者完成约束检查,持久化必要的恢复信息,并保留履行承诺所需的资源,然后回答准备成功或失败。准备成功意味着参与者承诺:之后即使进程重启,也能够按照全局决定完成提交或放弃,而不只是“当前看起来可以”。

第 2 阶段是决定。只有全部必要参与者都准备成功,协调者才可以形成提交决定;任意参与者拒绝,都不能把这次事务当成共同提交。协调者需要把决定可靠记录下来,再通知参与者。通知和确认可能需要重复发送,参与者也必须能重复处理同个决定。这里有多份持久记录,不能用协调进程内的布尔变量代替。

两阶段提交中的准备、决定与结果未知
两阶段提交中的准备、决定与结果未知

准备和提交之间存在关键边界:已回答准备成功的参与者,通常不能仅凭自己的超时就随意放弃。它没有收到提交通知,不代表协调者没有记录提交决定。可能另个参与者已经提交,只是自己的通知在网络中丢失。此时自行回滚会破坏共同决定。它需要查询权威的恢复信息,或等待协议允许的恢复流程;基础 2PC 在某些故障下可能阻塞。

协调者在形成决定前,可以根据协议与已知状态选择放弃;形成并持久化提交决定后,就不能因为用户页面超时而把它改成放弃。用户等待期限、网络请求期限和事务协议期限是不同概念。页面可以先返回“处理中”,后台继续恢复。把客户端断开连接解释成事务回滚,会让业务状态依赖网络偶然性。

观察动画时,特别留意“参与者已准备,但尚未收到决定”的状态。此时文件服务的进程即使活着,相关资源也可能仍被占用。对于数据库,这可能表现为锁等待或连接占用;对于支持预留的资源管理器,可能表现为可用容量减少。事务耗时不仅包含真正执行更新的时间,还包含等待慢参与者、决定落盘以及故障恢复的时间。

普通文件系统操作不会因为应用写了“prepare”日志就自动成为 2PC 参与者。参与者必须提供可恢复的准备状态,并能兑现后续决定;普通 HTTP 接口也未必支持这样的协议。把 POST /reserve 和 mv 各执行 1 次,再补写提交标记,只是应用组织了多个动作,不能据此宣称已经实现原子的 2PC。选用它之前,需要确认每个资源是否真正支持相应接口和恢复语义。

可以用 3 个时间点训练判断:准备答复之前失败,协调者可以按协议放弃;准备成功之后、决定未知时失败,参与者需要恢复并查明决定;提交决定已经持久化之后通知失败,需要重发提交。对初学者而言,先掌握这些边界已经足够。协调者自身如何容错、日志如何复制,留给后续课程,不必为了理解超时就提前展开内部实现。

3 TCC、Saga 与本地消息表

如果资源不支持共同提交,应用可以显式设计中间状态。TCC 把操作分成 Try、Confirm、Cancel。以配额为例,Try 为任务预留 20 MiB,Confirm 把预留转成正式占用,Cancel 释放预留。Try 应当保证后续确认所需的业务条件,而不能只是查询剩余容量;查询后到确认前,容量可能已经被别的任务抢走。预留记录既是资源占用,也是恢复时查找的依据。

Confirm 和 Cancel 都要能重试。确认已经完成时,再次确认应返回已有结果;取消已经完成时,再次取消不能继续增加容量。还要规定乱序行为:取消先到而预留尚未到时,是否记录取消标记;迟到的 Try 是否必须拒绝;确认与取消并发时,哪个状态迁移可以成功。这些约束需要由状态记录、唯一约束或条件更新落实,不能只写成接口注释。具体并发算法可以后学,但接口契约现在就要说明。

Saga 将长流程拆成多个已经提交的本地步骤,并为需要撤回的步骤定义补偿。例如先预留配额,再上传临时文件,最后发布下载入口;发布失败时,删除未发布文件并归还配额。补偿是新的业务动作,数据库历史可能同时保留原操作和补偿记录。它通常不能让外界忘记曾经发生过什么:通知已经被看到,下载已经开始,都不能靠更新状态恢复成从未发生。

预留确认与顺序补偿的两种组织方式
预留确认与顺序补偿的两种组织方式

Saga 的步骤顺序应考虑可撤销程度。先做可重试的准备,把对外暴露或难以撤销的动作尽量后移。若某步无法自动补偿,就应在设计时确定人工处理入口、暂停状态和责任人。不能仅仅在失败分支打印“稍后重试”,就认为最终一定会一致;需要有人或某个工作进程持续扫描未完成任务,并有明确的终止条件。

本地消息表解决另一类常见缺口:数据库更新已提交,进程却在发送消息前退出。做法是在同个本地事务里写业务变化和待发送消息,例如把任务改为 READY 的同时插入“建立下载索引”的消息。独立发送进程扫描消息表,发送后标记进度。这样,业务提交就意味着发送意图也已持久存在,进程重启后仍能继续投递。

不过,发送成功后、标记成功前仍可能崩溃,因此消息可能重复。消费者应以消息标识或业务操作标识,在自己的本地事务中把去重记录与效果一起提交。如果先记录“处理过”,随后才执行效果,中间崩溃可能永久漏做;反过来先执行效果再记标识,则可能重复做。本地消息表也不能让文件写入与数据库更新自动原子化,它保护的是同个数据库内的更新和发送意图。

这 3 种办法可按问题选择:可以预留资源时考虑 TCC,已经提交的长流程需要业务撤回时考虑 Saga,需要可靠传播本地已提交事实时考虑消息表。实际系统可以组合使用,例如 Saga 的某步通过本地消息表发布结果。组合后仍要逐项回答谁负责重试、哪个状态是事实来源、重复消息如何处理,而不是把机制名称当作正确性的证明。

4 动手:演练一次补偿路径

下面用本机 shell、SQLite 和普通文件操作演练配额补偿。需要 sqlite3 命令行工具。这里没有实现网络服务,也没有实现完整 Saga 调度器;数据库与文件系统的独立提交,足以呈现失败窗口。所有文件写进 mktemp -d 创建的临时目录,任务号固定为 job-52,便于观察同个任务的重复恢复。配额单位用 MiB,示例只发布短文本,不测真实文件大小。

先创建已经完成预留的状态:总配额 100,可用配额 80,任务持有 20。配额扣减与预留记录在同个本地事务中提交。另个数据库保存流程状态,用来表示协调者的独立持久记录。实际服务还应防止同个初始化请求重复扣减,这段初始化只执行 1 次。

work=$(mktemp -d)
cd "$work" || exit 1
sqlite3 quota.db 'CREATE TABLE quota(free_mib INTEGER NOT NULL);
CREATE TABLE reservation(job TEXT PRIMARY KEY, amount INTEGER NOT NULL,
state TEXT NOT NULL);
BEGIN;
INSERT INTO quota VALUES(100);
UPDATE quota SET free_mib=free_mib-20 WHERE free_mib>=20;
INSERT INTO reservation VALUES("job-52",20,"HELD");
COMMIT;'
sqlite3 workflow.db 'CREATE TABLE job(id TEXT PRIMARY KEY,state TEXT NOT NULL);
INSERT INTO job VALUES("job-52","PUBLISH_PENDING");'
printf '%s\n' 'demo payload' > staged.txt
printf '%s\n' 'this is a file' > blocked

blocked 是普通文件,不能成为目录。把临时文件移动到它下面会明确失败。用 if 捕获失败,再持久记录需要补偿的状态。这里故意选择能确定未发布的本地错误;真实网络调用若超时,则必须先按任务号查询发布结果,不能直接假定发布失败。

if mv staged.txt blocked/result.txt; then
  sqlite3 workflow.db 'UPDATE job SET state="READY" WHERE id="job-52";'
else
  sqlite3 workflow.db 'UPDATE job SET state="COMPENSATION_PENDING"
  WHERE id="job-52";'
fi
sqlite3 workflow.db 'SELECT id,state FROM job;'

现在模拟补偿工作进程收到 2 次相同任务。每次都先用条件更新将 HELD 改为 RELEASED,只有这条状态迁移确实改变了 1 行,才归还原记录里的配额。changes() 紧接着读取前条更新的影响行数;这段 SQL 中间不能插入其他会改变其含义的写语句。状态迁移与归还在同个事务内,重复执行会成为无效果操作。

for pass in 1 2; do
  sqlite3 quota.db 'BEGIN IMMEDIATE;
  UPDATE reservation SET state="RELEASED"
  WHERE job="job-52" AND state="HELD";
  UPDATE quota SET free_mib=free_mib+
    (SELECT amount FROM reservation WHERE job="job-52")
  WHERE changes()=1;
  COMMIT;'
  sqlite3 quota.db 'SELECT free_mib FROM quota;
  SELECT job,state FROM reservation;'
done
sqlite3 workflow.db 'UPDATE job SET state="COMPENSATED" WHERE id="job-52";'

每轮都应看到可用配额为 100,预留状态为 RELEASED。若第 1 轮提交后进程退出,流程库可能仍显示待补偿;恢复时再执行同个补偿,再推进流程状态即可。若补偿数据库命令失败,则应保留待补偿状态,不能提前宣告完成。练习结束后可保留临时目录检查,不要把这里的固定任务号复制进长期运行的脚本。

练习还暴露了另个窗口:文件发布成功后,流程状态更新前也可能退出。生产方案要能通过稳定路径、任务标识和文件元数据查询实际结果,再决定继续确认还是撤回。仅凭流程库中仍是 PUBLISH_PENDING 就删除文件,可能撤销已经对外可见的成功。补偿路径和正常路径一样,需要可恢复的状态判断。

5 原子性与长事务成本取舍

强调整体原子性,有助于减少应用暴露的中间状态,但代价往往由资源持有时间承担。配额预留需要占住容量,数据库锁可能阻塞其他更新,工作连接也可能无法及时归还。如果事务期间还等待大文件上传、人工确认或远端服务恢复,资源占用时间就从毫秒扩大到秒甚至更长。事务只是做了很少的计算,也可能拖住大量后续请求。

用简单关系估算:稳定负载下,平均在途数量约等于到达率乘以平均停留时间。每秒到达 80 个任务,平均占用连接 0.05 秒,约有 4 个任务在途;占用变成 2 秒时,约为 160 个。若连接池只有 32 个,后一种需求无法持续满足,任务会排队或被拒绝。这是容量直觉,不能把它当作包含锁冲突、突发流量和失败重试的完整性能模型。

等待时间如何转化为连接占用和排队
等待时间如何转化为连接占用和排队

滑块把到达率和持有时间分开,是为了说明降低单次计算量并不必然消除瓶颈。若连接一直在等网络,优化 SQL 的微小执行时间可能作用有限。缩小事务边界、把耗时上传移到预留之外、为并发设置上限,都可能更直接。但缩短预留时间又要求明确到期行为:不能在任务仍可能确认成功时,只靠定时清理就把容量分配给别人。

允许中间状态也有成本。Saga 需要流程记录、补偿代码、重试队列和人工处理;本地消息表需要发送进程、积压监控和消费者去重。最终一致性不等于可以不保存状态,而是允许系统先承认处理中,再在约定时间和条件下收敛。用户能否在中间状态读取、下载或取消,都应有具体规则,否则技术上正在恢复,产品上却可能已经给出错误承诺。

还有些不变量不能靠事后补偿满足。例如容量不足时绝不能接受新的正式占用,就需要在权威配额记录处做原子条件检查。可以把这个严格约束留在短小的本地事务内,让文件传输和索引更新异步进行。这样分配责任比把所有步骤塞进同个长事务更清楚:关键资源的合法性同步保证,派生状态通过恢复流程追上。

选择时可以比较失败后果、允许等待时间、资源能力和恢复负担。若失败可以明确撤回,并且处理中状态可接受,补偿方案可能合适;若不允许部分提交,且所有参与资源确实支持共同提交,才进一步评估 2PC。没有免费方案,目标是把必要的严格性放在正确边界内,而不是追求名称上的“最强”。

6 常见坑:跨库强事务拖垮吞吐

某文件管理服务把配额更新、文件发布确认和检索索引写入放进跨库强事务。平时看起来很整齐:接口成功就代表所有地方都完成。后来索引库出现磁盘延迟,准备或提交答复变慢,事务开始长时间占用连接。配额库本身执行很快,却也因为等待整体决定而延长资源持有时间。于是原本不依赖索引查询的上传任务也被阻塞。

常见误判是看到数据库连接用满,就立即扩容连接池。连接增加可能让更多请求进入同个争用点,锁等待、磁盘请求和恢复压力一起增加。另个误判是统一缩短客户端超时。客户端更快放弃,不会自动释放所有已经准备的事务;如果重试还生成新任务号,系统甚至在为同个用户意图执行多份工作。超时参数必须与服务端取消和恢复语义一起讨论。

排查先建立同个操作的时间线:接收请求、取得连接、进入准备、形成决定、收到参与者确认、返回客户端。日志中应包含操作标识、参与资源、当前阶段、重试次数和耗时,但不要写入文件内容或凭证。把连接等待与执行耗时分开,才能区分“SQL 很慢”和“根本还没拿到连接”。平均响应时间还可能掩盖少量长期挂起任务,因此也要观察高分位耗时与最老未完成任务的年龄。

从操作标识关联流程、网络与资源观测
从操作标识关联流程、网络与资源观测

在 Linux 主机上,ps -o pid,stat,etime,cmd -p PID 能帮助确认进程是否仍在运行以及已运行多久;ss -tanp 可以查看可见权限范围内的 TCP 连接;df -h 与 df -i 分别检查空间和 inode 是否耗尽。它们能提供进程、连接或文件系统证据,却不能证明业务已经提交。看见连接为 ESTAB,只说明连接状态,不能替代按操作标识查询事务决定。

处理时先限制新增在途数量,防止积压继续扩大;然后定位慢参与者,按照恢复规程处理已有事务。不要把“重启所有服务”当作默认回滚手段,重启后的参与者仍可能需要恢复准备状态。若索引仅服务搜索展示,可以重新评估它是否必须进入严格提交边界:主记录提交后通过消息表更新索引,搜索短暂落后,而下载授权仍查询权威记录。

这个调整需要同时改接口承诺和观测指标。不能继续承诺接口返回后立刻能从索引搜到文件,却在内部悄悄改成异步。应明确预计可见延迟、积压告警、补建索引的方法,以及主记录与索引不一致时哪个来源优先。吞吐优化只有保持业务契约清晰,才算真正完成。

7 案例:补偿未幂等造成重复扣款

设想计算节点启动临时计算进程,需要向计费账本登记预扣费用。主流程用任务号 run-52 登记预扣 30,随后执行节点准备失败,进入撤销流程。账本为保留审计历史,采用追加分录:原预扣分录保留,补偿时追加方向相反的撤销分录。这里的“撤销”仍是独立提交的账本操作,不会让原分录从历史上消失。

故障发生在补偿答复丢失之后。工作进程已经提交撤销分录,却没有收到成功响应,于是再次请求撤销。错误实现给每次请求生成新的分录标识,只检查请求格式,不检查原操作是否已撤销。结果同笔预扣被撤销 2 次。恢复程序随后发现余额不符,又按每条撤销记录发起重新扣回;其中 1 次扣回的答复再次丢失,重试又追加扣回分录,最终出现重复扣款投诉。

问题不是网络不够可靠,而是把每次尝试都当成新的业务操作。原操作、补偿、补偿重试之间没有稳定关联,导致自动修复不断制造新账务事实。若只是给调用层加上“最多重试 3 次”,只能限制损失次数,不能保证正确。把失败请求全部禁止重试,又会让真实未执行的撤销永久丢失。

补偿答复丢失后的错误重试与正确恢复
补偿答复丢失后的错误重试与正确恢复

修正首先明确业务身份:原预扣标识固定,撤销操作标识可以由原预扣标识和撤销类型确定。账本通过唯一约束限制同个原操作只能有 1 次有效撤销,并把撤销分录与原操作状态迁移放在同个本地事务中。重复请求应返回已有撤销结果,若金额或对象与首次请求不符,则拒绝并告警,不能把冲突参数当作成功重放。

其次区分“已执行”“明确未执行”和“未知”。答复丢失时,协调者以稳定标识查询账本;查询仍失败,就保持待恢复状态。补偿接口本身应允许安全重试,查询只是帮助尽快收敛,不能成为弥补非幂等写入的唯一措施。因为查询得到未完成之后,原请求仍可能迟到,下一次写入依旧需要由账本的原子约束裁决。

最后检查历史数据。修复代码不会自动纠正已经存在的重复分录。需要按原操作关联所有分录,核对有效扣减与有效撤销,形成待核实清单,并通过受控的调整分录处理差额。审计记录应保留原因和关联标识,不能直接删掉看起来多余的记录。对于文件配额,同样应核对有效预留、已发布文件与释放记录,而不是只把某个数字改回预期值。

这个案例说明,幂等关注的是重复请求产生的业务效果。单纯缓存某个 HTTP 响应,或给工作进程加互斥锁,都不能覆盖进程重启、多个恢复实例并行和缓存过期。真正的保护应放在资源拥有者能够原子更新的地方,并覆盖正常操作与补偿操作。

8 把事务边界写进业务方案

设计文档应先列资源和事实来源。文件发布场景中,配额库决定有效预留,文件服务决定对象是否存在,流程库决定当前恢复进度,下载入口决定哪些对象对用户可见。它们的职责不能互相替代:流程显示成功而文件不存在,仍然是故障;文件存在而未通过发布状态,也不应自动获得下载权限。把每个判断对应到具体接口,才能在故障时查到答案。

接着写正常路径和可见性边界。可以约定先以任务号创建预留,再上传到不可下载的临时位置,校验成功后请求发布,最后确认占用并开放入口。这里还必须处理发布与确认之间的窗口,例如发布只改变内部对象状态,下载入口仍以有效占用和任务状态共同判断。若基础设施无法提供这样的访问控制,应调整顺序或降低承诺,不能靠文档假设绕过。

业务不变量、资源边界与恢复责任
业务不变量、资源边界与恢复责任

失败路径建议按切断点书写。预留答复未知时,查询或重试同个预留;上传明确失败时,进入释放;发布结果未知时,先确认是否已经发布;补偿失败时,持久记录待补偿并继续恢复。每条路径写明下一步由哪个进程执行、多久再试、在哪里查状态,以及何时需要人工处理。只有“失败则回滚”这句话,无法指导已经提交的资源如何恢复。

状态字段也需要转移条件。例如只有 HELD 可以进入 CONFIRMED 或 RELEASED,已经释放的预留不能再次确认;流程只有观察到资源端释放成功,才能进入 COMPENSATED。取消与确认并发时,由资源拥有者的条件更新决定哪个成功,协调者再根据实际结果推进。不要让多个进程分别读到旧状态后,各自无条件写入相互冲突的新状态。

可观测性应围绕恢复建立。除了请求成功率,还应统计准备中数量、待补偿数量、消息表积压、重复请求命中次数和最老未完成任务年龄。待恢复任务长期为零可能表示系统健康,也可能表示扫描进程已经停止,所以还需记录扫描心跳与最近成功推进时间。日志、指标和查询接口共同回答“哪个任务卡在哪里”,而非只回答“服务是否活着”。

验收时选择具体故障窗口:预留提交后终止调用进程、发布成功后丢弃答复、补偿提交后再次投递任务。预期应写成可核对的不变量:有效预留不重复,已开放文件有占用记录,重复补偿不多归还,未知任务最终得到确定状态。故障注入只在隔离环境进行,并保留操作标识与资源记录。这样才能把“支持重试”变成可检验的工程承诺。

9 从分布式事务走向复制与分片(S53)

本课把资源视为若干独立参与者,下一课会继续追问:这些参与者内部的数据又如何存放?复制让多个副本保存相关数据,以支持可用性和读取需求;分片把不同数据分到不同节点,以分担容量和访问压力。它们改变数据所在的位置以及访问路径,也会改变事务需要跨越的边界,但不会自动消除事务问题。

例如把配额记录复制到另个节点,刚完成释放后,读取落后的副本可能仍看到 HELD。如果恢复进程据此决定再次执行补偿,接口仍必须通过稳定标识和原子约束防止多次归还。对决定下一次写入的关键查询,还需要明确读取结果是否足够新,以及是否应向权威写入端查询。只说“已经上了复制”不能回答这些问题。

分片则会改变本地事务范围。若用户配额和该用户的预留记录放在同个分片,配额扣减与预留创建可能仍能由本地事务保护;若随意把它们分开,就可能需要跨分片协调。分片键因此不只是负载均衡参数,也与常见不变量、查询方式和事务边界有关。不过,把所有关联数据集中到某个分片也可能产生热点,需要在下一课继续比较。

复制的确认规则与 2PC 的准备承诺虽然都会出现等待和确认,却解决不同问题:前者关注数据副本如何形成可用的记录,后者关注独立参与者如何遵循共同决定。不要把“收到了多个副本答复”直接解释为“多个业务资源已经原子提交”。实际系统可能组合这些机制,本课只要求分清各自承诺的对象。

带着文件发布例子进入 S53,可以继续检查 3 个问题:任务状态写入后从哪个副本读取;同个用户的配额与预留如何放置;节点不可达时谁能接受新的写入。无论后续采用什么复制和分片方式,稳定操作标识、清楚的可见性边界、可恢复的中间状态,以及资源端的幂等约束,仍然是应用层必须保留的基础。

放进方案里

文件发布方案可写成如下约定:配额库是有效占用的事实来源,文件服务保存临时对象与发布结果,流程库保存恢复进度。每次发布使用稳定任务标识,网络重试沿用该标识。配额扣减与预留创建在同个本地事务内完成;文件传输在该事务之外执行。下载入口只允许状态满足发布条件且拥有有效占用的对象,临时文件不对外开放。

正常路径为预留、上传校验、内部发布、确认占用、开放下载。预留接口提供确认、取消和结果查询;条件更新保证同份预留不能既确认又释放。发布接口按任务标识返回已有结果。遇到超时先记录结果未知,通过查询或同标识重试恢复,禁止仅凭超时再次创建独立任务。明确上传失败后,流程进入待补偿,删除未发布对象并释放预留。

补偿由后台工作进程推进,任务及进度持久保存。释放状态迁移与配额归还在同个事务内完成,重复执行返回既有结果。提交后答复丢失时,重试不会增加归还量;流程只有确认资源端已释放才标记补偿完成。对外通知通过本地消息表发送,消费者以消息标识去重,接受可能重复投递的事实。

运行时限制并发在途任务,监控待补偿数量、消息积压、最老任务年龄和扫描心跳。超过约定恢复时限的任务进入人工核对队列,保留原操作、补偿和调整记录。上线前演练预留后退出、发布答复丢失及重复补偿,逐项核对配额与文件可见性。检索索引允许短暂延迟,不进入严格占用边界;其延迟目标和重建责任在接口说明中单独列明。

10 小结:下一步复制与分片(S53)

分布式事务首先是边界与恢复问题。2PC 依赖真实的准备承诺和持久决定;TCC 依赖可确认、可取消的预留;Saga 依赖业务补偿;本地消息表保护本地更新与发送意图。超时意味着可能未知,重试安全依赖稳定标识及资源端的原子约束。

自测时,尝试不用“失败就回滚”回答这些问题:文件已经发布但答复丢失,该查询什么?参与者已经准备却没有收到决定,为什么不能自行放弃?补偿提交后进程退出,再次执行如何避免重复归还?下一课进入复制与分片,继续观察数据位置和读取路径如何改变这些判断。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务