spring事务@Transactional失效原因及解决办法

2022-07-04 09:57:31

spring事务@Transactional失效情况分析主要从以下几个方面考虑:

1. mysql数据库

默认情况下mysql数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表的存储引擎是MyISAM,MyISAM是不支持事务的。这样就会出现“事务失效”的问题了。

解决方案:修改存储引擎为Innodb

2. 业务代码

2.1 执行事务的Bean交由Spring管理

我们要使用Spring的申明式事务,那么需要执行事务的Bean是否已经交由了Spring管理,在代码中的体现就是类上是否有@Service、Component等一系列注解。

解决方案:将Bean交由Spring进行管理(添加@Service注解)

2.2非public的方法进行事务管理

默认情况下你无法使用@Transactional对一个非public的方法进行事务管理

解决方案:修改需要事务管理的方法为public

2.3 出现了自调用

多个方法都在同一个类中,其中第一个方法中调用了本类中的第二个和第三个方法,这就是自调用。

那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP的,当容器在创建Service这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中。由于方法实际上是由Service也就是目标类自己调用的,所以在方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因:自调用时,调用的是目标类中的方法而不是代理类中的方法。

解决方案

  1. 自己注入自己,然后显示的调用,
  2. 这种方案看起来不是很优雅
  3. 利用AopContext,如下:
@Service
public class DemoService {

	@Transactional
	public void save(A a, B b) {
		((DemoService) AopContext.currentProxy()).saveB(b);
	}

	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public void saveB(B b){
		dao.saveB(a);
	}
}

使用上面这种解决方案需要注意的是,需要在配置类上新增一个配置

// exposeProxy=true代表将代理类放入到线程上下文中,默认是false
@EnableAspectJAutoProxy(exposeProxy = true)

3. 事务回滚相关问题

3.1 该回滚的时候事务提交了

这种情况往往是程序员对Spring中事务的rollbackFor属性不够了解导致的。

Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务,已经执行的SQL会提交掉。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性。

默认情况下,只有出现RuntimeException或者Error才会回滚

public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

所以,如果你想在出现了非RuntimeException或者Error时也回滚,请指定回滚时的异常,例如:

@Transactional(rollbackFor = Exception.class)

3.2 该提交的事务被回滚

对应的异常信息如下:

Transaction rolled back because it has been marked as rollback-only

总结起来,主要的原因就是因为内部事务回滚时将整个大事务做了一个rollbackOnly的标记,所以即使我们在外部事务中catch了抛出的异常,整个事务仍然无法正常提交,并且如果你希望正常提交,Spring还会抛出一个异常。

3.3 解决方案

这个解决方案要依赖业务而定,你要明确你想要的结果是什么

内部事务发生异常,外部事务catch异常后,内部事务自行回滚,不影响外部事务
将内部事务的传播级别设置为nested/requires_new均可。在我们的例子中就是做如下修改:

// @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void a() throws ClassNotFoundException{
    // ......
    throw new ClassNotFoundException();
}

虽然这两者都能得到上面的结果,但是它们之间还是有不同的。当传播级别为requires_new时,两个事务完全没有联系,各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。但是传播级别为nested时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,实际上是回滚到保存点上,并且当外部事务提交时,内部事务才会提交,外部事务如果回滚,内部事务会跟着回滚。

内部事务发生异常时,外部事务catch异常后,内外两个事务都回滚,但是方法不抛出异常

4. 读写分离跟事务结合使用时的问题

读写分离一般有两种实现方式

  1. 配置多数据源
  2. 依赖中间件

如果是配置了多数据源的方式实现了读写分离,那么需要注意的是:如果开启了一个读写事务,那么必须使用写节点,如果是一个只读事务,那么可以使用读节点。

如果是依赖于中间件那么需要注意:需要根据中间件的事务规范使用事务。

  • 作者:程序猿(攻城狮)
  • 原文链接:https://blog.csdn.net/liaomingwu/article/details/121236692
    更新时间:2022-07-04 09:57:31