前言:在日常的开发过程中,多多少少会遇到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种失效场景,如有遗漏,欢迎评论区补充!



















