什么时候该拆、什么时候别拆?这篇把“建订单 + 扣库存 + 扣余额”做成五种实现,跑在 4 个独立的 MySQL 上,同样的失败、同样的订单,量出延迟、吞吐、对账结果和代码量,看拆分的代价到底落在哪里。
实现 | 延迟中位数 | p99 | 每个订单的提交次数 -----------------------------------+---------+---------+---------- 单体:一个库、一个本地事务 | 0.22 ms | 0.65 ms | 1.0 拆成三个库,各自提交,没有保护 | 0.45 ms | 1.03 ms | 3.0 拆成三个库 + XA(两阶段提交) | 0.82 ms | 1.97 ms | 3.0 拆成三个库 + Saga(状态表 + 补偿 + 重试) | 0.56 ms | 0.72 ms | 4.0 订单与库存合并成一个库,只有支付在另一个库(Saga 只剩支付一步) | 0.40 ms | 0.56 ms | 3.0
拆得越多,每单要多几次往返和提交;XA 的中位延迟约为单体的 3.7 倍(0.82 ms 对 0.22 ms 算出的比值),Saga 多一次状态表提交。
实现 | 订单/秒(3 轮中位数) | 3 轮范围 | 相对单体 -----------------------------------+--------------+-------------+------ 单体:一个库、一个本地事务 | 4901 | 4614 ~ 4958 | 1.00x 拆成三个库,各自提交,没有保护 | 3111 | 2626 ~ 3857 | 0.63x 拆成三个库 + XA(两阶段提交) | 1306 | 1294 ~ 1319 | 0.27x 拆成三个库 + Saga(状态表 + 补偿 + 重试) | 2982 | 2945 ~ 3020 | 0.61x 订单与库存合并成一个库,只有支付在另一个库(Saga 只剩支付一步) | 3846 | 2567 ~ 4008 | 0.78x
XA 并发只有单体的约四分之一,Saga 约六成;合并订单与库存的版本介于 Saga 与单体之间,但这一行抖动很大。
实现 | 下单成功 | 业务拒绝(余额不足) | 失败 / 卡住 | 库里有记录的订单 | 订单已确认却缺预留或扣款 | 预留或扣款泄漏(订单不是 CONFIRMED) | 停在 PENDING 的订单 -----------------------------------+------+------------+---------+----------+--------------+-------------------------+--------------- 单体:一个库、一个本地事务 | 1959 | 41 | 0 | 1959 | 0 | 0 | 0 拆成三个库,各自提交,没有保护 | 1851 | 37 | 112 | 2000 | 149 | 0 | 0 拆成三个库 + XA(两阶段提交) | 1959 | 41 | 0 | 1959 | 0 | 0 | 0 拆成三个库 + Saga(状态表 + 补偿 + 重试) | 1959 | 41 | 0 | 2000 | 0 | 0 | 0 订单与库存合并成一个库,只有支付在另一个库(Saga 只剩支付一步) | 1959 | 41 | 0 | 2000 | 0 | 0 | 0 Saga 里补偿重试用完仍没释放库存、停在“卡住”的:0 个(要靠恢复 / 人工;没有失败注入的恢复扫描能把它们收尾,见第 30 篇) 单体和 XA 失败时整体回滚,什么都不留;朴素拆分在中间步骤失败(临时失败、余额不足)时,前面已经提交的步骤没有人撤销;Saga 靠补偿把它们撤回,代价是中间状态对外可见一小段时间
朴素拆分留下 149 个对不上的订单;XA 靠整体回滚、Saga 靠补偿、合并版靠本地事务,对账都是 0,代价各不相同。
实现 | 有效代码行数(Python,本实验里的版本) | 额外依赖 ----------+------------------------+--------------------------------------------------------- 单体 | 14 | 0 个额外的表,1 个库 朴素拆分 | 10 | 3 个库,没有额外的表 XA | 29 | 3 个库 + 数据库对 XA 的支持(prepared 事务要有人收尾,见第 3、5 篇) Saga | 44 | 3 个库 + saga_log 表 + 重试 / 补偿逻辑;生产里还需要恢复守护进程和对账(第 30、16 篇) 订单 + 库存合并 | 24 | 2 个库 + 只剩支付一步需要重试 / 补偿 行数只是这几个函数在本实验里的写法,不是任何框架的真实工作量;但相对大小能说明:每多拆一个库,就要多写一段“这一步失败怎么办”
每多拆一个库就要多写一段“失败怎么办”。能放进同一个本地事务的,就不要拆开去补偿。