分布式事务
@Transactional 属于本地事务模型,是基于数据库的 ACID 特性实现的,它只适用于单进程、单数据库**的场景,**无法解决分布式事务问题。
# 分布式事务的实现方式
首先需要先指定CAP理论。
- C consistency 一致性: 所有节点同一时间具有相同的数据
- A availability 可用性: 节点可用,每个请求都能收到非错误性的响应
- P partition tolerance 分区容错性: 节点不能因网络分区而崩溃
在分布式系统中,CAP理论的一个限制是: CAP理论的实现中,只能同时满足两个条件。而 P 是必须的,我们通常会选择 AP,牺牲 C 强一致性。
通常放弃强一致性(如 2PC/XA),转而追求最终一致性(Eventual Consistency)
实现方案大致如下:
# 1、基于“本地消息表” (Local Message Table)
这是最经典、最不依赖特定中间件特性的方案,适用于对一致性要求极高的场景 。
具体流程如下:
- 利用数据库的分布式事务,保证原子性。(业务数据+消息记录 在同一个事务里面完成,成功提交,失败则回滚)
- 定时任务扫描,独立后台任务轮询扫描待处理的消息,最后更新。
使用定时任务扫描,可以提高对接口的容错性,从而提升系统的可用性。
优点:不依赖第三方中间件,可靠性极高。实现简单,容易排查回溯,适用于大多数场景。 缺点:实时性差,有延迟,定时任务对数据库压力颇大
# 2、MQ 消息中间件
基于“事务消息” (Transactional Messaging)
这是目前互联网大厂(特别是阿里系技术栈)非常主流的方案,典型代表是 RocketMQ。它其实是“本地消息表”的封装和优化。
核心在于它解决了本地事务与消息发送的原子性问题,即保证:
业务操作成功,消息一定能被消费
业务操作失败,消息一定不会被消费
核心流程(RocketMQ 为例):
- 发送半消息(Half Message):生产者向 MQ 发送一条“半消息”,MQ 收到后持久化但不投递(消费者看不见)。
- 执行本地事务:生产者执行本地业务逻辑。
- 提交/回滚:
- 如果本地事务成功,向 MQ 发送 Commit,MQ 将消息改为“可投递”,消费者收到消息。
- 如果本地事务失败,向 MQ 发送 Rollback,MQ 删除消息。
- 回查机制(关键点):如果 MQ 长时间没收到 Commit/Rollback(比如网络断了),MQ 会反向回调生产者的接口,检查本地事务的状态,从而决定是提交还是回滚。
┌─────────┐ 1.发送Half消息 ┌─────────┐
│ Producer│───────────────────────▶│ MQ Server │
└────┬────┘ └─────┬───┘
│ 2.返回发送结果 │
│◀─────────────────────────────────│
│ │
│ 3.执行本地事务 │
│ (数据库操作) │
│ │
│4.提交/回滚事务状态 │
│─────────────────────────────────▶│
│ │
│ │5.MQ定时检查
│ │ 事务状态
│ │ (回查机制)
│◀─────────────────────────────────│
│ │
│6.返回最终事务状态 │
│─────────────────────────────────▶│
│ │
│ │7.提交/回滚消息
│ │ (对消费者可见/删除)
│ │
│ ▼
┌────┴────┐ ┌─────────┐ 8.消费消息
│Producer │ │ MQ Server│─────────────────┐
│本地事务 │ │ │ │
└─────────┘ └─────────┘ │
▼
┌─────────┐
│Consumer │
└─────────┘
优点:业务解耦,不需要维护本地消息表,性能更好。
缺点:强依赖支持事务消息的 MQ 中间件(RabbitMQ、Kafka 原生不支持这种强一致性逻辑,需要魔改)。
# 3、Seata
Seata是一个平台/框架:它提供了包括AT、TCC、Saga、XA在内的多种分布式事务解决方案。
# 1. XA模式
它的核心思想是**"全局锁定"**。
- 第一阶段(准备):事务协调者通知所有服务(订单、库存、账户):"你们都准备好,我要开始事务了,把要改的数据都锁住,不许其他人动。"
- 订单服务锁住这张订单,库存服务锁住这个商品,账户服务锁住这个用户的余额。
- 第二阶段(提交/回滚):
- 如果都准备好:协调者通知"提交",所有服务同时解锁,事务完成。
- 如果任何一个失败:协调者通知"回滚",所有服务撤销刚才的操作。
你可以这样理解:就像一场需要多人合作的手术,主刀医生(协调者)必须先确认麻醉师、护士、助手全都准备好(第一阶段),然后才喊"开始手术"(第二阶段)。如果任何一个人说"没准备好",整个手术就要取消。
- 优点:强一致性,对业务无侵入(加个注解就行)。
- 缺点:性能差。在整个事务过程中,所有涉及的数据都被锁着,高并发下数据库就是瓶颈。
- 一句话总结:牺牲性能,换取绝对的、像单机数据库一样的强一致性,适合极重要但并发不高的场景。
# 2.AT模式
这是Seata主推的模式,它觉得XA那种"一直锁着"太笨了,做了个聪明的优化:"大部分时间不用锁,只在提交那一瞬间锁一下"。
- 第一阶段(直接提交):订单服务直接"创建订单成功"(并提交本地事务),同时记录下这个订单修改前的快照(Undo Log)到一张专门的回滚日志表里。库存、账户服务同理。
- 第二阶段(异步清理或回滚):
- 如果都成功:协调者通知"提交",各服务异步删除刚才的快照记录(Undo Log)。
- 如果任何一个失败:协调者通知"回滚",各服务拿着自己的快照(Undo Log),通过生成反向SQL(比如
update...set...where...)把数据改回去。
你可以这样理解:就像办公室里大家各忙各的(直接提交),但你随时在电脑上开着一个"撤销(Ctrl+Z)"按钮,一旦谁出问题,就全局按一下"撤销"键,大家回到操作前的状态。由于大部分时间没锁,性能就比XA好多了。
- 优点:高性能,且对业务代码几乎零侵入(开发者只需加
@GlobalTransactional注解)。 - 缺点:依赖数据库的
undo_log表,本质上是最终一致性。 - 一句话总结:最常用的模式。高并发下性能好,又几乎不修改代码,是绝佳的**"万金油"**选择。
# 3.TCC模式
如果AT模式是依靠数据库快照自动"撤销",那TCC模式就是让你自己写代码来定义"如何撤销"。
它分为三个阶段:
- Try(尝试/预留):订单服务"创建一条状态为'待确认'的订单",库存服务"扣减1个商品的可销售库存并增加1个预扣库存",账户服务"冻结100元"。
- Confirm(确认):协调者检查所有Try都成功后,通知执行Confirm。订单服务"把订单状态改为'已创建'",库存服务"把预扣库存转为已售库存",账户服务"把冻结金额转为已扣款"。
- Cancel(取消):如果任何一个Try失败,执行Cancel。订单服务"删除待确认订单",库存服务"释放预扣库存",账户服务"解冻余额"。
你可以这样理解:就像你网购用信用卡支付。
Try:银行先冻结你这100元额度,但这钱还没真的转给商家。
Confirm:商家发货后,银行扣除你这100元,转给商家。
Cancel:你取消订单,银行解冻你这100元。
优点:性能非常高(因为是业务层面的短事务,无锁),且不依赖数据库事务,可以操作Redis等非数据库资源。
缺点:代码侵入性极高。每个参与的服务都要自己实现
Try/Confirm/Cancel三个接口,开发成本很高,且要处理幂等和空回滚问题。
# 4.Saga 模式 (长事务编排)
这是专门为业务流程特别长、无法接受长时间锁的场景准备的。
当你的业务本身就是由多个步骤组成的长时间过程时,Saga是比TCC更轻量、更适合最终一致性的模式。
通过将大事务分解为一系列有序的、可以独立执行的本地事务来管理一致性。这些本地事务通过协调器或者事件驱动的方式依次执行,如果其中一个事务失败,则使用相应的补偿事务来撤销之前已经完成的事务,以确保系统的一致性,属于补偿性事务。
它的核心思想是:正向执行,失败了就反向补偿。没有Try和Confirm阶段,就是直接执行真正的业务操作(如"创建订单")。同时给每个操作配一个补偿操作(如"取消订单")。
- 正常情况:订单服务→创建订单 → 库存服务→扣减库存 → 账户服务→扣减余额。一路成功。
- 异常情况:假设扣减余额时失败了,Saga就会反向触发:调用库存服务的补偿接口(加回库存) → 调用订单服务的补偿接口(取消订单)。
你可以这样理解:就像你去政府窗口办事,流程是A窗口盖章→B窗口审核→C窗口缴费。当你走到C窗口发现钱不够、办不成了,办事员会带你倒着走一遍:C窗口"取消缴费" → B窗口"取消审核" → A窗口"取消盖章"。整个过程没有锁,失败了就"撤销"回去。
- 优点:适合超长事务(比如几十个步骤),不锁定资源,高吞吐。
- 缺点:隔离性差(过程中数据对其他事务可见),补偿逻辑复杂,也需要手动实现。
- 一句话总结:业务流程步骤非常多(如旅游预订、银行开户)或者你要调用第三方API(微信支付,扣款后无法回滚,只能调退款补偿)时选择。