对于一个数据库事务而言,当在我们事务当中将异常捕获后,该事务不应该会被回滚。但事实上这并不是绝对的,这里以PostgreSQL事务为例进行说明
环境搭建
DB环境
这里采用的PostgreSQL数据库版本为13.2。首先我们建立一张表。该表重点在于其username字段存在唯一约束
1 | -- 定义一个序列 |
工程代码
这里仅展示部分相关工程代码,其中数据表所对应的entity类如下所示
1 | 4j |
然后借助于Mybatis Plus快速实现相应的dao层
1 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
问题复现
插入不违反唯一约束的数据
这里为了方便验证问题,我们搭建一个Controller类PGTest用于控制、操作数据库。首先提供一个test1请求,不难看出。其会向数据库中插入两个不同人的记录。由于不违反唯一约束,故显然可以成功
1 |
|
测试结果如下所示,符合预期
捕获普通的(非数据库层面的)异常
然后我们在进行另外一个测试,同样是插入一批数据。只不过我们会在事务中先人为的抛出一个异常,然后对其进行捕获。当然插入的数据同样不违反唯一约束
1 |
|
测试结果如下所示,符合预期。因为异常已经在事务当中被捕获,故未发生回滚
捕获数据库层面的异常
至此一切都是顺利的,故这次测试插入一批数据。但违反了唯一约束。我们看看会发生什么现象
1 |
|
从测试结果不难看出,本次测试的数据一条也没被插入。换言之,发生了回滚
看上去似乎有点不合理,因为即使由于存在重复数据而导致插入失败,我们实际上也对其异常进行了捕获啊。为什么会发生回滚,使得其他正常的数据也没有被插入。现在我们结合日志来进行分析
- 对于蓝色框部分而言,此时由于数据并未违反唯一约束,故插入到数据库,且未有任何异常
- 对于桃色框部分而言,由于重复插入了username值为Dog的记录,违反了唯一约束,进而抛出异常
- 对于绿色框部分而言,由于我们之前捕获了异常,故业务代码实际上并未被打断,会继续向DB插入新的数据。显然这条新的数据没有违反唯一约束,但实际上该条数据并未被DB插入。且又抛出了一个新的异常。从本次异常信息——current transaction is aborted, commands ignored until end of transaction block,我们不难看出。对于DB而言,由于违反唯一约束而引发异常使得事务被终止了!故对于事务中的剩余SQL语句,DB也不再会执行、选择忽略。最后将该事务之前已经执行的部分也全部回滚掉。这也就是此次我们真正需要关注的问题所在
至此,相信大家已经不难理解了。虽然我们在业务代码中可以通过捕获异常,来避免由于事务直接抛出异常而导致的回滚。但这并不能完全保证事务肯定会被提交。对于某些数据库层面的异常(比如这里的违反唯一约束)来说,PostgreSQL会认为此时该事务已经遭到了破坏。故其会直接中止当前事务,进而导致回滚
解决方案
拆分事务
至此我们已经知道该场景下事务发生回滚的原因,那最直接的解决方法就是对原事务进行拆分,缩小每个事务的范围。使得某条SQL发生异常、事务回滚时,不会对其他事务的SQL造成任何影响。在基于Spring Boot框架下使用声明式事务时,我们可以直接利用 REQUIRES_NEW 事务传播行为 来实现,其可以保证每次调用该方法时均会开启一个新的事务。需要注意的是,由于声明式事务是基于AOP实现的,故不能在类内部调用事务方法。其会导致 @Transactional 事务注解 无法生效。具体地,我们通过一个PGService类来单独放置
1 |
|
测试结果如下所示,数据被正确地添加到了DB当中
通过保存点进行回滚
在一个事务中,如果某条SQL插入的数据违反了唯一约束。导致事务遭到了破坏。我们还可以通过save point保存点实现回滚到执行这条异常SQL之前。以恢复事务到正常状态,避免发生全部回滚的情形。这里我们使用基于TransactionTemplate的编程式事务
1 |
|
测试结果如下,符合预期