实测本地事务的边界:同一个实例里余额不足会让订单和库存一起回滚;拆到三个 MySQL 实例后订单和库存已提交、钱没扣;kill -9 调用方进程会停在半截;5% 的单步失败率下 2000 单有 177 单(8.8%)不一致,同样的单放进一个事务是 0。
用两个真实的 PostgreSQL 实例和一个手写协调者实测两阶段提交:prepare 之后修改不可见、断开连接和 kill -9 后事务与行锁仍在;一次提交从 1 次变成 4 次 WAL fsync,中位数慢 6.9 倍;同一行上吞吐 = 1000/(hold + 3.3 ms)。
实测 2PC 里协调者在五个位置被 kill -9:参与者停在已承诺等决定的状态,行锁一直握着(同一行 UPDATE 等到超时)并挡住 VACUUM;参与者自己回滚会造成不一致;有决定日志可恢复,没有日志位置 4 不一致;先发 COMMIT PREPARED 再写日志也不一致。
用协议模型穷举实测 3PC 为什么很少用:没有分区时 2PC 在 7 个崩溃点里有 1 个阻塞、3PC 的 10 个都不阻塞;叠加网络分区后 2PC 的 35 个场景 17 个阻塞、0 个不一致,3PC 的 50 个场景 0 个阻塞、6 个不一致;3PC 还多 6 条消息、多一个往返。
实测 MySQL 的 XA 事务:状态机里 ACTIVE 不能 COMMIT、IDLE 不能写;prepare 之前断开连接会回滚、prepare 之后留在服务端;prepared 事务的行锁让别的写等到 1205 超时;kill -9 重启后 XA RECOVER 能看到、锁被恢复;跨两个实例的 XA 比普通事务多 5 次 redo fsync、中位数 3.4 倍。
用两个 MySQL 实例上的账户和库存服务实测 TCC:Try 把资源预留到冻结列、Confirm 转成已用、Cancel 退回;订单 A 在中间停 500 ms,同一行的订单 B 在 TCC 下 2.5 ms、在 XA 下 423.3 ms;检查和预留写成两条语句会多卖 15 件;崩溃订单留下的预留要靠 xid 记录才能安全释放。
实测 TCC 的三个坑:Cancel 先到而 Try 没执行(空回滚)会让朴素版多退 80;Cancel 先到、Try 后到(悬挂)让预留永久卡住;重复的 Confirm / Cancel 让 spent 记成 160。穷举 23 条消息顺序:朴素 14 条、宽容 15 条终态不对,带 tcc_log 防护的 0 条;防护让 Try 慢 2.2 倍,tcc_log 不能过早清理。
用订单、库存、支付三个服务实测 Saga 的两种写法:编排式一个编排器调用三个服务(21.3 ms),协同式用 RabbitMQ 事件接力(3 条消息);支付失败时补偿路径不同、终态相同;库存服务挂 2 秒编排式 0.63 s 取消、协同式 2.06 s 完成;加一个积分步骤,编排式改 1 行,协同式要改 2 个已有服务的订阅。
实测 Saga 补偿失败怎么办:被吞掉的补偿失败让 400 单里 9 单永久占着库存;指数退避重试 2 秒内 7 次调用(固定间隔 21 次),补偿不幂等会把库存加成 102;补偿不是回滚(手续费、短信撤不回);永久失败进人工队列 3 个;不可补偿步骤放前面 81 个订单发了通知又被取消。
实测 Saga 没有隔离性的后果:中间状态被别人读到(1007 次读里 506 次错误拒绝)、绝对值补偿覆盖并发更新(200 次里丢 90 次)、预留窗口里到达的 27 个买家全被拒;相对增量补偿把丢失更新降到 0,语义锁(pending 列)把错误拒绝降到 0。
实测双写的问题与 Outbox 的修复:kill -9 在两次操作之间,先写库再发消息丢事件、反过来出幽灵事件;1000 单 3% 失败率下 A 丢 34 个、B 幽灵 30 个,Outbox 丢 0、幽灵 0、重复 20 条;写 outbox 多 40% 的延迟,每行约 149 字节。
实测 Outbox 的两种发布方式:轮询的延迟约等于间隔的一半(10 ms 间隔 7.0 ms,1000 ms 间隔 552.7 ms),游标轮询会因 id 顺序不等于提交顺序丢 142 / 600 个事件,多个 relay 不加锁重复 17800 条,SKIP LOCKED 为 0;CDC 延迟 1.2 ms、不丢、按提交顺序,代价是复制槽保留 WAL(9 万个事件落后 37.2 MiB)。