如何正確處理 Spring 聲明式事務
1. 前言
Spring 針對 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事務 API,實現(xiàn)了一致的編程模型,我們大多數(shù)做業(yè)務開發(fā)的時候,通常就在業(yè)務方法上使用聲明式注解 @Transactional 來開啟事務,大多數(shù)我們就沒有去關注事務是否會生效,出錯后事務是否能正確回滾,所以這里是有“坑”的。 事務沒有正確處理,對于我們來說通常是不易發(fā)現(xiàn)的,當壓力越來越大數(shù)據(jù)越來越多的時候,極有可能帶來大量的數(shù)據(jù)不一致臟數(shù)據(jù)的問題,所以處理好事務極為重要。
2. Spring事務沒有生效
首先來看一下,Spring 在什么情況下事務是不生效的,在這里為了方便我直接就采用了Sping JPA 作為數(shù)據(jù)庫訪問,首先定義一個實體類
@Entity
@Data
@NoArgsConstructor
public?class?SysUser?{
????@Id
????@GeneratedValue(strategy?=?GenerationType.AUTO)
????private?Long?id;
????private?String?name;
????public?SysUser(String?name)?{
????????this.name?=?name;
????}
}
實現(xiàn)一個repository 接口,里面有一個根據(jù)名字查SysUser的方法
@Repository
public?interface?SysUserRepository?extends?JpaRepository<SysUser,?Long>?{
????List<SysUser>?findByName(String?name);
}
實現(xiàn)一個SysUserService,其中使用一個公有方法調(diào)用標記了 @Transactional 注解的私有方法
@Service
@Slf4j
public?class?SysUserService?{
????@Resource
????private?SysUserRepository?sysUserRepository;
????/**
?????*?公有方法調(diào)用標記了?@Transactional?注解的私有方法
?????*?@param?name
?????*?@return
?????*/
????public?int?createUserWrong(String?name)?{
????????this.createUserPrivate(new?SysUser(name));
????????return?this.sysUserRepository.findByName(name).size();
????}
????@Transactional
????private?void?createUserPrivate(SysUser?sysUser)?{
????????this.sysUserRepository.save(sysUser);
????????if?(sysUser.getName().contains("test"))?{
????????????throw?new?RuntimeException("invalid?name");
????????}
????}
}
實現(xiàn)一個Controller如下
@RestController
@RequestMapping("/proxyfailed")
@RequiredArgsConstructor
public?class?ProxyFailedController?{
????private?final?SysUserService?sysUserService;
????@GetMapping("/wrong1")
????public?int?wrong1(@RequestParam?String?name)?{
????????return?this.sysUserService.createUserWrong1(name);
????}
}
測試接口可以發(fā)現(xiàn),程序報異常了,但是數(shù)據(jù)庫已經(jīng)卻成功的插入了記錄,事務并未生效!!!


其實在上面已經(jīng)看出來了,idea會在當你使用@Transactional 標記 private 修飾的方法時報紅。 @Transactional生效的原則之一就是,只有定義在public方法上的 @Transactional 注解才能生效。這是因為Spring 默認使用動態(tài)代理實現(xiàn)AOP,對目標方法進行增強,private 修飾的方法是無法被代理到的。 那如果說,我把上面的 createUserPrivate 方法改為 public 修飾,那么事務是否會生效呢?

答案是否定的,事務是依然不會生效的。要使 @Transactional生效的原則之二就是,必須通過代理類從外部調(diào)用目標方法才能生效。在這里,使用this調(diào)用目標方法,this指向的并不是代理類,而是當前目標類實例。 在這里可以在目標Service類注入自己的Bean 實例,如下:
??@Resource
????private?SysUserService?sysUserService;
????public?int?createUserRight1(String?name)?{
????????this.sysUserService.createUserPublic(new?SysUser(name));
????????return?this.sysUserRepository.findByName(name).size();
????}

可以看到此時 this.sysUserService是通過cglib增強過的代理類實例,所以此時 @Transactional 注解是生效的。但是自己注入自己是一件很怪的事情,最好還是在controller中直接調(diào)用被 @Transactional標記的 public 方法,使事務生效,這里可以看到this.sysUserService同樣是被增強后的代理類

那接下來我們看看下面這種情況, @Transactional有沒有生效,也就是標記了 @Transactional的public方法調(diào)用private修飾的方法,且在private方法中進行了數(shù)據(jù)庫操作
?@GetMapping("/right3")
????public?int?right3(@RequestParam?String?name)?{
????????return?this.sysUserService.createUserRight3(name);
????}
?@Transactional
????public?int?createUserRight3(String?name)?{
????????this.createUserPrivate1(new?SysUser(name));
????????return?this.sysUserRepository.findByName(name).size();
????}
????private?void?createUserPrivate1(SysUser?sysUser)?{
????????this.sysUserRepository.save(sysUser);
????????if?(sysUser.getName().contains("test"))?{
????????????throw?new?RuntimeException("invalid?name");
????????}
????}
答案是事務是生效的,因為在controller層調(diào)用createUserRight3方法,是通過代理對象調(diào)用的,在這時已經(jīng)開啟了事務,接下來在createUserRight3方法中的createUserPrivate1方法的調(diào)用只不過對應著線程中棧幀的壓棧,事務已經(jīng)在前面開啟了。對應之前事務不生效的幾種情況是它們的事務就根本沒開啟。
3. Spring事務沒有回滾
上面講了 Spring 聲明式事務未生效的幾種情況,下面來談一談事務沒有回滾的幾種情況,也就是說事務生效了,但是事務(對應于數(shù)據(jù)庫數(shù)據(jù))并沒有回滾的情況。
@RestController
@RequestMapping("/rollbackfailed")
@RequiredArgsConstructor
public?class?RollbackFailedController?{
????private?final?SysUserService?sysUserService;
????@GetMapping("/wrong1")
????public?void?wrong1(@RequestParam?String?name)?{
????????this.sysUserService.createUserWrong1(name);
????}
}
@Service
@Slf4j
public?class?SysUserService?{
????@Resource
????private?SysUserRepository?sysUserRepository;
????@Transactional
????public?void?createUserWrong1(String?name)?{
????????try?{
????????????sysUserRepository.save(new?SysUser(name));
????????????throw?new?RuntimeException("error");
????????}?catch?(Exception?e)?{
????????????log.error("create?user?failed",?e);
????????}
????}
}
我們可以看到在createUserWrong1方法中捕獲了異常,但是在 catch 塊處理中只是打印了錯誤日志,在這里事務并不會回滾。默認情況下,出現(xiàn) RuntimeException(非受檢異常)或 Error 的時候,Spring 才會回滾事務。 也就是說對于上面的情況,你需要在catch 塊中手動的去回滾事務,或者你干脆不捕獲異常
???@Transactional
????public?void?createUserWrong1(String?name)?{
????????try?{
????????????sysUserRepository.save(new?SysUser(name));
????????????throw?new?RuntimeException("error");
????????}?catch?(Exception?e)?{
????????????log.error("create?user?failed",?e);
????????????TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
????????}
????}
上面說了,默認情況下,出現(xiàn) RuntimeException(非受檢異常)或 Error 的時候,Spring 才會回滾事務。下面的情況是檢查時異常,這種情況下,是不會回滾事務的。
?@Transactional
????public?void?createUserWrong2(String?name)?throws?IOException?{
????????sysUserRepository.save(new?SysUser(name));
????????readFile();
????}
????/**
?????*?檢查時異常
?????*?@throws?IOException
?????*/
????private?void?readFile()?throws?IOException?{
????????Files.readAllLines(Paths.get("file-that-not-exist"));
????}
想要遇到所有的 Exception 都回滾事務,需要在 @Transactional注解中添加屬性
?@Transactional(rollbackFor?=?Exception.class)
????public?void?createUserWrong2(String?name)?throws?IOException?{
????????sysUserRepository.save(new?SysUser(name));
????????readFile();
????}
4. 總結
針對 Spring 聲明式事務生效,需要保證:
-
??@Transactional生效的原則之一就是,只有定義在public方法上的 @Transactional 注解才能生效。
-
??要使 @Transactional生效的原則之二就是,必須通過代理類從外部調(diào)用目標方法才能生效。
針對 Spring 聲明式事務回滾,需要注意:
-
??默認情況下,出現(xiàn) RuntimeException(非受檢異常)或 Error 的時候,Spring 才會回滾事務。
-
??要使檢查時異常也回滾,考慮設置@Transactional屬性
