目录
- Spring事务失效问题
- 开场
- 问题
- Spring事务失效的原因
- 排查bug
- 解决方案
- 事务的传播属性
- 总结
Spring事务失效问题
开场
Spring事务管理,在Service实现类的方法上添加@Transactional注解就能进行事务控制,但是最近遇到一个事务失效的问题,浅浅分析一下
问题
在开发过程中发现这样一个bug,有一段逻辑代码:商品入库、并且生成一个入库单的操作;首先肯定的是需要操作两张表,一是 入库加库存,二是 新增一个入库单类似于入库记录。但是测试时候遇到一个问题,有一个商品入库成功了,库存也加进去了,但是没有生成入库单,我立马想到的是难道是事务没有控制好?翻看接口代码发现还真是。。。
Spring事务失效的原因
我们都知道@Transactional失效的原因有多种,列举一下,后面有时间再仔细分析研究
1.方法访问修饰符:
- @Transactional注解只对public方法生效。
- 如果事务性方法是private、protected,事务不会生效
2.类内部方法调用:
- 在同一个类中调用标注了**
@Transactional**的方法(即自调用)时,事务管理器不会介入,因为Spring AOP代理无法拦截内部调用
3.没有启用事务管理:
- 必须显式启用Spring的事务管理,通常使用**@EnableTransactionManagement**注解在配置类上,或者在XML中配置使用
<tx:annotation-driven/>
4.事务传播属性不当:
- 事务传播属性(Propagetion)配置不当会导致事务失效。
- 例如:传播行为
REQUIRES_NEW会挂起当前事务,创建一个新事物,这可能不是我们想要的
5.异常处理不当:
- 默认情况下,Spring事务管理只在运行时异常(RuntimeExpection及其子类)和错误(Error及其子类)时回滚。
- 如果捕获并处理了一场,但没有重新抛出,事务不会回滚。此外需要注意,对于检查异常(Exception及其子类),需要制定rollbackFor属性来触发事务回滚,例如:
@Transactional(rollbackFor = Exception.class)
6.多线程环境:
- Transactional 注解无法再多线程环境中传播事务。
- Spring事务管理依赖于线程局部变量(ThreadLocal),在不同献策会给你之间共享事务需要显式处理。(后面遇到的话仔细研究一下)
7.数据源和事务管理器配置不一致:
- 确保数据源(DataSource)和事务管理器(TransactionManager)配置一致。
- 如果有多个数据源编程客栈和事务管理器,需要明确指定使用的事务管理器。
8.Spring代理模式限制:
- Spring默认使用AOP代理来管理事务。
- 默认情况下使用JDK动态代理,只有实现接口的类可以使用事务。
- 如果没有实现接口,需要使用CGLIB代理,通过proxyTargetClass=true启用CGLIB代理。
9.异步方法:
- 一步方法(如使用@Async注解的方法)运行在独立线程中,不会参与当前线程的事务管理。
- 需要特别处理异步方法的事务管理。
排查bug
言归正传,我在这段逻辑代码中发现有两个问题,首先代码块被 try catch 捕获后没有重新抛出异常,而且这段代码在catch 里面有日志记录,插入到一张日志表;业务逻辑太多此处就不贴真实代码了,但是我写了一个demo是可以复刻问题的。
代码如下:
@Transactional(rollbackFor = Exception.class)
public User update(User userDto) {
try {
//查询用户
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::get编程客栈Id, userDto.getId());
User user = userDao.selectOne(queryWrapper);
//修改数据:扣钱
user.setMoney(user.getMoney().subtract(userDto.getMoney()));
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
userDao.update(user);
//模拟异常
int i = 1/0;
} catch (Exception e) {
log.error(e.getMessage(),e);
//日志记录:将错误信息插入到日志表
insertLog(userDto, e);
}
return null;
}
/*
* 日志记录的方法
*/
public void recordLog(Exception e) {
LogHistory logHistory = new LogHistory();
logHistory.setCode("1");
logHistory.setTime(jsnew Date());
logHistory.setErrorMsg(e.getMessage());
logHistoryMapper.insert(logHistory);
}
上面这段代码,大家还有发现问题其他吗,细心的小伙伴一定还能发现其他问题,那就是这个 catch 捕获异常后并没有抛出,所以这段代码并不会回滚,但是问题来了,如果我们在 catch 中最后抛出异常,那么日志记录的操作也将回滚,这里就会有点冲突,如果代码有异常报错,我们的目的就是要回滚python,但是还不能把日志记录给回滚。
解决方案
发现了问题,该如何去解决呢?
在这里我用到了一个事务的传播行为:先说结论,将事务的传播属性设置为REQUIRES_NEW,就是在当前事务中新建了一个事务,用新的事务去控制我日志记录的操作,这样的话两个事务互不影响,A事务如果抛出异常,不影响B事务,B事务正常插入数据,完美解决!当然不止这一种解决方案,还有异步调用,mq发消息等方式。
但是需要注意,我们需要在单独的Service实现类中去编写日志记录的方法,不能在A事务中去编写了,代码如下:
@Transactional(rollbackFor = Exception.class)
public User update(User userDto) {
try {
//查询用户
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
User user = userDao.selectOne(queryWrapper);
//修改数据:扣钱
user.setMoney(user.getMoney().subtract(userDto.getMoney()));
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
userDao.update(user);
//模拟异常
int i = 1/0;
} catch (Exception e) {
log.error(e.getMessage(),e);
//日志记录:需要在另外的service中编写
logHistoryService.insertLog(userDto, e);
//抛出异常
throw e;
}
return null;
}
/*
* 日志记录
* 此处我们需要设置事务的传播属性为 REQUIRES_NEW
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public LogHistory insertLog(User userDto, Exception e) {
LogHistory logHistory = new LogHistory();
logHistory.setCode("111");
logHistory.setTime(new Date());
logHistory.setErrorMsg(e.getMessage());
logHistoryMapper.insert(logHistory);
return logHistpythonory;
}
事务的传播属性
在 Spring 中,事务的传播属性(Propagation)决定了事务的行为在方法间调用时的传播方式。传播属性通过 @Transactional 注解的 propagation 属性来设置,Spring 提供了以下几种传播属性:
1.REQUIRED(默认值):
- 如果当前已经存在一个事务,则加入该事务。
- 如果当前没有事务,则创建一个新的事务。
- 这是最常见的传播行为。
2.REQUIRES_NEW:
- 无论当前是否存在事务,总是创建一个新的事务。
- 如果当前存在事务,则挂起当前事务,直到新事务完成。
3.SUPPORTS:
- 如果当前存在事务,则加入该事务。
- 如果当前没有事务,则以非事务方式执行。
4.NOT_SUPPORTED:
- 以非事务方式执行操作。
- 如果当前存在事务,则挂起当前事务,直到当前操作完成。
5.MANDATORY:
- 必须在一个现有事务中运行。
- 如果当前没有事务,则抛出异常。
6.NEVER:
- 必须在非事务上下文中运行。
- 如果当前存在事务,则抛出异常。
7.NESTED:
- 如果当前存在事务,则在嵌套事务中运行。
- 如果当前没有事务,则创建一个新的事务。
- 嵌套事务使用保存点(savepoint),如果嵌套事务回滚,它只回滚到保存点,外部事务可以继续。
- 如果当前没有事务,则抛出异常。
8.NEVER:
- 必须在非事务上下文中运行。
- 如果当前存在事务,则抛出异常。
9.NESTED:
- 如果当前存在事务,则在嵌套事务中运行。
- 如果当前没有事务,则创建一个新的事务。
- 嵌套事务使用保存点(savepoint),如果嵌套事务回滚,它只回滚到保存点,外部事务可以继续。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
加载中,请稍侯......
精彩评论