环境
jdk1.8 + springboot 2.1.0.RELEASE+mysql 8 innerDB存储引擎
正常在数据插入一条数据
抛出checked异常
public ApiResult updateUser( UserParams user) throws Exception {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老三");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
try {
// 模拟异常发生场景
int a = 1 / 0;
}catch (ArithmeticException exception){
throw new Exception("发生异常了~");
}
return new ApiResult().success(res);
}
此时虽然报了异常,但事务并未生效
数据库数据依然被改为了‘张老三’
原因
Spring的事务只支持未检查异常(unchecked),不支持已检查异常(checked)
下图中的蓝色部分是未检查异常,红色部分中Exception的除RuntimeException之外的其他子类是已检查异常
特别说明,开发过程中比较常见的就是SQLException。通过查看SQLException的继承关系可以看出,SQLException不属于未检查异常,所以SQLException的抛出不会导致事务回滚。
解决方案
捕获异常,手动回滚
public ApiResult updateUser( UserParams user) throws Exception {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
try {
// 模拟异常发生场景
int a = 1 / 0;
}catch (ArithmeticException exception){
//手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//throw new Exception("发生异常了~");
}
return new ApiResult().success(res);
}
抛出一个事务支持回滚的异常
捕获异常然后抛出一个事务支持回滚的异常,比如RuntimeException或者自定义继承RuntimeException的子类等。
public ApiResult updateUser( UserParams user) throws Exception {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
try {
// 模拟异常发生场景
int a = 1 / 0;
} catch (ArithmeticException exception) {
//抛出unchecked异常
throw new RuntimeException("发生异常了~");
}
return new ApiResult().success(res);
}
rollbackFor 指定回滚的异常类型
在注解上指定回滚的异常类型,@Transactional(rollbackFor = Exception.class)
但要注意抛出的异常和指定的异常类型保持一致,抛出的异常可以是指定异常的子类,反过来就不行。
比如指定Exception.clas,抛出IOException ,事务会生效
但如果指定IOException,而抛出Exception,事务不会生效
rollbackFor = Exception.class)(
public ApiResult updateUser( UserParams user) throws Exception {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
try {
// 模拟异常发生场景
int a = 1 / 0;
} catch (ArithmeticException exception) {
throw new Exception("发生异常了~");
}
return new ApiResult().success(res);
}
catch掉异常后未处理
rollbackFor = Exception.class)(
public ApiResult updateUser( UserParams user) throws Exception {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
try {
// 模拟异常发生场景
int a = 1 / 0;
} catch (ArithmeticException exception) {
//未处理异常
System.out.println("发生了异常");
}
return new ApiResult().success(res);
}
原因
异常被化解了,事务无法捕捉到异常的发生,事务自然无法生效
解决方案
不化解异常
捕获异常后需要手动处理事务,或者抛出unchecked异常,具体参考上一个场景的解决方案。
注解加在非public方法上
//非public修饰,包括private/protected
private ApiResult updateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
ArithmeticException继承自RuntimeException,虽然产生的是uncheck异常,但事务依然失效;
原因
sping源码显示:不支持非public方法添加事务。
Don't allow non-public methods, as configured.
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
解决方法
改成public修饰就好了。
注解加在final、static所修饰的方法上
原因
spring事务的实现依赖动态代理,而被final、static修饰的方法无法被重写,也就无法织入事务方法,所以事务会失效,在我这个环境中无法模拟,当方法被final修饰是,sysUserMapper会为null,执行会NullPointerException
public final ApiResult updateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
而如果被static修饰,则语法检查通不过,sysUserMapper是非静态变量,如果将sysUserMapper也改为static修饰,语法检查通过,但依旧是报NullPointerException
public static ApiResult updateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
解决方法
避免上述情况,将方法修饰符改为public。
同一类中无注解方法调用有注解方法
public ApiResult updateUser( UserParams user) {
return dealUpdateUser(user);
}
public ApiResult dealUpdateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老四");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
原因
还是因为@Transactional实现依靠的是AOP,而AOP实现原理是动态代理,同一个类中方法调用,相当于this去调用,自己调用自己,没有中间商赚差价,没有产生代理对象,所以事务也就没有生效。
解决方案
解决思路就是让AOP生效,产生代理对象。
注入本身
("/api/test")
public class TransactionTest {
SysUserMapper sysUserMapper;
TransactionTest transactionTest;//注入自己
("/add")
public ApiResult updateUser( UserParams user) {
// 注入自己,用注入的对象去调用注解方法
return transactionTest.dealUpdateUser(user);
}
public ApiResult dealUpdateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老三");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
}
方法拆分到不同类中
将dealUpdateUser方法拆分到另一个类TransactionTestSub中
public class TransactionTestSub {
SysUserMapper sysUserMapper;
public ApiResult dealUpdateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老三");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
}
然后将TransactionTestSub注入TransactionTest,进行调用
public class TransactionTest {
TransactionTestSub transactionTestSub;
("/update")
public ApiResult updateUser( UserParams user) {
return transactionTestSub.dealUpdateUser(user);
}
}
AopContext生成代理对象
public ApiResult updateUser( UserParams user) {
// 获取代理类,用代理类调用
TransactionTest transactionTest = (TransactionTest)AopContext.currentProxy();
return transactionTest.dealUpdateUser(user);
}
public ApiResult dealUpdateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老三");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
注解转移
将被调用方法上的注解加到调用方法上,被调用方法抛出unchecked异常
//调用方法加上注解
@Transactional
public ApiResult updateUser(@RequestBody UserParams user) {
return dealUpdateUser(user);
}
//将被调用方法上的注解去掉
//@Transactional
public ApiResult dealUpdateUser(@RequestBody UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老三");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
try {
// 模拟异常发生场景
int a = 1 / 0;
} catch (Exception e) {
//被调用方法抛出unchecked异常
throw new RuntimeException("抛出unchecked");
}
return new ApiResult().success(res);
}
事务传播类型不支持事务
TransactionTestSub类中,dealUpdateUser方法上的 @Transactional注解,传播类型不支持事务
public class TransactionTestSub {
SysUserMapper sysUserMapper;
// 传播类型不支持事务
(propagation = Propagation.NOT_SUPPORTED)
public ApiResult dealUpdateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张老三");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
return new ApiResult().success(res);
}
}
TransactionTest类中updateUser方法上有 @Transactional注解,调用TransactionTestSub的dealUpdateUser方法
此时事务失效。
public class TransactionTest {
SysUserMapper sysUserMapper;
TransactionTestSub transactionTestSub;
("/update")
public ApiResult updateUser( UserParams user) {
return transactionTestSub.dealUpdateUser(user);
}
}
原因
指定不生效,肯定就不会生效。
解决方案
根据业务需要,改成支持事务的传播机制。
不同线程调用
当前类TransactionTest中先操作数据库,再去掉另一个类TransactionTestSub的方法也是操作数据库,但在transactionTestSub操作数据库是新启了一个线程操作,事务失效。
public class TransactionTest {
SysUserMapper sysUserMapper;
TransactionTestSub transactionTestSub;
("/update")
public ApiResult updateUser( UserParams user) {
SysUser sysUser = new SysUser();
sysUser.setUserName("张三丰");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
return transactionTestSub.dealUpdateUser(user);
}
}
public class TransactionTestSub {
SysUserMapper sysUserMapper;
public ApiResult dealUpdateUser( UserParams user) {
new Thread(() -> {
SysUser sysUser = new SysUser();
sysUser.setUserName("张三丰");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);
// 模拟异常发生场景
int a = 1 / 0;
}).start();
return new ApiResult().success();
}
}
原因
spring事务最终靠的是数据库事务的支持,而不同线程用的是不同的连接,故而不在同一个事务中,事务就会失效。
解决方案
相关连的逻辑,尽量放在同一个事务中执行。
总结
以上是在spring环境中,基于mysql 8 innerDB存储引擎事务失效的几个典型场景,其中【同一类中无注解方法调用有注解方法】最容易采坑,也最难排查,要格外留意。
而实际操作中可能还有其他原因导致事务失效,比如:事务方法所在类未被Spring容器管理、数据库引擎不支持事务(myisam)等等。
时刻牢记spring事务是基于AOP的,AOP又是基于动态代理实现的,而事务底层还是依赖数据库来实现,深刻理解了aop、动态代理和数据库事务的机制,就能避免绝大部分事务失效问题。