線上問題事跡(一)數(shù)據(jù)庫事務(wù)居然都沒生效?
Spring聲明式事務(wù)提供給 Javaer 們方便的事務(wù)配置方式,再搭配Spring Boot自動(dòng)配置,基本只需在方法上添加@Transactional注解,即可瞬間開啟方法的事務(wù)性配置。
- 但僅為方法添加@Transactional注解

你就以為這就夠了嗎?
事務(wù)未被正確處理,一般不會導(dǎo)致停止服務(wù),更不易在測試階段復(fù)現(xiàn)。但隨系統(tǒng)業(yè)務(wù)越來越復(fù)雜,就會帶來大量數(shù)據(jù)不一致問題,隨后就是大量線上問題而后人工排查檢修數(shù)據(jù)。
1 你的Spring事務(wù)怎么才算生效?
使用@Transactional開啟聲明式事務(wù)時(shí), 靈魂發(fā)問:事務(wù)生效了嗎?
案例
用戶表實(shí)體類

DAO 層
根據(jù)username查詢所有數(shù)據(jù)
- @Repository
- public interface UserRepository extends JpaRepository<UserEntity, Long> {
- List<UserEntity> findByName(String name);
- }
Service層
UserService類
負(fù)責(zé)業(yè)務(wù)邏輯處理,包括如下方法:
createUserWrong1調(diào)用private方法:
createUserPrivate,被@Transactional注解。當(dāng)傳入的用戶名包含test則拋異常,讓用戶的創(chuàng)建操作失敗,期望事務(wù)回滾:

getUserCount

Controller層
調(diào)用一下剛才定義的UserService中的入口方法createUserWrong1。

測試結(jié)果
即便用戶名不合法,用戶也能創(chuàng)建成功。刷新瀏覽器,多次發(fā)現(xiàn)非法用戶注冊。
2 @Transactional怎么確保生效?
除非特殊配置(比如使用AspectJ靜態(tài)織入實(shí)現(xiàn)AOP),否則只有定義在public方法上的@Transactional才能生效。
Spring默認(rèn)通過動(dòng)態(tài)代理實(shí)現(xiàn)AOP,對目標(biāo)方法增強(qiáng),private方法無法代理到,自然也無法動(dòng)態(tài)增強(qiáng)事務(wù)處理邏輯。
那簡單,把createUserPrivate方法改為public即可。
在UserService中再建一個(gè)入口方法createUserWrong2,來調(diào)用這個(gè)public方法再次嘗試:
- public int createUserWrong2(String name) {
- try {
- this.createUserPublic(new UserEntity(name));
- } catch (Exception ex) {
- log.error("create user failed because {}", ex.getMessage());
- }
- return userRepository.findByName(name).size();
- }
- //標(biāo)記了@Transactional的public方法
- @Transactional
- public void createUserPublic(UserEntity entity) {
- userRepository.save(entity);
- if (entity.getName().contains("test"))
- throw new RuntimeException("invalid username!");
- }
新的createUserWrong2方法事務(wù)同樣不生效。
必須通過代理過的類從外部調(diào)用目標(biāo)方法
要調(diào)用增強(qiáng)過的方法必然是調(diào)用代理后的對象。
嘗試修改UserService,注入一個(gè)self,然后再通過self實(shí)例調(diào)用標(biāo)記有@Transactional注解的createUserPublic方法。設(shè)置斷點(diǎn)可以看到,self是由Spring通過CGLIB方式增強(qiáng)過的類。
CGLIB通過繼承方式實(shí)現(xiàn)代理類,private方法在子類不可見,自然也就無法進(jìn)行事務(wù)增強(qiáng);
this指針代表對象自己,Spring不可能注入this,所以通過this訪問方法必然不是代理。
把this改為self,在Controller中調(diào)用createUserRight方法可以驗(yàn)證事務(wù)生效了:非法的用戶注冊操作可以回滾。
雖然在UserService內(nèi)部注入自己調(diào)用自己的createUserPublic可以正確實(shí)現(xiàn)事務(wù),但這不符合習(xí)慣用法。更合理的實(shí)現(xiàn)方式是,讓Controller直接調(diào)用之前定義的UserService的createUserPublic方法。
- @GetMapping("right2")
- public int right2(@RequestParam("name") String name) {
- try {
- userService.createUserPublic(new UserEntity(name));
- } catch (Exception ex) {
- log.error("create user failed because {}", ex.getMessage());
- }
- return userService.getUserCount(name);
- }
this自調(diào)用/self調(diào)用/Controller調(diào)用UserService

- this自調(diào)用
無法走到Spring代理類
- 后兩種
調(diào)用的Spring注入的UserService,通過代理調(diào)用才有機(jī)會對createUserPublic方法進(jìn)行動(dòng)態(tài)增強(qiáng)。
推薦在開發(fā)時(shí)打開相關(guān)Debug日志,以了解Spring事務(wù)實(shí)現(xiàn)的細(xì)節(jié)。
比如JPA數(shù)據(jù)庫訪問,可以這么開啟Debug日志:
logging.level.org.springframework.orm.jpa=DEBUG
開啟日志后再比較下在UserService中this調(diào)用、Controller中通過注入的UserService Bean調(diào)用createUserPublic的區(qū)別。
很明顯,this調(diào)用因沒走代理,事務(wù)沒有在createUserPublic生效,只在Repository的save生效:
- // 在UserService中通過this調(diào)用public的createUserPublic
- [23:04:30.748] [http-nio-45678-exec-5] [DEBUG] [o.s.orm.jpa.JpaTransactionManager:370 ] -
- Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]:
- PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- //在Controller中通過注入的UserService Bean調(diào)用createUserPublic
- [10:10:47.750] [http-nio-45678-exec-6] [DEBUG] [o.s.orm.jpa.JpaTransactionManager :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo1.UserService.createUserPublic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
這種實(shí)現(xiàn)在Controller里處理異常顯得繁瑣,還不如直接把createUserWrong2加@Transactional注解,然后在Controller中直接調(diào)用該方法。
這既能從外部(Controller中)調(diào)用UserService方法,方法又是public的能夠被動(dòng)態(tài)代理AOP增強(qiáng)。要不你試試?看看效果如何,下回分解~