
1、非運行時異常導致事務無法回滾
我們知道,Spring是通過AOP的方式來實現(xiàn)事務的,而在處理事務的過程中,Spring只有捕獲到RuntimeException或者Error的時候才會觸發(fā)回滾操作,如果我們在代碼中拋出的是非運行時異常,而又沒有特殊配置的話,事務就會無法回滾。
下面我們以一個簡單的例子,復現(xiàn)一下這種情況,以及針對這種情況的解決方案。
本文Springboot版本:2.7.6,數(shù)據(jù)源為MySQL。
首先創(chuàng)建一個測試用的User對象:
@Data
public 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表插入一條數(shù)據(jù),如果插入成功,就拋出exception異常,測試數(shù)據(jù)是否回滾。
@Service
@AllArgsConstructor
public class DemoService {
private final UserMapper userMapper;
@Transactional
public ResponseEntity<Object> addUser(User user) throws Exception {
int insert = this.userMapper.insert(user);
if (insert > 0) {
throw new Exception("異?;貪L測試");
}
return ResponseEntity.ok().build();
}
}
新建測試方法:
@Test
public void addUserTest() throws Exception {
User user = new User();
user.setName("測試");
user.setPwd("123456");
this.demoService.addUser(user);
}
運行測試方法,從控制臺可以看到,我們手動指定的異常被成功拋出。

但是,當異常發(fā)生時,事務并沒有被回滾,數(shù)據(jù)依然被插入到了數(shù)據(jù)庫。

解決辦法:
1,將異常包裝成運行時異常:throw new RuntimeException("異?;貪L測試");
2,在@Transactional指定回滾的異常類型,@Transactional(rollbackFor = Exception.class)。
一般來說,使用第二種方式會更清晰一些,但是有些朋友往往會忘記手動指定回滾的異常類型,進而導致非預期的bug產(chǎn)生。
2、通過this調用本類事務方法導致的事務無法回滾
隨著業(yè)務的發(fā)展,核心業(yè)務代碼會越來越多,同一個方法也會越寫越長。我們?yōu)榱耸勾a邏輯更加高內聚低耦合,會將功能相同的代碼進行封裝成一個個的子方法。
但是,如果我們對事務的運行機制了解不透徹,隨意在同一個類中通過this調用事務方法,就可能導致非預期的bug。
@Service
@RequiredArgsConstructor
public class DemoService {
private final UserMapper userMapper;
public ResponseEntity<Object> 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,如果數(shù)據(jù)插入成功,就拋出一個異常,測試數(shù)據(jù)是否能夠回滾。

通過測試用例可以看到,異常已經(jīng)拋出,但是數(shù)據(jù)庫中卻成功的插入了數(shù)據(jù),我們期望的數(shù)據(jù)并沒有回滾。

原因探究:
原因其實很簡單,通過this方法調用時,Spring的代理沒能起作用,事務自然也就無法介入,關于這一點的原理在之前的文章中也有分析過,感興趣的朋友可以去看一看。
有的朋友可能會說,項目的代碼已經(jīng)是這樣了,再將老方法重寫到新類中也不現(xiàn)實,有沒有辦法改動較小的方式呢?
其實很簡單,現(xiàn)在事務失效的原因是代理失效,那么想辦法讓代理重新生效就行了。
我們在本類中注入一個當前對象,這個對象可以被Spring代理,那么這個對象的方法自然也可以被代理。
@Service
@RequiredArgsConstructor
public class DemoService {
private final UserMapper userMapper;
@Resource
private DemoService self;
public ResponseEntity<Object> 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("測試添加異?;貪L");
}
}
}
3、被聲明的事務方法是private類型
這種錯誤在博主剛工作時遇到挺多次的,不過現(xiàn)在現(xiàn)代IDE已經(jīng)越來越智能了,對于這種情況會直接給出錯誤提示,所以這里提出這種錯誤只是告訴大家,事務方法是不能聲明為private的。
至于為什么不能是private,那自然還是和代理有關了。
4、嵌套事務異常導致事務被提前關閉而報錯
當使用嵌套事務時,需要明確指定事務的傳播范圍。
@Service
@RequiredArgsConstructor
public class DemoService {
private final UserMapper userMapper;
@Resource
private DemoService self;
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<Object> addUser(User user) {
int insert = this.userMapper.insert(user);
if (insert > 0) {
try {
this.self.update(user);
} catch (Exception e) {
System.out.println("即使更新異常也不要影響添加數(shù)據(jù)");
}
}
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("測試更新數(shù)據(jù)回滾");
}
}
}
如以上代碼,我們添加完一條數(shù)據(jù)之后,嘗試將密碼更新為666666,并且希望即使更新異常,也不要影響添加操作。
然而運行測試用例,我們會得到這樣一條錯誤信息:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。
什么意思呢?就是當Spring處理事務時,發(fā)現(xiàn)事務已經(jīng)被回滾了。
這是因為我們并沒有指定事務的傳播行為,默認情況下,Spring的事務傳播是REQUIRED,即:如果本來有事務,則加入該事務,如果沒有事務,則創(chuàng)建新的事務。
我們添加數(shù)據(jù)時啟動了一個事務,更新數(shù)據(jù)時,Spring判斷當前已經(jīng)存在事務,所以就不再新建事務,而是加入當前事務。
但是當更新操作失敗時,需要對事務進行回滾,更新是沒問題的,正常回滾。
但是插入操作就不行了,當要提交插入操作的事務時,由于事務已經(jīng)被回滾了,無法再次操作,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("測試更新數(shù)據(jù)回滾");
}
}
再測試一下,發(fā)現(xiàn)插入操作的事務可以正常提交了。
總結
事務是我們日常開發(fā)工作中無法避免的一個功能,深刻理解事務的運行機制,正確使用事務的聲明式操作,才能讓我們寫出更健壯的代碼。