一、事情起因是这样的
首先,我们是使用Spring + mybatis 进行开发。
某功能在测试环境看到报错日志, 但是数据库里面的数据发生了变化,没有回滚。
执行数据库update 操作的方法上明确有
@Transactional(rollbackFor = Exception.class)的注解。
假设,异常日志之前执行的sql是
update t1 set c1= 10 where id = 10 //原来 c1 = 5
方法抛出异常后, 数据库里面确实被修改成了10 。 说明事务没有回滚,或者事务没有生效。
二、验证是事务失效还是回滚失败
1、本地启动服务, 在update 执行的结束的地方和异常抛出之前,打断点, 执行玩update。使用Navicat查看, 数据已经被修改。 说明事务没生效或者数据库隔离级别为读未提交。
2、检查数据库隔离级别:可重复读
在可重复度的隔离级别下, 事务之间是是无法看到相互之间没有提交的数据的。所以确定当前问题是,事务失效。@Transactional(rollbackFor = Exception.class) , 没有生效。
下面就开始了查看@Transactional在什么情况下会失效。
三、Spring Transactional 失效的可能原因
1、、失效场景集一:代理不生效
(1)将注解标注在接口方法上
@Transactional
是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到@Transactional
,从而事务失效。
这种错误我们还是犯得比较少的,基本上我们都会将注解标注在接口的实现类方法上,官方也不推荐这种。
(2)被final、static关键字修饰的类或方法
CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰后,无法继承父类与父类的方法。
(3)类方法内部调用
事务的管理是通过代理执行的方式生效的,如果是方法内部调用,将不会走代理逻辑,也就调用不到了。
2、失效场景集二:框架或底层不支持的功能
(1)非public修饰的方法
(2)多线程调用
(3)数据库本身不支持事务
比如Mysql的Myisam存储引擎是不支持事务的,只有innodb存储引擎才支持。
这个问题出现的概率极其小,因为Mysql5之后默认情况下是使用innodb存储引擎了。
但如果配置错误或者是历史项目,发现事务怎么配都不生效的时候,记得看看存储引擎本身是否支持事务。
(4)未开启事务
这个也是一个比较麻瓜的问题,在Springboot项目中已经不存在了,已经有DataSourceTransactionManagerAutoConfiguration默认开启了事务管理。
但是在MVC项目中还需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
3、失效场景集三:错误使用@Transactional
(1)错误的传播机制
(2)rollbackFor属性设置错误
默认情况下事务仅回滚运行时异常和Error,不回滚受检异常(例如IOException)。
因此如果方法中抛出了IO异常,默认情况下事务也会回滚失败。
我们可以通过指定@Transactional(rollbackFor = Exception.class)
的方式进行全异常捕获。
(3)异常被内部catch
(4)嵌套事务
检查了以上问题, 在我们系统中不存在这些问题。 所以很苦恼。
四、查找事务失效的真正原因
首先捋一下思路:
在应用系统调用声明@Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据@Transactional 的属性配置信息,这个代理对象决定该声明@Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务.
事务管理的框架是由抽象事务管理器 AbstractPlatformTransactionManager 来提供的,而具体的底层事务处理实现,由 PlatformTransactionManager 的具体实现类来实现,如事务管理器 DataSourceTransactionManager。不同的事务管理器管理不同的数据资源 DataSource,比如 DataSourceTransactionManager 管理 JDBC 的 Connection。
1、进行一次Spring未生效事务管理的Debug
(1)、我们的测试代码
@Service
@Slf4j
public class TestSevice {
@Autowired
XXXDao xxxDao;
@Transactional(rollbackFor = Exception.class)
public void updateAData() {
System.out.println("updateAData 方法执行开始");
//这里进行数据库操作
xxxDao.updateOne(XXX.builder().id("0002C2B4-D5AA-4596-B592-510185E4B115").type("8888").build());
//throw异常
throw new RuntimeException("error");
System.out.println("updateAData 方法执行完成");
}
}
我们期待能够正常回滚!
(2)我们打开
org.springframework.transaction.interceptor.
TransactionInterceptor
在如下位置打个断点
一步步跟下去会发现在
org.springframework.transaction.interceptor.
TransactionAspectSupport
发现
PlatformTransactionManager 是 KafkaTransactionManager ~ WTF!!
先往下走走看。
进入completeTransactionAfterThrowing, 我们会发现。 最终进入到AbstractPlatformTransactionManager.rollback()
最终我们会发现,调用了KafkaTransactionManager(实现了 AbstractPlatformTransactionManager)的rollback。并没有对mysql 数据源做任何的rollback。
2、为什么会是Kafka的事务管理器呢?
反过来想一下?为什么AbstractPlatformTransactionManager 的四个实现里面, 只有Kafka的注册进去了呢?
gpt 查了下这四种事务管理器。大概如下。
CciLocalTransactionManager
CciLocalTransactionManager
通常用于与企业信息系统 (EIS) 的交互,如数据库、消息队列等。- 它是针对 Common Client Interface (CCI) 标准的事务管理器,用于管理在单个资源管理器范围内的本地事务。
- 主要用于与符合 CCI 标准的资源适配器一起使用,确保在事务上下文中适当地协调对这些资源的操作。
JtaTransactionManager
JtaTransactionManager
是用于管理分布式事务的事务管理器之一。- 它是在 Java EE 环境下使用的,通常与 JTA(Java Transaction API)一起使用。
- JTA 是 Java 平台的事务管理 API,用于管理分布式事务,允许跨多个资源管理器的事务管理。
JtaTransactionManager
通过 JTA 实现分布式事务的管理,它协调不同资源管理器之间的事务操作,确保事务的一致性和隔离性。
DataSourceTransactionManager
DataSourceTransactionManager
通常用于与关系型数据库 (RDBMS) 的交互。- 它是 Spring 框架中用于管理数据库事务的事务管理器之一。
- 主要用于管理与
DataSource
关联的事务,DataSource
是用于获取数据库连接的接口。 DataSourceTransactionManager
通过 JDBC 连接管理事务,它可以管理多个连接,并将它们与 Spring 中的事务进行关联。
KafkaTransactionManager
KafkaTransactionManager
是用于管理 Kafka 消息系统中事务的事务管理器。- 它是在 Spring Kafka 框架中使用的,用于管理 Kafka 生产者和消费者之间的事务。
KafkaTransactionManager
实现了 Kafka 的事务性 API,允许在生产者和消费者之间实现端到端的事务性保证。- 它管理 Kafka 生产者和消费者之间的事务性操作,以确保消息的可靠传递和处理。
结论是:CciLocalTransactionManager JtaTransactionManager 我们并没有使用, 也没有配置。 所以不会被注册进去。
那么我们看下DataSourceTransactionManager 在没有Kafka 的情况下, 是怎么注入到SpringBean管理器的
看到了这个 @ConditionalOnMissingBean注解。 在(IOC容器内)不存在PlatformTransactionManager的实现类的时候,才加载并注入DataSourceTransactionManager(PlatformTransactionManager的实现)。所以。 在这里, Spring 容器先加载了KafkaTransactionManager。 就不会再加载DataSourceTransactionManager。
3、So!问题是由于数据源事务管理器没有注入。
下面我们尝试一下,手动写一个配置类,将DataSourceTransactionManager 注入到容器内。
@Configuration
public class DatasourceTransactionConfig {
private final DataSource dataSource;
private final TransactionManagerCustomizers transactionManagerCustomizers;
DatasourceTransactionConfig(DataSource dataSource,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
this.dataSource = dataSource;
this.transactionManagerCustomizers = transactionManagerCustomizers.getIfAvailable();
}
@Bean
@Primary //配置默认事务管理器
public DataSourceTransactionManager transactionManager(DataSourceProperties properties) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(this.dataSource);
if (this.transactionManagerCustomizers != null) {
this.transactionManagerCustomizers.customize(transactionManager);
}
return transactionManager;
}
}
我们尝试一下,事物是否生效?
生效!完美!
但是Kafka的事务怎么办呢?
我们再次debug进去吧
这里的确是DatasourceTransactionManager了。Kafka的去哪里了呢?
我们跳到上图的上一步
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
进入determineTransactionManager 发现, transactionManagerCache只有一个TransactionManager, 所以如果Kafka有事务需求, 一样会失效。
这可如何是好。
五、兼容Kafka和DataSource两个TransactionManager
可以尝试链式事务管理器。
ChainedTransactionManager 是 Spring Data 项目中的一个事务管理器实现。可以向 ChainedTransactionManager 指定事务管理器列表,它将按照列表中事务管理器的顺序启动事务。在退出时(即方法退出时),事务将按照事务管理器列表的相反顺序提交。
虽然这听起来是个合理的策略,但需要注意的一点是,ChainedTransactionManager 目前已被弃用,不推荐使用。弃用的原因见 大意是,人们通常希望 ChainedTransactionManager 是一颗神奇的银弹,可以解决所有事务问题,包括具有两阶段提交的分布式事务和其他问题,而这与事实相去甚远。
ChainedTransactionManager 只能确保事务按特定顺序启动和提交。它不能保证任何事务同步,更不用说任何分布式事务协调了。假设你可以接受 ChainedTransactionManager 的限制,并希望按照我们的用例所要求的特定顺序执行事务。在这种情况下,使用该事务管理器是合理的,只要你记住你使用的是框架中一个已废弃的类即可。
public class TransactionConfig {
private final DataSource dataSource;
private final TransactionManagerCustomizers transactionManagerCustomizers;
TransactionConfig(DataSource dataSource,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
this.dataSource = dataSource;
this.transactionManagerCustomizers = transactionManagerCustomizers.getIfAvailable();
}
@Bean
@Primary //配置默认事务管理器
public DataSourceTransactionManager transactionManager(DataSourceProperties properties) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(this.dataSource);
if (this.transactionManagerCustomizers != null) {
this.transactionManagerCustomizers.customize(transactionManager);
}
return transactionManager;
}
@Bean
public ChainedKafkaTransactionManager chainedKafkaTransactionManager(DataSourceTransactionManager transactionManager,
KafkaTransactionManager<?, ?> kafkaTransactionManager){
return new ChainedKafkaTransactionManager<>(transactionManager, kafkaTransactionManager);
}
}