拆掉的不只是代码,还有事务
单体时代,下单扣库存是一条 @Transactional 里的事:SQL 都在一个连接里,要么全成、要么全回。服务拆开后,订单在订单服务、库存在库存服务,两个数据库、两个连接——本地事务的边界画不下去了。拆掉的不只是代码,还有事务的物理边界。
第一问:能不能不用分布式事务
成熟团队的第一反应不是上方案,而是拆需求:强一致的场景,尽量收敛到一个服务、一个库里(把库存扣减留在订单事务的同库边界的代价与收益,值得反复权衡);能接受最终一致的,改造成事件驱动——订单先落库,发"订单已创建"事件,库存服务异步扣减,失败重试或人工兜底。绝大多数跨服务场景,最终一致就够了,强一致是少数——先绕,绕不开再选方案。
Seata AT:一行注解的代价
AT 模式的卖点是对代码近乎零侵入:方法上加 @GlobalTransactional,框架接管跨服务的回滚。原理两句话:一阶段——各分支本地事务直接提交,同时记录前后镜像到 undo_log;二阶段——全局提交时异步删日志,回滚时按镜像反向补偿。代价藏在细节里:全局锁在事务期间持有,热点数据的并发写会被串行化;分支事务越短越好,长事务会拖死锁表;undo_log 表本身要维护清理。
TCC 与消息最终一致
TCC(Try-Confirm-Cancel)把控制权交给业务:Try 预留资源、Confirm 确认、Cancel 释放——性能好、不依赖锁,但每个参与方要写三个接口加空回滚、幂等、悬挂三大防护,成本最高,只留给资金核心链路。消息最终一致是最常用的折中:本地消息表保证业务与消息的原子性,MQ 投递给下游,消费方幂等——一致性窗口放宽到秒级,换来链路的解耦与削峰能力。
// 本地消息表:业务数据与消息同库同事务,原子性由本地事务保证
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 业务数据
outboxMapper.insert(new OutboxMsg("OrderCreated", toJson(order))); // 同事务写消息
}
// 独立投递器扫描 outbox 表发 MQ,成功后标记已发送
// 消费方按 orderId 做幂等,重复消息直接忽略决策树:按一致性要求与链路形态选
| 方案 | 一致性 | 侵入性 | 适合场景 |
|---|---|---|---|
| AT 模式 | 强(隔离期短) | 低 | 同链路多库,短事务 |
| TCC | 强 | 高 | 资金核心,高并发扣减 |
| 事务消息/本地消息表 | 最终 | 中低 | 跨域解耦,可异步 |
| Saga/状态机 | 最终 | 中 | 长流程,多参与方 |
一句话总结选型:能绕就绕(改设计),能异步就异步(最终一致),真要强一致再上 AT/TCC。分库分表之后的事务边界问题,在分片场景里还会再变一次形,原理相通。下一篇转向工程实践中最日常也最容易出事的环节:发布策略。
评论 (0)