记录一次springboot中事务失效场景+几大失效场景(防止后面入坑)
遇到的坑
背景
最近一次开发中遇到Springboot中事务失效的问题,具体场景如下,此场景事务失效的原因较为常见,主要是记录一下其他失效情况:
导入了一个比较复杂的xml文件数据然后转化为java实体类接收,接收后需要处理数据进行入库,然后需要操作到4个表中的数据,但是发现执行过程中又出现异常,但是数据正常入库了。
代码贴一下
@Service
@AllArgsConstructor
@Transactional
public class BusinessXmlServiceImpl {
private final SnowPlayerService playerService;
private final SnowEventService eventService;
private final SnowScoreService scoreService;
private final SnowScoreItemService itemService;
public JsonResult importXml(MultipartFile file) {
// 利用输入流获取XML文件内容
StringBuffer buffer;
try (BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
buffer = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
// XML转为JAVA对象
OdfBody odfBody = (OdfBody) XmlBuilder.xmlStrToObject(OdfBody.class, buffer.toString());
// 处理数据入库
handlerIntoDB(odfBody);
} catch (Exception e) {
e.printStackTrace();
}
return JsonResult.SUCCESS;
}
private void handlerIntoDB(OdfBody odfBody) {
// 赛事信息
SnowEventEntity eventEntity = new SnowEventEntity(odfBody);
// 保存赛事
eventService.save(eventEntity);
// 制造一个异常
int i = 5 / 0;
// 处理具体信息
List<Result> resultList = odfBody.getCompetition().getResultList();
if (CollectionUtils.isNotEmpty(resultList)) {
// 各种入库操作
}
}
}
问题复现
捕获到异常,正常打印了堆栈信息,但是前面的数据操作入库了。
以上代码相信各位一眼就看出来了,原因其实就是使用了try-catch
异常捕获,然后把异常给吃掉了,只打印了堆栈信息,改进一下:
解决
public JsonResult importXml(MultipartFile file) {
// 利用输入流获取XML文件内容
StringBuffer buffer;
try (BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
buffer = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
// XML转为JAVA对象
OdfBody odfBody = (OdfBody) XmlBuilder.xmlStrToObject(OdfBody.class, buffer.toString());
// 处理数据入库
handlerIntoDB(odfBody);
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException("数据入库失败");
}
return JsonResult.SUCCESS;
}
捕获异常后不要将异常吃掉,直接手动抛出,让事务捕获到异常,进行数据回滚,且可以全局捕获异常并将错误信息返回给客户端。
效果
捕获到异常
数据也没有入库了
记录下其他失效场景,避免入坑
总览图
SpringBoot中使用事务一个注解@Transactional
即可解决,但是也要注意会导致事务失效的一些场景。
-
使用事务的方法的访问级别是
private
的,spring要求被代理方法必须是public
的,而且@Transactional
声明在方法上时,开发工具基本上都会编译报错 -
方法使用了
final
修饰,Spring事务底层使用了Aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现了事务功能。而如果使用final修饰了,那他的代理类就无法重写该方法,无法添加事务功能
。 -
如果某个方法是static的,同样无法通过动态代理,变成事务方法。
-
内部方法调用:在同一个类中,一个非事务方法直接调用本类的事务方法时,事务将会失效;而如果一个事务方法调用本类或其他类的非事务/事务方法时,调用的其他方法都会被添加事务。
@Service public class UserService { @Autowired private UserMapper userMapper; public void add(UserModel userModel) { userMapper.insertUser(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { .... } } /* 以上,add方法是一个非事务方法,但是调用的是本类的事务方法,如果直接调用,那么将会直接调用本类的updateStatus方法,不会去走代理类的代理方法,会造成事务失效。 解决方式有很多(方法1比较简单): 1.可以将add()方法也声明为事务方法(用@Transactional修饰) 2.在上文的UserService类中注入自己,不会有循环依赖问题的 ... */
-
如果类没有反转到spring容器(未被spring管理),那么声明
@Transactional
是无用的,无法生成代理类 -
假如是使用了多线程,开启不同的线程任务去操作数据库,那么这样的话,只能保证单个线程中的事务是有效的,也就是多个不同的线程中获取到的数据库连接是不一样的,从属于不同的事务中。
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { doOtherThing(); }).start(); } @Transactional public void doOtherThing() { System.out.println("保存表数据"); } }
我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的
-
注意数据库的存储引擎,
innodb
才支持事务 -
异常不要自己吃掉
-
如果捕获了异常,请正确抛出异常,在spring的事务中,默认只会回滚
RuntimeException
和Error
,对于受查异常是不会进行回滚的。 -
自定义的回滚异常:其实也就是声明遇到某种类型的异常时才进行回滚,而程序报错时基本是抛出
SqlException
或DuplicateKeyException
等异常,像我们自定义的异常一般情况不会被抛出。@Transactional(rollbackFor = Exception.class) public class BusinessXmlServiceImpl { .. } // @Transactional中有个rollcackFor属性,默认是UncheckedException,也就是非受查异常(包含RuntimeException和Error // 阿里开发规范中建议:要求开发者重新指定该参数,声明为Exception或Throwable即可。)
特殊情况
一般情况下,我们希望事务中所有的操作的原子性,也就是所有的操作单元也么都执行成功,要么都执行失败。但是如果遇到这样的场景时:事务中有两个数据库操作,我们希望就算后一个执行失败了,但是前一个操作如果执行成功了不希望回滚,感觉有点难遇到这样的需求,打破了原子性,但是也可能遇到。
@Service
@AllArgsConstructor
@Transactional
public class BusinessXmlServiceImpl {
private final SnowPlayerService playerService;
private final SnowScoreService scoreService;
public void add(SnowPlayerEntity playerEntity, SnowScoreEntity scoreEntity) {
playerService.save(playerEntity);
scoreService.save(scoreEntity);
}
}
上块代码中,就是通过一个事务方法调用其他的非事务方法,由于事务的传播性,其他非事务方法都会被加到事务中,确保执行的原子性
。
如果要打破原子性,满足上文不会滚后一个原子操作,也非常简单,那么这么写即可。
@Service
@AllArgsConstructor
@Transactional
public class BusinessXmlServiceImpl {
private final SnowPlayerService playerService;
private final SnowScoreService scoreService;
public void add(SnowPlayerEntity playerEntity, SnowScoreEntity scoreEntity) {
playerService.save(playerEntity);
try {
scoreService.save(scoreEntity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
偷偷把后一个方法的异常吃掉即可。O(∩_∩)O哈哈~
简单介绍几种失效和回滚失败的场景,详细可以参考下面文章。
参考
详细可参考文章Spring boot事务不生效