性能与侵入的交易
下单链路要跨订单、库存、账户三个服务,XA 一个数量级的吞吐折扣付不起,DBA 那句「用业务改造来换性能」就是出路:TCC(Try-Confirm-Cancel)——把 2PC 的两阶段从数据库层抬到业务层,锁不再由数据库行锁担任,改用业务字段自己表达。本篇先把模型立起来,三大坑留给下一篇。
三个接口各管一段
每个参与者从「一个接口」扩成「三个接口」:
Try:预留资源——不直接完成业务,而是把资源圈起来。账户服务 Try:校验余额充足,冻结 100 元(balance 不动,frozen 加 100);库存服务 Try:预占库存(可售减一,预占加一);订单服务 Try:生成「处理中」订单。
Confirm:确认提交——全部 Try 成功后逐个确认,用预留的资源真正完成业务:frozen 减 100(钱正式扣掉)、预占转真实扣减、订单状态改为已创建。Confirm 不允许再失败——它该做的检查和资源,Try 阶段已经都保证好了。
Cancel:取消释放——任一 Try 失败就逐个取消,把圈走的资源放回去:frozen 减 100、预占库存退回可售、订单标记关闭。
扣款一单的完整时序
Try 账户: 校验余额,frozen +100 (可提现余额不变)
Try 库存: 预占 +1,可售 -1
Try 订单: 生成「处理中」订单
↓ 全部成功
Confirm 账户: frozen -100(钱正式划走)
Confirm 库存: 预占 -1(转真实扣减)
Confirm 订单: 状态改为「已创建」
↓ 任一 Try 失败
Cancel 各方: 解冻、退预占、关订单对照 2PC 看本质:Try 就是业务版的 prepare——都先把资源「握在手里」,但 2PC 握的是数据库行锁(锁着等全局指令),TCC 握的是业务字段里的冻结标记(不影响其他请求读余额、下别的单)。锁的粒度从「行」变成「该笔业务的部分资源」,阻塞消失,吞吐回来了。这就是业务改造换来的性能。
谁在当协调者
三阶段接口有了,谁来排时间表?事务管理器(框架层)负责:发起各参与者的 Try、收集结果、决定 Confirm 或 Cancel、处理超时、对失败的 Confirm/Cancel 持续重试。落地上可以用 Seata 的 TCC 模式(第 11 篇统一讲),也可以轻量自研——发起方一个状态表加定时任务,就是最朴素的协调者。纪律只有一条:Confirm 和 Cancel 必须设计成可重入——框架的重试没有任何一次保证只调一次,重试风暴是常态不是异常。
Try 的预留粒度怎么定
预留资源的形态因业务而异,老王总结了三种常见姿势:字段冻结(金额冻结、预占库存,最典型);状态占位(生成处理中订单、预创建记录);额度标记(外部系统额度预扣、券核销预占)。粒度设计的原则是 Try 要「够便宜、可释放」——预留本身不产生资损、不过期失效、Cancel 能一键还原。凡是 Try 里就开始真实扣减的,都是设计错了。
TCC 的得与失
| 维度 | 2PC/XA | TCC |
|---|---|---|
| 锁机制 | 数据库行锁,全局阻塞 | 业务冻结标记,无全局阻塞 |
| 吞吐 | 低(掉一个数量级) | 高(接近本地事务) |
| 业务侵入 | 无(改配置) | 每个参与者三个接口 |
| 一致性 | 强一致 | 最终一致(Confirm 前可见冻结态) |
| 开发成本 | 低 | 高(含幂等、防悬挂全套纪律) |
适用场景由此清晰:资源占用型、高并发、资金敏感的短事务——支付冻结、预占库存、额度预扣。长流程多环节的编排场景,TCC 三接口会膨胀成十几个,那是 Saga 的地盘(第 6 篇)。
小结
TCC 一句话:Try 预留、Confirm 确认、Cancel 释放,把 2PC 的锁从数据库行抬到业务字段,性能换侵入;Confirm 与 Cancel 天生要被重试,幂等纪律从写第一个接口起就要立。重试只是三大坑之一——空回滚、幂等、悬挂这三兄弟怎么防,下一篇逐个拆解。
评论 (0)