前言:在日常的开发过程中,多多少少会遇到Spring事务失效导致的一些事故,本篇主要通过具体的案例分析来讲解常见的8种失效的场景,让阅读者通俗易懂的明白每一种事务失效的原因,知其然并知其所以然!
目录
一、未指定回滚异常
二、异常被捕获
三、方法内部直接调用
四、异步多线程
五、使用了错误的事务传播机制
六、方法被private或者final修饰
七、当前类没有被Spring容器托管
八、数据库不支持事务
九、总结
一、未指定回滚异常
@Transactional注解默认的回滚异常类型是运行时异常(RuntimeException),如果我们自定义了一个异常直接继承了Exception,代码如下:
public class CustomException extends Exception{}
如果@Transactional未指定异常,当程序中抛出CustomException异常则不会回滚,测试代码如下:
@Transactional
public void insert() throws CustomException {
Notice notice = new Notice();
notice.setId(UUID.randomUUID().toString());
notice.setTitle("《发布关于新版本更新的通知》");
notice.setAuthor("管理员");
notice.setContent("******");
History history = new History();
history.setId(UUID.randomUUID().toString());
history.setContent(notice.toString());
noticeMapper.insert(notice);
historyMapper.insert(history);
throw new CustomException();
}
运行结果如下:
虽然程序当中抛出了异常,但是数据库还是成功入库了,这样显然是不合理的!
所以我们需要在@Transactional指定回滚异常的类型,遇到异常就要回滚:@Transactional(rollbackFor = Exception.class)
二、异常被捕获
当抛出的异常被try-catch捕获时,事务也会失效,具体看代码:
@Transactional(rollbackFor = Exception.class)
public void insert(){
try {
Notice notice = new Notice();
notice.setId(UUID.randomUUID().toString());
notice.setTitle("《发布关于新版本更新的通知》");
notice.setAuthor("管理员");
notice.setContent("******");
History history = new History();
history.setId(UUID.randomUUID().toString());
history.setContent(notice.toString());
noticeMapper.insert(notice);
historyMapper.insert(history);
throw new CustomException();
}catch (Exception e){
e.printStackTrace();
}
}
运行结果如下:
数据还是成功入库了,明显不合理!
所以我们需要主动将此异常抛出: throws CustomException。
我们也可以修改catch包裹的代码,以此来达到回滚的目的。
catch (Exception e){
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
三、方法内部直接调用
在Spring的Aop代理下,只有目标方法在外部进行调用,目标方法才会由Spring生成的代理对象来进行管理,如果是其他不包含@Transactional注解的方法中调用包含@Transactional注解的方法时候,有@Transactional注解的方法的事务会被忽略,则不会发生回滚。
public void insert() throws CustomException {
doSomething();
}
@Transactional(rollbackFor = Exception.class)
public void doSomething() throws CustomException {
Notice notice = new Notice();
notice.setId(UUID.randomUUID().toString());
notice.setTitle("《发布关于新版本更新的通知》");
notice.setAuthor("管理员");
notice.setContent("******");
History history = new History();
history.setId(UUID.randomUUID().toString());
history.setContent(notice.toString());
noticeMapper.insert(notice);
historyMapper.insert(history);
throw new CustomException();
}
运行结果如下:
数据也成功入库了,明显不合理!
只要在insert方法上面加上@Transactional注解即可。
@Transactional(rollbackFor = Exception.class)
public void insert() throws CustomException {
doSomething();
}
四、异步多线程
这里我撰写了一个新的myService2用于保存history对象,并在myService2的方法上加上了@Async的注解,并休眠了5s。
注:主启动类需要加上@EnableAsync
@Async
public void save(Notice notice) throws CustomException, InterruptedException {
System.out.println("异步任务开始...");
History history = new History();
history.setId(UUID.randomUUID().toString());
history.setContent(notice.toString());
historyMapper.insert(history);
Thread.sleep(50000);
System.out.println("异步任务结束...");
}
在2个插入操作都执行完毕以后,我主动抛出一个异常。
@Transactional(rollbackFor = Exception.class)
public void insert() throws CustomException, InterruptedException {
Notice notice = new Notice();
notice.setId(UUID.randomUUID().toString());
notice.setTitle("《发布关于新版本更新的通知》");
notice.setAuthor("管理员");
notice.setContent("******");
noticeMapper.insert(notice);
myService2.save(notice);
int a = 1/0;
}
运行结果如下:
notice没有入库,history入库了
这是因为@Async注解使用的是独立线程和独立的事务,和notice的不处于同一个事务(指的是公用的同一个数据库链接)当中,所以notice回滚了,但是history入库了。
五、使用了错误的事务传播机制
先简单介绍一下Spring事务的7种传播机制
PROPAGATION_REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 |
PROPAGATION_SUPPORTS | 如果当前存在事务,则加入该事务;如果没有事务,则以非事务方式继续运行 |
PROPAGATION_MANDATORY | 必须运行在已存在的事务中,否则抛出异常 |
PROPAGATION_REQUIRES_NEW | 创建一个新事务,如果已经存在一个事务,则把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则等同于`PROPAGATION_REQUIRED |
这边我使用的是PROPAGATION_REQUIRES_NEW的传播机制。
MyService2代码如下:
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void save(Notice notice){
History history = new History();
history.setId(UUID.randomUUID().toString());
history.setContent(notice.toString());
historyMapper.insert(history);
}
依旧在2个插入操作后抛出异常:
@Transactional(rollbackFor = Exception.class)
public void insert() throws CustomException {
Notice notice = new Notice();
notice.setId(UUID.randomUUID().toString());
notice.setTitle("《发布关于新版本更新的通知》");
notice.setAuthor("管理员");
notice.setContent("******");
noticeMapper.insert(notice);
myService2.save(notice);
throw new CustomException();
}
运行结果如下:
notice的信息入库失败,但是history成功入库了。
这是因为PROPAGATION_REQUIRES_NEW使得notice和history公用的不是同一个数据库链接,事务都是独立开来的。
六、方法被private或者final修饰
这种情况下,事务也是会失效的。
@Transactional(rollbackFor = Exception.class)
private void insert() throws CustomException {
}
@Transactional(rollbackFor = Exception.class)
public final void insert() throws CustomException {
}
七、当前类没有被Spring容器托管
在当前实体类上面要打上@Service注解,否则项目启动时也会报错,不多做阐述。
//@Service
public class MyService {
@Resource
private HistoryMapper historyMapper;
@Resource
private NoticeMapper noticeMapper;
@Transactional(rollbackFor = Exception.class)
public void insert() throws CustomException {
}
}
八、数据库不支持事务
比如Mysql的Myisam存储引擎是不支持事务的,只有innodb存储引擎才支持。 这个问题出现的概率极其小,了解一下。
九、总结
这就是目前日常开发当中我总结的Spring事务常见的8种失效场景,如有遗漏,欢迎评论区补充!