目录标题
- 1. 代理不生效
- 1.1 将注解标注在接口方法上
- 1.2 被final、static关键字修饰的类或方法
- 1.3 类方法内部调用
- 示例
- 解决方案:新加一个Service方法
- 1.4 (类本身) 未被spring管理
- 2. 框架或底层不支持的功能
- 2.1 非public修饰的方法
- 2.2 多线程调用
- 举例1
- 举例2
- 2.3 数据库本身不支持事务
- 3. 失效场景集三:错误使用@Transactional
- 传播特性
- 3.1 传播特性设置错
- 3.2 rollbackFor属性设置错误
- 3.3 异常被内部catch
- 3.4 嵌套事务
1. 代理不生效
Spring中对注解解析都是基于代理的,如果目标方法无法被Spring代理到,那么它将无法被Spring进行事务管理。
Spring生成代理的方式有两种:
基于接口的JDK动态代理,要求目标代理类需要实现一个接口才能被代理
基于实现目标类子类的CGLIB代理
1.1 将注解标注在接口方法上
@Transactional是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到@Transactional,从而事务失效。
这种错误我们还是犯得比较少的,基本上我们都会将注解标注在接口的实现类方法上,官方也不推荐这种。
1.2 被final、static关键字修饰的类或方法
CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰后,那么在它的代理类中,就无法重写该方法,而添加事务功能。
1.3 类方法内部调用
如果是方法内部调用,将不会走代理逻辑,也就调用不到了。
示例
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
解决方案:新加一个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();
}
}
1.4 (类本身) 未被spring管理
在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
如下所示, 开发了一个Service类,但忘了加@Service注解,比如:
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
从上面的例子,我们可以看到UserService类没有加@Service注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。
2. 框架或底层不支持的功能
2.1 非public修饰的方法
自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。
众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
// Don't allow no-public methods as required.可以看到, 这里不支持public类型的方法
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
2.2 多线程调用
举例1
主线程A调用线程B保存Id为1的数据,然后主线程A等待线程B执行完成再通过线程A查询id为1的数据。
这时你会发现在主线程A中无法查询到id为1的数据。因为这两个线程在不同的Spring事务中,本质上会导致它们在Mysql中存在不同的事务中。
Mysql中通过MVCC保证了线程在快照读时只读取小于当前事务号的数据,在线程B显然事务号是大于线程A的,因此查询不到数据。
举例2
@Slf4j
@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(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
2.3 数据库本身不支持事务
比如Mysql的Myisam存储引擎是不支持事务的,只有innodb存储引擎才支持。
3. 失效场景集三:错误使用@Transactional
传播特性
我们在使用@Transactional注解时,是可以指定propagation参数的。
该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:
REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
MANDATORY 如果当前上下文中存在事务,否则抛出异常。
REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
上面不支持事务的传播机制为:PROPAGATION_SUPPORTS,PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER。
这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
3.1 传播特性设置错
如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
3.2 rollbackFor属性设置错误
默认情况下事务仅回滚运行时异常和Error,不回滚受检异常(例如IOException)。
因此如果方法中抛出了IO异常,默认情况下事务也会回滚失败。
我们可以通过指定@Transactional(rollbackFor = Exception.class)的方式进行全异常捕获。
3.3 异常被内部catch
@Slf4j
@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认为程序是正常的。
3.4 嵌套事务
UserService
UserService1
上面是我想同时回滚UserService与UserService1。但是也会有这种场景只想回滚UserService1中报错的数据库操作,不影响主逻辑UserService中的数据落库。
有两种方式可以实现上述逻辑:
1.直接在UserService1内的整个方法用try/catch包住
2.在UserService1使用Propagation.REQUIRES_NEW传播机制
一口气怼完12种@Transactional的失效场景
spring事务(注解 @Transactional )失效的12种场景