1、非运行时异常导致事务无法回滚

我们知道,Spring是通过AOP的方式来实现事务的,而在处理事务的过程中,Spring只有捕获到RuntimeException或者Error的时候才会触发回滚操作,如果我们在代码中抛出的是非运行时异常,而又没有特殊配置的话,事务就会无法回滚。

下面我们以一个简单的例子,复现一下这种情况,以及针对这种情况的解决方案。

本文Springboot版本:2.7.6,数据源为MySQL。


(资料图)

首先创建一个测试用的User对象:

@Datapublic class User {    @TableId(type = IdType.AUTO)    private Integer id;    private String name;    private String pwd;}

建表语句:

CREATE TABLE user  (  `id` int(11) NOT NULL AUTO_INCREMENT,  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,  `pwd` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB;

测试逻辑:往user表插入一条数据,如果插入成功,就抛出exception异常,测试数据是否回滚。

@Service@AllArgsConstructorpublic class DemoService {    private final UserMapper userMapper;      @Transactional    public ResponseEntity addUser(User user) throws Exception {        int insert = this.userMapper.insert(user);        if (insert > 0) {            throw new Exception("异常回滚测试");        }        return ResponseEntity.ok().build();    }}

新建测试方法:

@Testpublic void addUserTest() throws Exception {    User user = new User();    user.setName("测试");    user.setPwd("123456");    this.demoService.addUser(user);}

运行测试方法,从控制台可以看到,我们手动指定的异常被成功抛出。

但是,当异常发生时,事务并没有被回滚,数据依然被插入到了数据库。

解决办法:1,将异常包装成运行时异常:throw new RuntimeException("异常回滚测试");

2,在@Transactional指定回滚的异常类型,@Transactional(rollbackFor = Exception.class)。

一般来说,使用第二种方式会更清晰一些,但是有些朋友往往会忘记手动指定回滚的异常类型,进而导致非预期的bug产生。

2、通过this调用本类事务方法导致的事务无法回滚

随着业务的发展,核心业务代码会越来越多,同一个方法也会越写越长。我们为了使代码逻辑更加高内聚低耦合,会将功能相同的代码进行封装成一个个的子方法。

但是,如果我们对事务的运行机制了解不透彻,随意在同一个类中通过this调用事务方法,就可能导致非预期的bug。

@Service@RequiredArgsConstructorpublic class DemoService {    private final UserMapper userMapper;    public ResponseEntity addUser(User user){        //注意这一行        this.doAddUser(user);        return ResponseEntity.ok().build();    }    @Transactional(rollbackFor = Exception.class)    public void doAddUser(User user) {        int insert = this.userMapper.insert(user);        if (insert > 0) {            throw new RuntimeException("测试添加异常回滚");        }    }}

如以上代码所示,在addUser方法中调用了事务方法doAddUser,如果数据插入成功,就抛出一个异常,测试数据是否能够回滚。

通过测试用例可以看到,异常已经抛出,但是数据库中却成功的插入了数据,我们期望的数据并没有回滚。

原因探究:

原因其实很简单,通过this方法调用时,Spring的代理没能起作用,事务自然也就无法介入,关于这一点的原理在之前的文章中也有分析过,感兴趣的朋友可以去看一看。

有的朋友可能会说,项目的代码已经是这样了,再将老方法重写到新类中也不现实,有没有办法改动较小的方式呢?

其实很简单,现在事务失效的原因是代理失效,那么想办法让代理重新生效就行了。

我们在本类中注入一个当前对象,这个对象可以被Spring代理,那么这个对象的方法自然也可以被代理。

@Service@RequiredArgsConstructorpublic class DemoService {    private final UserMapper userMapper;    @Resource    private DemoService self;    public ResponseEntity addUser(User user){        //通过self引用使代理生效        this.self.doAddUser(user);        return ResponseEntity.ok().build();    }    @Transactional(rollbackFor = Exception.class)    public void doAddUser(User user) {        int insert = this.userMapper.insert(user);        if (insert > 0) {            throw new RuntimeException("测试添加异常回滚");        }    }}3、被声明的事务方法是private类型

这种错误在博主刚工作时遇到挺多次的,不过现在现代IDE已经越来越智能了,对于这种情况会直接给出错误提示,所以这里提出这种错误只是告诉大家,事务方法是不能声明为private的。

至于为什么不能是private,那自然还是和代理有关了。

4、嵌套事务异常导致事务被提前关闭而报错

当使用嵌套事务时,需要明确指定事务的传播范围。

@Service@RequiredArgsConstructorpublic class DemoService {    private final UserMapper userMapper;    @Resource    private DemoService self;    @Transactional(rollbackFor = Exception.class)    public ResponseEntity addUser(User user) {        int insert = this.userMapper.insert(user);        if (insert > 0) {            try {                this.self.update(user);            } catch (Exception e) {                System.out.println("即使更新异常也不要影响添加数据");            }        }        return ResponseEntity.ok().build();    }    @Transactional(rollbackFor = Exception.class)    public void update(User user) {        user.setPwd("666666");        int update = this.userMapper.updateById(user);        if (update > 0) {            throw new RuntimeException("测试更新数据回滚");        }    }}

如以上代码,我们添加完一条数据之后,尝试将密码更新为666666,并且希望即使更新异常,也不要影响添加操作。

然而运行测试用例,我们会得到这样一条错误信息:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。

什么意思呢?就是当Spring处理事务时,发现事务已经被回滚了。

这是因为我们并没有指定事务的传播行为,默认情况下,Spring的事务传播是REQUIRED,即:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。

我们添加数据时启动了一个事务,更新数据时,Spring判断当前已经存在事务,所以就不再新建事务,而是加入当前事务。

但是当更新操作失败时,需要对事务进行回滚,更新是没问题的,正常回滚。

但是插入操作就不行了,当要提交插入操作的事务时,由于事务已经被回滚了,无法再次操作,Spring只好报错来提示我们了。

如何处理呢?在更新操作上指明事务的传播范围就行。

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)public void update(User user){  user.setPwd("666666");  int update = this.userMapper.updateById(user);  if (update > 0) {    throw new RuntimeException("测试更新数据回滚");  }}

再测试一下,发现插入操作的事务可以正常提交了。

总结

事务是我们日常开发工作中无法避免的一个功能,深刻理解事务的运行机制,正确使用事务的声明式操作,才能让我们写出更健壮的代码。

推荐内容

  • 盘点Spring事务失效的4种写法及解决方案,RReview代码再也不慌了

  • Vite 配置篇:日常开发掌握这些配置就够了!

  • 项目终于用上了动态Feign,真香!

  • 逆天了!用Numpy开发深度学习框架,透视神经网络训练过程

  • 世界观点:科大讯飞6英寸智能有声书上架:300PPI墨水屏 1199元

  • 天天关注:小米13性能释放太顶了 雷军:影像被大家忽略了 也非常强

  • 热讯:荣耀80 GT首次官宣:12月26日正式发布

  • 焦点速讯:小米13 Pro为何不用直屏?雷军解释原因

  • 每日视讯:用户认为4G够用?全国5G现状感受下:基站密度翻倍 后续你不得不升

  • 今热点:雷军/卢伟冰熬夜看球 恭喜梅西阿根廷!网友神评:小米赶紧签代言

  • 每日快讯!魅族未来产品规划曝光:3年打造“全家桶”、不止手机和汽车

  • AMD、NVIDIA齐发新品 显卡厂商的好日子来了:加速去库存

  • 除了让苹果赔了三个亿,蝶式键盘真的一无是处?

  • 索尼要爆发了!明年有望推出新款PS5:独占大作护航

  • 每日讯息!魅友等到了!魅族多终端产品曝光:手机、平板、AR眼镜齐了

  • 场景定义设计,艾瑞泽8树立“中国式豪华轿车”智能标杆

  • 世界观察:小米平板6/6 Pro现身!处理器、屏幕大升级:骁龙8+和OLED都来了

  • 【环球时快讯】iPhone要支持第三方应用商店了?我看未必是好事

  • 【全球速看料】苹果车祸检测又现多起误报 直升机飞出去了结果人没事

  • 【天天新要闻】史上口碑最好的小米旗舰!小米13京东好评率接近100%

三好网