事务回滚的解决方案
背景
现如今起帐项目框架采用ruoyi-vue分离版实现,由C#--->java的代码迁移,相关业务代码比较完全,细节上略微有差异,由此需要选择对现如今业务代码改动最小的解决方案。
分布式事务选型
- 事务手动处理:涉及到多类型、多数据源的问题,事务手动回滚没有任何意义(反复造轮子)
- Atomikos分布式事务:属于两阶段提交方式(2PC),基本满足事务的ACID原则;(V)--官方推荐
- dynamic-datasource:第三方解决方案,通用的多数据源解决方案,满足事务ACID原则;(V)--官方推荐
- Seata分布式事务:单独服务,互联网分布式事务通用解决方案,满足事务ACID原则;(V)
A - 原子性 (Atomicity):事务要么完全成功,要么完全失败,不能部分执行。
C - 一致性 (Consistency):事务执行前后,数据库必须保持一致性,所有约束和规则得到遵守。
I - 隔离性 (Isolation):一个事务的执行不应受到其他事务干扰,事务之间彼此隔离。
D - 持久性 (Durability):事务提交后的修改是永久性的,即使系统崩溃也不会丢失。
Atomikos:
- 优点:需要对ruoyi-vue的formwork包进行处理,使用Spring自带的事务方式即可,对业务代码没有任何的侵入性
- 缺点:没有设置对Mybatis中接口注解的支持,处理SQL时候需要配置XML实现
dynamic-datasource:
- 优点:mybatis-plus原作者开发的项目,集成度好,使用方便,有针对多数据源事务回滚方式(本地多数据源事务方案、基于
Seata
的分布式事务方案) - 缺点:单独的使用注解@DS以及@@DSTransactional,需要对现有业务代码修改
- 删除框架中原先的默认所有多数据源相关类文件
- 所有数据源切换注解换成@DS
- 修改druid配置文件
Seata分布式事务:
- 单体项目引入另外的分布式框架,目前来说不是必要的(牛刀)
注
结论:最后选择的分布式事务处理方式为:Atomikos。
Atomikos 介绍及使用
Atomikos介绍
Atomikos 是一个开源的分布式事务管理器,专门设计用于在分布式系统中管理 全局事务,确保多个数据库或其他资源管理器(如消息队列)之间的一致性。Atomikos 通过 XA协议 提供了对分布式事务的支持,尤其适用于跨多个数据库、消息系统和其他资源的事务管理。
Atomikos 实现了 两阶段提交协议(2PC)和 XA事务,用于协调跨多个资源的事务一致性,常用于微服务架构、分布式数据库、多数据源等场景中。
基础概念
问题:
为什么Spring自带事务对多数据源不支持;
表现:如果你在多个数据源中执行了操作,并且某些操作失败了,Spring 默认事务管理器不会自动回滚其他数据源的操作,导致事务(数据)的不一致性。
- Spring默认提供的事务主要是为单一数据源设计的,在单一数据源的情况下,Spring 使用
DataSourceTransactionManager
来管理事务。这个事务管理器与单一数据源的生命周期绑定,事务的开始、提交、回滚等操作都围绕着这个数据源进行。- Spring 默认的事务管理器(
DataSourceTransactionManager
)并不能同时协调多个数据源的事务操作。对于数据源事务的处理都需要一个独立的事务管理器,而 Spring 没有内建的事务管理器来同时协调多个数据源的事务操作。
Atomikos使用前需要先拆解下目前SQL在Spring中的执行流程;涉及到这几个概念需要明白:
组件 | 角色 | 管理方式 | 责任范围 |
---|---|---|---|
DataSource | 提供数据库连接 | 由连接池管理 | 数据源配置和管理 |
SqlSessionFactory | 创建 SqlSession | 由框架自动管理 | 配置 MyBatis 会话和连接池 |
SqlSession | 负责执行 SQL 和数据库交互 | 需手动管理生命周期 | 提供查询、更新、删除等操作接口 |
SqlSessionTemplate | SqlSession 的线程安全实现 | Spring 管理 | 自动管理会话生命周期,整合事务 |
以下是四者的关系总结及操作流程:
DataSource
提供连接:- 是底层数据库连接的来源。
- 由连接池实现,如 HikariCP 或 Druid。
SqlSessionFactory
创建SqlSession
:- 从
DataSource
获取连接。 - 创建并配置
SqlSession
。
- 从
SqlSession
执行 SQL:- 负责与数据库交互。
- 需手动管理生命周期,但在 Spring 中通常被
SqlSessionTemplate
包装。
SqlSessionTemplate
管理SqlSession
:- 是
SqlSession
的线程安全实现。 - 管理会话的创建和关闭。
- 集成 Spring 的事务管理。
客户端请求 -----> 控制器 (Controller) -----> Service 层 -----> Mapper 接口 | v SqlSessionTemplate(Spring 管理事务和生命周期) | v SqlSession(执行 SQL,交互数据库) | v SqlSessionFactory(创建 SqlSession) | v DataSource (数据库连接池)
- 是
流程改造
有了前面的概念,那么我们可以知道需要改造的地方了
原
Spring
事务管理器只支持一种数据源;改造Atomikos
需要配置事务管理器
原框架数据源没有接入
Atomikos
改造- 改造
DruidConfig
配置类,将数据源放入druid提供的XA中(druidXADataSource )
: - 再将
druidXADataSource
数据源放入atomikosDataSourceBean
供Atomikos
能够管理的数据源
- 改造
在
MyBatis 中
,需要使用SqlSessionFactory
和SqlSessionTemplate
两者来执行 SQL 操作。当涉及多个数据源时,SqlSessionFactory
和SqlSessionTemplate
的配置需要结合 Atomikos 事务管理器,以确保 MyBatis 操作在 Atomikos 事务管理下执行 改造- 针对不同的数据源创建不同的
SqlSessionFactory
- 不同的
SqlSessionFactory
需要注入对应的数据源 - 通过自定义
DynamicSqlSessionTemplate
实现来处理不同的Factory
,管理不同的sqlSession
- 针对不同的数据源创建不同的
Atomikos验证以及原理
概念
XA RM (XA Resource Manager):
- 资源管理器,负责管理和控制各个资源(如数据库、消息队列等)并参与事务。它根据事务管理器的指令执行 prepare、commit 和 rollback 操作。
TM (Transaction Manager):
- 事务管理器,协调多个 XA RM 参与的分布式事务,确保全局事务的一致性。TM 使用两阶段提交协议(2PC)来保证事务要么全部提交,要么全部回滚。
属于两阶段提交流程(2PC)
XA规范中,多个RM状态之间的协调通过TM进行,而这个资源协调的过程采用了两阶段提交协议,在两阶段提交中,分为准备阶段和提交阶段:
第一阶段:
第二阶段:
基本满足了事务的 ACID 特性,但其不中之处也是明显的:(任务)
- 在事务的执行过程中,所有的参与节点都是阻塞型的,在并发量高的系统中,性能受限严重; 不影响
- 如果TM在commit前发生故障,那么所有参与节点会因为无法提交事务而处于长时间锁定资源的状态;
- 在实际情况中,由于分布式环境下的复杂性,TM在发送commit请求后,可能因为局部网络原因,导致只有部分参与者收到commit请求时,系统便出现了数据不一致的现象;
- XA协议要求所有参与者需要与TM进行直接交互,但在微服务架构下,一个服务与多个RM直接关联常常是被不允许的;
执行流程:
事务注意点
@Transactional 注解应该只被应用在 public 修饰的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 该注解,它也不会报错(IDEA 会有提示), 但事务并没有生效。
被外部调用的公共方法 A 有两个进行了数据操作的子方法 B 和子方法 C 的事务注解说明:
被外部调用的公共方法 A 未声明事务 @Transactional,子方法 B 和 C 若是其他类的方法且各自声明事务,则事务由子方法 B 和 C 各自控制
被外部调用的公共方法 A 未声明事务 @Transactional,子方法 B 和 C 若是本类的方法,则无论子方法 B 和 C 是否声明事务,事务均不会生效
被外部调用的公共方法 A 声明事务 @Transactional,无论子方法 B 和 C 是不是本类的方法,无论子方法 B 和 C 是否声明事务,事务均由公共方法 A 控制
被外部调用的公共方法 A 声明事务 @Transactional,子方法运行异常,但运行异常被子方法自己
try-catch
处理了,则事务回滚是不会生效的!如果想要事务回滚生效,需要将子方法的事务控制交给调用的方法来处理:
- 方案 1:子方法中不用
try-catch
处理运行异常 - 方案 2:子方法的 catch 里面将运行异常抛出【throw new RuntimeException();】
- 方案 1:子方法中不用
同类中不能调用带有事务的方法,会造成事务失效
- spring中的事务都是通过代理来实现的
问题
【1】为什么在事务的执行过程中,所有的参与节点都是阻塞型的?
- 在 2PC 中,参与节点的阻塞是必要的,因为每个节点必须等待其他节点的状态,直到所有节点都准备好提交或者全部回滚。每个节点在等待事务管理器的指令时,不能执行其他操作(例如提交或释放资源)。
- 这就导致了所有参与节点都在准备阶段阻塞,直到事务管理器做出最终决策(提交或回滚)。
【2】为什么TM在commit前发生故障,那么所有参与节点会因为无法提交事务而处于长时间锁定资源的状态?
因为在两阶段提交协议(2PC)中,所有资源管理器都依赖事务管理器来发出最终的提交或回滚指令。TM 故障时,参与节点无法独立完成事务操作,必须等待 TM 恢复并发出指令,而在此期间,它们已经锁定了相关资源,从而导致资源长时间无法释放。