1、Spring事务
1.1 事务开启类型
- REQUIRED (默认) 如果已存在事务,则加入该事务,否则新建一个事务
- REQUIRED_NEW 每次都是新建一个事务,上一个事务挂起
- SUPPORTS 支持当前事务,如果没有则不创建事务
- NOT_SUPPORTED 以非事务的方式执行,如果当前存在事务,则挂起
- MANDATORY 必须在一个已有事务中运行,否则抛异常。
- NEVER 不能在事务中运行,否则抛异常。
- NESTED 如果当前存在事务,则在嵌套事务(SavePoint,可回滚子事务,不影响主事务)内执行;否则行为类似 REQUIRED。
1.2 事务原理
- 基于AOP,通过代理模式,JDK代理或者CGLIB代理
- 通过拦截器开启事务,提交或者回滚
- 通过try catch 方式,在catch块中回滚事务
- 通过ThreadLocal记录当前线程的事务,当前线程是否开启事务,当前线程持有的数据源对应的数据库连接
1.3 事务失效
- 在一个未开启事务的方法中调用了本对象中一个开启事务的方法,不会走代理
- 捕获异常,导致无法回滚
- 在非public的方法上使用@Transactional
- 抛出异常不匹配@Transactional设定的异常
- 在方法中使用异步操作
2、分布式事务
2.1 分布式事务的种类
- 2PC,两阶段提交,其实就是利用本地事务,对修改行开启事务,然后执行完了,协调者向所有参与者发送提交指令,强一致性,但是性能很差,如果哪一个方法有异常,则统一回滚,如果协调者有问题,就会导致事务一直挂起
- 3PC,相对于2PC增加了预校验,看能不能执行该操作,但是和2PC一样,同样开启本地事务,强一致性,但是性能很差
- TCC try, confirm, concel,通过预先执行操作,比如转账操作,进行一些不被用户看到的数据库表变更,好处是,所有的操作都是直接执行提交,不会有锁竞争,所以高性能,而且也满足最终一致性,缺点是需要自己实现try confirm concel操作,而且会出现悬挂,空回滚问题,confirm超时问题
- Saga 长事务模式,每一步的变更都有对应的回退操作,不管哪一步失败,都从失败的地方逐步回退,但是问题是变更状态会被用户直接看到,而且会存在无法回滚的情况,最终一致性
- 本地消息表模式/MQ模式,在完成某一步操作后,同步插入一条数据到本地消息表,然后通过MQ的方式进行处理,最终一致性
- seata的AT模式,在每个数据库设置一个属于seata的undo log表,用于回滚真提交事务之后的操作,只能用于数据库,最终一致性
- seata的XA模式,强一致性,和2PC类似
2.2 TCC的问题
- Seata 等 TCC 框架内部已经通过 xid + 分支 ID 的方式,在 TC 侧做了幂等控制,但业务开发者依然需要保证自己 Confirm/Cancel 方法的业务逻辑是幂等的,这是双重保险。
- 每个服务必须记录阶段的状态!
在方法中,先插入一条事务记录到本地数据库(如 tcc_transaction 表),通过状态来进行幂等处理
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 空回滚 | Cancel 在 Try 之前到达 |
Cancel 时检查 Try 是否执行过,没执行过就跳过。 |
| 悬挂 | Cancel 先执行成功,Try 后到达并执行 |
Try 时检查是否已被 Cancel,如果已被取消就拒绝执行。 |
| Confirm/Cance 超时 | 网络问题导致 TM 不确定操作是否成功 | 确保 Confirm 和 Cancel 方法是幂等的,允许被重复调用。 |
2.3 Saga和MQ如何做到有迹可循
| 特性 | Saga (Orchestration) | MQ (本地消息表) |
|---|---|---|
| "迹"的载体 | 中心化的 Saga 状态机实例表 (saga_id, current_state, history) |
去中心化的本地消息表 (message_id, content, status) |
| 追踪方式 | 通过 saga_id 可以完整还原整个长事务的执行路径。 |
通过 message_id 只能知道单个消息的生产和消费状态,跨服务的完整链路需要额外的链路追踪(如 SkyWalking)。 |
| 协调者 | 有明确的中心化编排器,负责驱动流程和补偿。 | 无中心协调者,靠消息的发布/订阅自然驱动。 |
| 适用场景 | 复杂的、多步骤的、需要精确补偿的业务流程(如订单创建、机票预订)。 | 简单的、点对点的、事件驱动的最终一致性(如用户注册后发欢迎邮件、积分变更)。 |
- Saga每次执行新的长事务,会生成一个唯一的saga_id,并且插入到数据库中
- 同一个长事务,执行到不同阶段,对应的记录也会变更状态
| 问题 | 答案 |
|---|---|
| 记录执行进度是 INSERT 还是 UPDATE? | 首次启动时 INSERT 一条实例记录,后续所有进度更新都是 UPDATE 这条记录。 |
| "执行到哪一步"的状态是用户自定义的吗? | 状态的名称(如 "扣库存")由用户在状态机定义文件中自定义,但状态的流转和持久化由框架自动管理。 |
2.4 分布式事务的要求
- CAP
- C considency 一致性
- A abilty 高可用
- P Partition 网络分区
- 其中P是必定会有问题的,所以只能满足CA
| 分布式事务模式 | 一致性 © | 可用性 (A) | 网络分区容忍 § | CAP 归属 | 说明 |
|---|---|---|---|---|---|
| 2PC / 3PC / XA | ✅ 强一致性 | ❌ 低可用性 | ✅ | CP | 协调者或任一参与者宕机/网络分区,整个事务会阻塞挂起,无法对外提供服务(不可用)。但一旦成功,数据绝对一致。 |
| TCC | ⚠️ 最终一致性 | ✅ 高可用性 | ✅ | AP | Try 阶段成功后,资源已被预留,即使后续 Confirm/Cancel 因网络问题失败,系统也是可用的(用户能看到"处理中"状态)。通过后台任务重试,最终能达到一致。牺牲了强一致性,换取了可用性。 |
| Saga | ⚠️ 最终一致性 | ✅ 高可用性 | ✅ | AP | 每个子事务都是独立提交的。即使中间某步因网络问题失败,前面的步骤已经生效(用户可见),系统依然可用。通过执行补偿事务来最终达到一致。典型的 AP 系统。 |
| 本地消息表 / MQ 事务消息 | ⚠️ 最终一致性 | ✅ 高可用性 | ✅ | AP | 生产者发消息和本地事务在一个库,保证了原子性。消费者可能因网络问题暂时收不到消息,但消息队列会重试,最终会被消费。整个链路是最终一致且高可用的。 |
| Seata AT | ⚠️ 最终一致性 | ✅ 高可用性 | ✅ | AP | 虽然看起来像本地事务,但它依赖 TC(协调者)。如果 TC 宕机,新的全局事务无法开启,但已经提交的本地事务不受影响,数据库依然可用。回滚依赖异步的 undo_log 清理。设计目标是高性能和高可用,接受最终一致。 |