连载中 16/20

分布式事务的微服务落位:Seata 与最终一致

2026-08-30 · 469 阅读 · 0 评论 · 0 赞

拆掉的不只是代码,还有事务

单体时代,下单扣库存是一条 @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。分库分表之后的事务边界问题,在分片场景里还会再变一次形,原理相通。下一篇转向工程实践中最日常也最容易出事的环节:发布策略。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#分布式事务#Seata#AT模式#本地消息表#最终一致

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞