对于一个数据库事务而言,当在我们事务当中将异常捕获后,该事务不应该会被回滚。但事实上这并不是绝对的,这里以PostgreSQL事务为例进行说明
环境搭建 DB环境 这里采用的PostgreSQL数据库版本为13.2。首先我们建立一张表。该表重点在于其username字段存在唯一约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CREATE SEQUENCE user_info_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1 ; create table user_info ( id int default nextval('user_info_seq' ::regclass) not null , username varchar (255 ) not null , sex varchar (255 ) not null , primary key (id) ); comment on column user_info.id is '主键ID' ; comment on column user_info.username is '姓名' ; comment on column user_info.sex is '性别' ; create unique index user_info_uindex on user_info (username);
工程代码 这里仅展示部分相关工程代码,其中数据表所对应的entity类如下所示
1 2 3 4 5 6 7 8 9 10 11 @Slf4j @NoArgsConstructor @AllArgsConstructor @Builder @Data @TableName("user_info") public class UserInfo { private Integer id; private String username; private String sex; }
然后借助于Mybatis Plus快速实现相应的dao层
1 2 3 4 import com.baomidou.mybatisplus.core.mapper.BaseMapper;public interface UserInfoMapper extends BaseMapper <UserInfo> {}
问题复现 插入不违反唯一约束的数据 这里为了方便验证问题,我们搭建一个Controller类PGTest用于控制、操作数据库。首先提供一个test1请求,不难看出。其会向数据库中插入两个不同人的记录。由于不违反唯一约束,故显然可以成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Controller @ResponseBody @RequestMapping("PGTest") @Slf4j public class PGTest { @Autowired private UserInfoMapper userInfoMapper; @Transactional @RequestMapping(value = "/test1") public String test1 () { UserInfo userInfo1 = UserInfo.builder().username("Aaron" ).sex("男" ).build(); UserInfo userInfo2 = UserInfo.builder().username("Bob" ).sex("女" ).build(); List<UserInfo> list = Arrays.asList(userInfo1, userInfo2); for (UserInfo userInfo : list) { try { userInfoMapper.insert( userInfo ); } catch (Exception e) { log.error("Happen Exception: {}" , e.getMessage()); } } return "Test 1 Over" ; } @RequestMapping(value = "/getAllData") public String getAllData () { List<UserInfo> list = userInfoMapper.selectList(null ); String res = list.stream() .sorted( Comparator.comparing(UserInfo::getId) ) .map( e -> e.getId()+" " +e.getUsername()+" " +e.getSex() ) .collect( Collectors.joining("\n" ) ); return res; } }
测试结果如下所示,符合预期
捕获普通的(非数据库层面的)异常 然后我们在进行另外一个测试,同样是插入一批数据。只不过我们会在事务中先人为的抛出一个异常,然后对其进行捕获。当然插入的数据同样不违反唯一约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Controller @ResponseBody @RequestMapping("PGTest") @Slf4j public class PGTest { @Autowired private UserInfoMapper userInfoMapper; @Transactional @RequestMapping(value = "/test2") public String test2 () { UserInfo userInfo1 = UserInfo.builder().username("Font" ).sex("男" ).build(); UserInfo userInfo2 = UserInfo.builder().username("Gil" ).sex("女" ).build(); UserInfo userInfo3 = UserInfo.builder().username("Helly" ).sex("男" ).build(); List<UserInfo> list = Arrays.asList(userInfo1, userInfo2, userInfo3); for (UserInfo userInfo : list) { try { userInfoMapper.insert( userInfo ); if ( userInfo.getUsername().equals("Gil" ) ) { int a = 1 /0 ; } } catch (Exception e) { log.error("Happen Exception: {}" , e.getMessage()); } } return "Test 2 Over" ; } }
测试结果如下所示,符合预期。因为异常已经在事务当中被捕获,故未发生回滚
捕获数据库层面的异常 至此一切都是顺利的,故这次测试插入一批数据。但违反了唯一约束。我们看看会发生什么现象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Controller @ResponseBody @RequestMapping("PGTest") @Slf4j public class PGTest { @Autowired private UserInfoMapper userInfoMapper; @Transactional @RequestMapping(value = "/test3") public String test3 () { UserInfo userInfo1 = UserInfo.builder().username("Cat" ).sex("女" ).build(); UserInfo userInfo2 = UserInfo.builder().username("Dog" ).sex("男" ).build(); UserInfo userInfo3 = UserInfo.builder().username("Dog" ).sex("男" ).build(); UserInfo userInfo4 = UserInfo.builder().username("Eat" ).sex("男" ).build(); List<UserInfo> list = Arrays.asList(userInfo1, userInfo2, userInfo3, userInfo4); for (UserInfo userInfo : list) { log.info("Handle Data Start: {}" , userInfo); try { userInfoMapper.insert( userInfo ); } catch (Exception e) { log.error("Happen Exception: {}" , e.getMessage()); } log.info("Handle Data End: {}" , userInfo); } return "Test 3 Over" ; } }
从测试结果不难看出,本次测试的数据一条也没被插入。换言之,发生了回滚
看上去似乎有点不合理,因为即使由于存在重复数据而导致插入失败,我们实际上也对其异常进行了捕获啊。为什么会发生回滚,使得其他正常的数据也没有被插入。现在我们结合日志来进行分析
对于蓝色框部分而言,此时由于数据并未违反唯一约束,故插入到数据库,且未有任何异常
对于桃色框部分而言,由于重复插入了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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Controller @ResponseBody @RequestMapping("PGTest") @Slf4j public class PGTest { @Autowired private PGService pgService; @Transactional @RequestMapping(value = "/fix1") public String fix1 () { UserInfo userInfo1 = UserInfo.builder().username("Cat" ).sex("女" ).build(); UserInfo userInfo2 = UserInfo.builder().username("Dog" ).sex("男" ).build(); UserInfo userInfo3 = UserInfo.builder().username("Dog" ).sex("男" ).build(); UserInfo userInfo4 = UserInfo.builder().username("Eat" ).sex("男" ).build(); List<UserInfo> list = Arrays.asList(userInfo1, userInfo2, userInfo3, userInfo4); for (UserInfo userInfo : list) { pgService.addData(userInfo); } return "Fix 1 Over" ; } } ... @Service @Slf4j public class PGService { @Autowired private UserInfoMapper userInfoMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) public void addData (UserInfo userInfo) { log.info("Handle Data Start: {}" , userInfo); try { userInfoMapper.insert( userInfo ); } catch (Exception e) { log.error("Happen Exception: {}" , e.getMessage()); } log.info("Handle Data End: {}" , userInfo); } }
测试结果如下所示,数据被正确地添加到了DB当中
通过保存点进行回滚 在一个事务中,如果某条SQL插入的数据违反了唯一约束。导致事务遭到了破坏。我们还可以通过save point保存点实现回滚到执行这条异常SQL之前。以恢复事务到正常状态,避免发生全部回滚的情形。这里我们使用基于TransactionTemplate的编程式事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Controller @ResponseBody @RequestMapping("PGTest") @Slf4j public class PGTest { @Autowired private UserInfoMapper userInfoMapper; @Autowired private TransactionTemplate transactionTemplate; @RequestMapping(value = "/fix2") public String fix2 () { UserInfo userInfo1 = UserInfo.builder().username("Cat" ).sex("女" ).build(); UserInfo userInfo2 = UserInfo.builder().username("Dog" ).sex("男" ).build(); UserInfo userInfo3 = UserInfo.builder().username("Dog" ).sex("男" ).build(); UserInfo userInfo4 = UserInfo.builder().username("Eat" ).sex("男" ).build(); List<UserInfo> list = Arrays.asList(userInfo1, userInfo2, userInfo3, userInfo4); transactionTemplate.executeWithoutResult( transactionStatus -> { Object savepoint = null ; for (UserInfo userInfo : list) { log.info("Handle Data Start: {}" , userInfo); try { userInfoMapper.insert( userInfo ); savepoint = transactionStatus.createSavepoint(); } catch (Exception e) { transactionStatus.rollbackToSavepoint( savepoint ); log.error("Happen Exception: {}" , e.getMessage()); } log.info("Handle Data End: {}" , userInfo); } }); return "Fix 2 Over" ; } }
测试结果如下,符合预期