0
点赞
收藏
分享

微信扫一扫

Spring @Transactional注解失效场景重现

环境

jdk1.8 + springboot 2.1.0.RELEASE+mysql 8 innerDB存储引擎

正常在数据插入一条数据

Spring @Transactional注解失效场景重现_后端

抛出checked异常

@Transactional
public ApiResult updateUser(@RequestBody 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 @Transactional注解失效场景重现_后端_02

原因

Spring的事务只支持未检查异常(unchecked),不支持已检查异常(checked)

下图中的蓝色部分是未检查异常,红色部分中Exception的除RuntimeException之外的其他子类是已检查异常

Spring @Transactional注解失效场景重现_mysql_03

特别说明,开发过程中比较常见的就是SQLException。通过查看SQLException的继承关系可以看出,SQLException不属于未检查异常,所以SQLException的抛出不会导致事务回滚

Spring @Transactional注解失效场景重现_数据库_04

解决方案

捕获异常,手动回滚

@Transactional
public ApiResult updateUser(@RequestBody 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的子类等。

@Transactional
public ApiResult updateUser(@RequestBody 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,事务不会生效

@Transactional(rollbackFor = Exception.class)
public ApiResult updateUser(@RequestBody 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掉异常后未处理

@Transactional(rollbackFor = Exception.class)
public ApiResult updateUser(@RequestBody 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方法上

@Transactional
//非public修饰,包括private/protected
private ApiResult updateUser(@RequestBody 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.

@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable 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

@Transactional
public final ApiResult updateUser(@RequestBody 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

@Transactional
public static ApiResult updateUser(@RequestBody 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(@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);

// 模拟异常发生场景
int a = 1 / 0;

return new ApiResult().success(res);
}

原因

还是因为@Transactional实现依靠的是AOP,而AOP实现原理是动态代理,同一个类中方法调用,相当于this去调用,自己调用自己,没有中间商赚差价,没有产生代理对象,所以事务也就没有生效。

解决方案

解决思路就是让AOP生效,产生代理对象。

注入本身

@RestController
@RequestMapping("/api/test")
public class TransactionTest {


@Autowired
SysUserMapper sysUserMapper;

@Autowired
TransactionTest transactionTest;//注入自己

@PostMapping("/add")
public ApiResult updateUser(@RequestBody UserParams user) {

// 注入自己,用注入的对象去调用注解方法
return transactionTest.dealUpdateUser(user);
}


@Transactional
public ApiResult dealUpdateUser(@RequestBody 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 {
@Autowired
SysUserMapper sysUserMapper;

@Transactional
public ApiResult dealUpdateUser(@RequestBody 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 {


@Autowired
TransactionTestSub transactionTestSub;


@PostMapping("/update")
public ApiResult updateUser(@RequestBody UserParams user) {

return transactionTestSub.dealUpdateUser(user);
}


}

AopContext生成代理对象

public ApiResult updateUser(@RequestBody UserParams user) {

// 获取代理类,用代理类调用
TransactionTest transactionTest = (TransactionTest)AopContext.currentProxy();

return transactionTest.dealUpdateUser(user);
}

@Transactional
public ApiResult dealUpdateUser(@RequestBody 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注解,传播类型不支持事务

@Service
public class TransactionTestSub {
@Autowired
SysUserMapper sysUserMapper;

// 传播类型不支持事务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public ApiResult dealUpdateUser(@RequestBody 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 {

@Autowired
SysUserMapper sysUserMapper;
@Autowired
TransactionTestSub transactionTestSub;


@PostMapping("/update")
@Transactional
public ApiResult updateUser(@RequestBody UserParams user) {

return transactionTestSub.dealUpdateUser(user);
}
}

原因

指定不生效,肯定就不会生效。

解决方案

根据业务需要,改成支持事务的传播机制。

不同线程调用

当前类TransactionTest中先操作数据库,再去掉另一个类TransactionTestSub的方法也是操作数据库,但在transactionTestSub操作数据库是新启了一个线程操作,事务失效。

public class TransactionTest {

@Autowired
SysUserMapper sysUserMapper;
@Autowired
TransactionTestSub transactionTestSub;


@PostMapping("/update")
public ApiResult updateUser(@RequestBody UserParams user) {

SysUser sysUser = new SysUser();
sysUser.setUserName("张三丰");
sysUser.setUserID(1L);
//操作数据库
Integer res = sysUserMapper.updateById(sysUser);


return transactionTestSub.dealUpdateUser(user);
}
}
@Service
public class TransactionTestSub {
@Autowired
SysUserMapper sysUserMapper;

@Transactional
public ApiResult dealUpdateUser(@RequestBody 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、动态代理和数据库事务的机制,就能避免绝大部分事务失效问题。


举报

相关推荐

0 条评论