前言
@Transactional
是一种基于注解管理事务的方式,spring通过动态代理的方式为目标方法实现事务管理的增强。
@Transactional
使用起来方便,但也需要注意引起@Transactional
失效的场景,本文总结了七种情况,下面进行逐一分析。
一、异常被捕获后没有抛出
当异常被捕获后,并且没有再抛出,那么deleteUserA
是不会回滚的。
@Transactional
public void deleteUser() {
userMapper.deleteUserA();
try {
int i = 1 / 0;
userMapper.deleteUserB();
} catch (Exception e) {
e.printStackTrace();
}
}
二、抛出非运行时异常
Spring 事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。
@Transactional
public void deleteUser() throws MyException{
userMapper.deleteUserA();
try {
int i = 1 / 0;
userMapper.deleteUserB();
} catch (Exception e) {
throw new MyException();
}
}
如果事务注解使用的是@Transactional(rollbackFor = Exception.class),那么抛出的是非RuntimeException类型异常是可以回滚的。
@Transactional(rollbackFor = Exception.class)
三、方法内部直接调用
这种场景很常见,方法A调用方法B,其中方法A未使用事务,而方法B使用了事务,此时方法B的事务是不生效的。例子如下,如果先调用deleteUser()
,那么deleteUserA()
是不会回滚的,其原因就是@Transactional
根本没生成代理,如果直接调用deleteUser2()
那么没问题,deleteUserA()
会回滚。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void deleteUser() throws MyException{
deleteUser2();
}
@Transactional
public void deleteUser2() throws MyException{
userMapper.deleteUserA();
int i = 1 / 0;
userMapper.deleteUserB();
}
}
1. 修改调用方式,把当前类自己注入一下调用即可。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
//自己注入自己
@Autowired
UserService userService;
public void deleteUser() throws MyException{
userService.deleteUser2();
}
@Transactional
public void deleteUser2() throws MyException{
userMapper.deleteUserA();
int i = 1 / 0;
userMapper.deleteUserB();
}
}
2. 新加一个service方法,只需要新加一个 Service 方法,把 @Transactional 注解加到新 Service 方法上,把需要事务执行的代码移到新方法中
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
四、新开启一个线程
如下的方式deleteUserA()
也不会回滚,因为spring实现事务的原理是通过ThreadLocal把数据库连接绑定到当前线程中,新开启一个线程获取到的连接就不是同一个了。
@Transactional
public void deleteUser() throws MyException{
userMapper.deleteUserA();
try {
//休眠1秒,保证deleteUserA先执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
int i = 1/0;
userMapper.deleteUserB();
}).start();
}
五、访问权限问题
java 的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。如果方法的访问权限被定义成了private,这样会导致事务失效,spring 要求被代理方法必须是public的。
@Transactional
private void deleteUser() throws MyException{
userMapper.deleteUserA();
int i = 1/0;
userMapper.deleteUserB();
}
六、方法被final修饰
spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
@Transactional
public final void deleteUser() throws MyException{
userMapper.deleteUserA();
int i = 1/0;
userMapper.deleteUserB();
}
注意:如果某个方法是 static修饰的,同样无法通过动态代理,变成事务方法。
七、数据库本身不支持
在 mysql5 之前,默认的数据库引擎是myisam。它的缺点就是不支持事务,因此在mysql5之后,必须设置数据库引擎为InnoDB。
八、未被Spring管理
在我们平时开发过程中,有个细节很容易被忽略,即使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例。通常情况下,我们通过 @Controller、@Service、@Component、@Repository 等注解,可以自动实现 bean 实例化和依赖注入的功能。如果有一天,你匆匆忙忙地开发了一个 Service 类,但忘了加 @Service 注解,那么该类不会交给 spring 管理,所以它内部的方法也不会生成事务。
九、事务传播属性设置错误
我们在使用@Transactional注解时,是可以指定propagation参数的。
该参数的作用是指定事务的传播特性,spring 目前支持 7 种传播特性:
REQUIRED 如果当前上下文中存在事务,则加入该事务,如果不存在事务,则创建一个事务,这是默认的传播属性值。
SUPPORTS 如果当前上下文中存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
MANDATORY 当前上下文中必须存在事务,否则抛出异常。
REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。设置其他传播特性都不会创建事务。