Spring 事务在哪几种情况下会失效?
一、数据库引擎不支持事务
这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都不能支持事务。
二、没有被 Spring 管理
// @Service public class OrderServiceImpl implements OrderService { @Transactional public void updateOrder(Order order) { // update order } }
三、方法不是public的
@Service public class UserService { @Transactional private void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
// Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }
如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。可以开启 AspectJ 代理模式。
四、方法内部调用
@Service public class UserService { @Autowired private UserMapper userMapper; public void add(UserModel userModel) { userMapper.insertUser(userModel); this.updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); } }
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:
@Servcie public class ServiceA { @Autowired prvate ServiceA serviceA; public void save(User user) { queryData1(); queryData2(); serviceA.doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }
@Service public class UserService { @Transactional // 编译时直接报错 Methods annotated with '@Transactional' must be overridable public final void add(UserModel userModel){ saveData(userModel); updateData(userModel); } }
spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,从而实现事务功能。
六、异常被吃了
事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。
@Service public class UserService { @Transactional public void add(UserModel userModel) { try { saveData(userModel); updateData(userModel); } catch (Exception e) { log.error(e.getMessage(), e); } } }
这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。
如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
七、手动抛了别的异常
即使没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。
public class UserService { @Transactional public void add(UserModel userModel) throws Exception { try { saveData(userModel); updateData(userModel); int i = 1/0 } catch (Exception e) { log.error(e.getMessage(), e); throw new Exception(e); } } }
八、抛出自定义异常
在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。
@Service public class UserService { @Transactional(rollbackFor = BusinessException.class) public void add(UserModel userModel) throws Exception { saveData(userModel); updateData(userModel); } }
这种情况事务也不会回滚。
如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
九、多线程
@Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { try { test(); } catch (Exception e) { roleService.doOtherThing(); } }).start(); } } @Service public class RoleService { @Transactional public void doOtherThing() { try { int i = 1/0; System.out.println("保存role表数据"); }catch (Exception e) { throw new RuntimeException(); } } }
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
十、错误的传播属性
spring目前支持7种传播特性:
- REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
- SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
- MANDATORY 如果当前上下文中存在事务,否则抛出异常。
- REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
- NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
- NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:
@Service public class UserService { @Transactional(propagation = Propagation.NEVER) public void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
十一、嵌套事务回滚多了
public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); roleService.doOtherThing(); } } @Service public class RoleService { @Transactional(propagation = Propagation.NESTED) public void doOtherThing() { System.out.println("保存role表数据"); } }
这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。
因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
怎么样才能只回滚保存点呢?
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); try { roleService.doOtherThing(); } catch (Exception e) { log.error(e.getMessage(), e); } } }
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
总结
本文总结了事务失效的场景,其实发生最多就是自身调用、异常被吃、异常抛出类型不对这三个。平常使用的时候一定要注意下。