当前位置: 首页 > news >正文

分布式事务到底怎么解决?本地消息表、TCC、Saga、Seata 一次讲清楚

一、前言

在单体项目中,我们经常用@Transactional来保证事务一致性。

比如创建订单时,同时写订单表、扣库存、写支付流水,只要这些操作都在同一个数据库里,一个本地事务基本就能解决问题。

但是到了微服务架构,事情就没这么简单了。

一个下单流程可能会拆成多个服务:

  • 订单服务:创建订单
  • 库存服务:扣减库存
  • 账户服务:扣减余额
  • 优惠券服务:核销优惠券
  • 积分服务:增加积分

这些服务可能有各自的数据库。订单服务的本地事务,只能保证订单库的数据一致,没办法直接保证库存库、账户库、优惠券库一起成功或一起失败。

这就是分布式事务问题。

本文就结合 Java 后端常见业务场景,把分布式事务的几种主流方案讲清楚:

  • 本地消息表
  • 可靠消息最终一致性
  • TCC
  • Saga
  • Seata

二、分布式事务到底解决什么问题?

先看一个下单场景。

用户提交订单后,系统需要做三件事:

1. 创建订单 2. 扣减库存 3. 扣减账户余额

如果这三个操作都在一个数据库中,可以直接使用本地事务:

@TransactionalpublicvoidcreateOrder(){orderMapper.insert(order);stockMapper.deduct(productId);accountMapper.deduct(userId,amount);}

只要中间任何一步失败,事务回滚即可。

但如果拆成微服务:

订单服务 -> 订单库 库存服务 -> 库存库 账户服务 -> 账户库

订单服务调用库存服务成功后,如果调用账户服务失败,会发生什么?

订单创建成功 库存扣减成功 账户扣减失败

此时数据就不一致了。

分布式事务要解决的,就是这种跨服务、跨数据库、跨资源操作时的数据一致性问题。


三、先明确:不是所有场景都需要强一致

很多人一听到分布式事务,就马上想到“我要保证所有服务同时成功或同时失败”。

但真实项目里,大部分业务并不需要强一致,而是可以接受最终一致。

比如积分增加:

用户支付成功后,积分晚几秒到账,一般可以接受。

比如短信通知:

订单创建成功后,短信发送失败,不应该影响订单创建。

比如优惠券使用:

如果优惠券核销失败,可以让订单创建失败; 也可以先创建待确认订单,再异步确认优惠券状态。

所以做分布式事务设计前,先问自己一个问题:

这个业务到底要求强一致,还是最终一致就可以?

如果最终一致可以满足,就不要轻易引入复杂的强一致方案。


四、方案一:本地消息表

1. 什么是本地消息表?

本地消息表是实际项目中非常常见的一种最终一致性方案。

核心思想是:

业务数据和消息记录放在同一个本地事务中提交。

比如订单创建成功后,需要通知库存服务扣库存。

订单服务不直接依赖 MQ 是否发送成功,而是在同一个事务中做两件事:

1. 写订单表 2. 写本地消息表

代码示例:

@TransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){Orderorder=newOrder();order.setUserId(command.getUserId());order.setProductId(command.getProductId());order.setStatus(OrderStatus.CREATED);orderMapper.insert(order);OutboxMessagemessage=newOutboxMessage();message.setBizId(order.getId());message.setTopic("order.created");message.setBody(JsonUtils.toJson(order));message.setStatus(MessageStatus.NEW);messageMapper.insert(message);}

只要本地事务提交成功,订单和消息记录就一定同时存在。

然后后台任务扫描消息表:

@Scheduled(fixedDelay=3000)publicvoidsendMessages(){List<OutboxMessage>messages=messageMapper.selectNewMessages();for(OutboxMessagemessage:messages){try{mqProducer.send(message.getTopic(),message.getBody());messageMapper.markSent(message.getId());}catch(Exceptione){messageMapper.increaseRetryCount(message.getId());}}}

这样即使 MQ 暂时不可用,也不会导致订单数据丢失。消息会留在本地表里,后续继续重试。

2. 本地消息表的优点

优点很明显:

  • 实现简单
  • 不强依赖分布式事务框架
  • 数据可查,方便排查问题
  • 失败后可以重试
  • 适合大部分最终一致场景

很多中小型系统,其实用本地消息表就够了。

3. 本地消息表的缺点

它也不是没有缺点:

  • 需要额外建消息表
  • 需要定时任务或消息投递任务
  • 消息可能重复发送
  • 消费端必须保证幂等
  • 消息表数据需要定期归档

尤其要注意:本地消息表只能保证消息不容易丢,不能保证消费者只执行一次。

所以消费端一定要做幂等。


五、方案二:可靠消息最终一致性

可靠消息最终一致性一般会结合 MQ 使用。

它的核心流程是:

1. 本地业务执行成功 2. 消息可靠发送到 MQ 3. 下游服务消费消息 4. 消费失败则重试 5. 多次失败进入死信队列或人工处理

以订单支付成功为例:

支付服务确认支付成功 -> 发送支付成功消息 -> 订单服务修改订单状态 -> 积分服务增加积分 -> 优惠券服务更新使用记录

消费者代码示例:

@RabbitListener(queues="pay.success.queue")publicvoidhandlePaySuccess(PaySuccessMessagemessage){StringmessageId=message.getMessageId();if(consumeLogMapper.exists(messageId)){return;}orderService.markPaid(message.getOrderId());consumeLogMapper.insert(messageId);}

这里有个重点:消费前先判断消息是否处理过。

因为 MQ 常见语义是:

至少投递一次

也就是说,消息可能重复。

所以消费端必须保证:

同一条消息消费多次,结果仍然正确。

这就是幂等。


六、方案三:TCC

1. 什么是 TCC?

TCC 是 Try、Confirm、Cancel 的缩写。

它把一个业务操作拆成三个阶段:

Try:尝试执行业务,预留资源 Confirm:确认执行业务,真正提交 Cancel:取消执行业务,释放资源

举个扣余额的例子。

Try 阶段不是直接扣钱,而是冻结余额:

publicvoidtryFreeze(LonguserId,BigDecimalamount){accountMapper.freeze(userId,amount);}

Confirm 阶段真正扣减冻结金额:

publicvoidconfirmDeduct(LonguserId,BigDecimalamount){accountMapper.deductFrozenAmount(userId,amount);}

Cancel 阶段释放冻结金额:

publicvoidcancelFreeze(LonguserId,BigDecimalamount){accountMapper.unfreeze(userId,amount);}

2. TCC 适合什么场景?

TCC 适合对一致性要求比较高,并且业务资源可以预留的场景。

比如:

  • 账户余额冻结
  • 库存预占
  • 优惠券锁定
  • 名额占用

这些业务都有一个共同点:

可以先冻结或预占,再确认或释放。

3. TCC 的缺点

TCC 最大的问题是业务侵入强。

每个业务接口都要写三套逻辑:

Try Confirm Cancel

而且还要处理:

  • 空回滚
  • 幂等
  • 悬挂
  • 重试

比如 Cancel 接口可能在 Try 还没执行成功时就被调用,这就是空回滚问题。

所以 TCC 虽然一致性强,但开发和维护成本也高。


七、方案四:Saga

Saga 更适合长流程事务。

它的思想是:

把一个大事务拆成多个本地事务。 每个本地事务都有一个对应的补偿动作。

比如订单履约流程:

1. 创建订单 2. 扣减库存 3. 安排出库 4. 生成物流单 5. 通知用户

如果第 4 步失败,可以执行补偿:

取消物流单 取消出库任务 释放库存 关闭订单

Saga 不追求数据库层面的回滚,而是通过业务补偿让系统最终回到可接受状态。

它适合:

  • 订单履约
  • 审批流程
  • 跨系统结算
  • 长时间业务流程

Saga 的难点在于补偿设计。

因为很多业务不是简单反向操作。

比如用户已经收到短信通知,再“回滚短信”就不现实,只能发一条新的通知解释状态变化。


八、方案五:Seata

Seata 是常见的分布式事务框架。

它有几种模式:

  • AT 模式
  • TCC 模式
  • Saga 模式
  • XA 模式

Java 项目里最常见的是 AT 模式。

1. Seata AT 模式大致原理

AT 模式对业务代码侵入较小。

它会在本地事务执行前后记录 undo log,用于全局回滚。

大致流程:

1. 开启全局事务 2. 各服务执行本地事务 3. Seata 记录 undo log 4. 所有分支成功,全局提交 5. 任一分支失败,根据 undo log 回滚

示例:

@GlobalTransactionalpublicvoidcreateOrder(CreateOrderCommandcommand){orderService.create(command);stockService.deduct(command.getProductId());accountService.deduct(command.getUserId(),command.getAmount());}

代码看起来很简单,但背后有全局事务协调器、分支事务、undo log、全局锁等机制。

2. Seata 的优点

  • 对业务代码侵入小
  • 使用方式接近本地事务
  • 适合快速接入分布式事务
  • 社区资料多

3. Seata 的问题

Seata 不是银弹。

它也有一些成本:

  • 引入事务协调器
  • 增加 undo log 表
  • 高并发下可能有全局锁竞争
  • 对 SQL 类型有要求
  • 故障排查复杂度更高

如果你的业务本来最终一致就能接受,强行上 Seata 反而可能增加系统复杂度。


九、几种方案怎么选?

可以简单按下面思路选。

1. 最终一致即可

优先考虑:

本地消息表 + MQ + 消费幂等

适合:

  • 积分发放
  • 消息通知
  • 数据同步
  • 支付成功后异步更新下游状态

2. 需要资源预留

可以考虑:

TCC

适合:

  • 冻结余额
  • 预占库存
  • 锁定优惠券

3. 长业务流程

可以考虑:

Saga

适合:

  • 订单履约
  • 审批流程
  • 跨系统业务编排

4. 想降低接入成本

可以考虑:

Seata AT

适合:

  • 关系型数据库
  • SQL 不太复杂
  • 并发压力不是特别夸张
  • 团队能接受引入事务协调器

十、实际项目中的建议

我个人更推荐的落地顺序是:

先业务规避 再最终一致 再 TCC / Saga 最后才考虑强一致框架

不要一上来就追求“绝对一致”。

真实系统更重要的是:

  • 状态可追踪
  • 操作可重试
  • 接口幂等
  • 失败可补偿
  • 数据可对账
  • 异常可人工处理

比如订单系统可以设计成状态机:

待支付 -> 已支付 -> 待发货 -> 已发货 -> 已完成

每次状态流转都带上当前状态条件:

updateorderssetstatus='PAID'whereid=#{orderId}andstatus='WAIT_PAY';

这样即使消息重复、接口重复调用,也不会把状态改乱。


十一、常见坑

1. 没有幂等

分布式系统里,重试是常态。

只要有重试,就可能重复执行。

所以接口必须考虑幂等。

2. 没有补偿

失败不可怕,可怕的是失败后没有修复机制。

比如扣库存失败后,要么重试,要么关闭订单,要么进入人工处理。

3. 没有对账

最终一致不代表永远不管。

核心业务必须有对账任务,比如:

支付成功但订单未支付 订单已支付但库存未扣 库存已扣但订单不存在

这些异常数据要能被定期发现。

4. 过度依赖框架

框架只能帮你处理一部分问题。

业务状态、幂等、补偿、对账,仍然要自己设计。


十二、总结

分布式事务本质上是跨服务、跨数据库、跨资源的一致性问题。

常见方案可以这样理解:

  • 本地消息表:简单可靠,适合最终一致
  • 可靠消息:适合异步解耦,但消费端必须幂等
  • TCC:一致性强,但业务侵入大
  • Saga:适合长流程,但补偿设计复杂
  • Seata:降低接入成本,但不是万能方案

实际项目中,不要为了技术而技术。

先判断业务到底需要强一致还是最终一致,再选择合适方案。

很多时候,一个设计良好的状态机,加上本地消息表、MQ、幂等、重试、补偿和对账,就已经能支撑大部分业务场景。

分布式事务没有银弹,真正可靠的系统,靠的是清晰的业务建模和完整的异常处理闭环。

http://www.jsqmd.com/news/979790/

相关文章:

  • 从零搭建一个工业监控界面:我用Qt Designer和QSS复刻了经典SCADA组态元素
  • 2026降AI工具实测避坑:这5款怎么组合最好用?附保姆级指南
  • 机器学习生产化落地:从Notebook到高可用模型服务的工程实践
  • Python 爬虫实战项目:资讯数据采集与词云可视化深度分析
  • 多项式回归实战指南:阶数选择、过拟合诊断与工业部署
  • 别再为hiprint表格数据绑定发愁了!Vue3项目实战,手把手教你搞定资产领用单打印
  • Eigen库
  • 如何安全合规地撰写AI技术博文:从业者内容创作指南
  • 恒路通交通杆件:四川公路标识牌、四川单柱式交通标志杆、四川反光标牌、四川反光膜数码打印、四川夜光交通标志牌、四川指路标志选择指南 - 优质品牌商家
  • 嵌入式MongoDB与Spring Boot的测试实践
  • 别再只认升压芯片了!聊聊电荷泵驱动NMOS的那些‘坑’:从原理到PCB布局避坑指南
  • 遗传算法进阶:自适应变异与熵驱动多样性控制
  • Platinum-MD:让复古MiniDisc焕发新生的终极免费开源工具
  • Labelme生成的JSON文件别乱扔!从标注到模型训练的全链路文件管理心得
  • 老项目救星?将传统Spring MVC单体应用,平滑迁移到普元EOS平台的实战记录
  • [智能体-325]:LangGraph如何定义图,代码示例
  • SQL 基础语法复习
  • 计算机的端口、端口漏洞
  • 助睿实验作业5:浏览器市场分析数据大屏制作与数据接入
  • 海尔(Haier)空调全国售后服务电话 官方24小时维修客服售后中心 - 故障统计表
  • STM32F103简易电子琴实战工程:带OLED显示、16键音阶响应与面包板接线图,开箱即烧录
  • 湖南科技大学EDA课FPGA霓虹灯控制工程全集(含仿真、烧录文件与演示视频)
  • 用Verilog手把手搭建一个RISC-V单周期CPU(附完整代码与仿真)
  • 时间不是补丁:机器学习中时间维度的四层工程化建模
  • 2026成都合成树脂瓦厂家评测:成都PC亮瓦/成都PC锁扣阳光板/成都PP装饰瓦/成都光扩散板/成都合成树脂瓦/选择指南 - 优质品牌商家
  • 不只是刷机:用QFIL和fh_loader命令行高效备份安卓手机eMMC全分区镜像
  • 大语言模型推理优化:重复采样如何提升覆盖率与精度
  • 告别取模软件!用C语言在51单片机上动态生成16x16点阵滚动字幕
  • MCP-RAG:动态检索与工具调用的AI新范式
  • 【西宁旺哥黄金回收】连锁品牌实测 - 润富黄金回收