今天,我來和你聊聊業(yè)務(wù)代碼中與數(shù)據(jù)庫事務(wù)相關(guān)的坑。
Spring針對Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API (JPA)等事務(wù)API,實(shí)現(xiàn)了一致的編程模型,而Spring的聲明式事務(wù)功能更是提供了極其方便的事務(wù)配置方式,配合Spring Boot的自動配置,大多數(shù)Spring Boot項(xiàng)目只需要在方法上標(biāo)記@Transactional注解,即可一鍵開啟方法的事務(wù)性配置。
據(jù)我觀察,大多數(shù)業(yè)務(wù)開發(fā)同學(xué)都有事務(wù)的概念,也知道如果整體考慮多個數(shù)據(jù)庫操作要么成功要么失敗時(shí),需要通過數(shù)據(jù)庫事務(wù)來實(shí)現(xiàn)多個操作的一致性和原子性。但,在使用上大多僅限于為方法標(biāo)記@Transactional,不會去關(guān)注事務(wù)是否有效、出錯后事務(wù)是否正確回滾,也不會考慮復(fù)雜的業(yè)務(wù)代碼中涉及多個子業(yè)務(wù)邏輯時(shí),怎么正確處理事務(wù)。
事務(wù)沒有被正確處理,一般來說不會過于影響正常流程,也不容易在測試階段被發(fā)現(xiàn)。但當(dāng)系統(tǒng)越來越復(fù)雜、壓力越來越大之后,就會帶來大量的數(shù)據(jù)不一致問題,隨后就是大量的人工介入查看和修復(fù)數(shù)據(jù)。
所以說,一個成熟的業(yè)務(wù)系統(tǒng)和一個基本可用能完成功能的業(yè)務(wù)系統(tǒng),在事務(wù)處理細(xì)節(jié)上的差異非常大。要確保事務(wù)的配置符合業(yè)務(wù)功能的需求,往往不僅僅是技術(shù)問題,還涉及產(chǎn)品流程和架構(gòu)設(shè)計(jì)的問題。今天這一講的標(biāo)題“20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒處理正確”中,20%這個數(shù)字在我看來還是比較保守的。
我今天要分享的內(nèi)容,就是幫助你在技術(shù)問題上理清思路,避免因?yàn)槭聞?wù)處理不當(dāng)讓業(yè)務(wù)邏輯的實(shí)現(xiàn)產(chǎn)生大量偶發(fā)Bug。
小心Spring的事務(wù)可能沒有生效
在使用@Transactional注解開啟聲明式事務(wù)時(shí), 第一個最容易忽略的問題是,很可能事務(wù)并沒有生效。
實(shí)現(xiàn)下面的Demo需要一些基礎(chǔ)類,首先定義一個具有ID和姓名屬性的UserEntity,也就是一個包含兩個字段的用戶表:
@Entity
@Data
public class UserEntity {
@Id
@GeneratedValue(strategy = AUTO)
Private Long id;
private String name;
public UserEntity() { }
public UserEntity(String name) {
= name;
}
}
為了方便理解,我使用Spring JPA做數(shù)據(jù)庫訪問,實(shí)現(xiàn)這樣一個Repository,新增一個根據(jù)用戶名查詢所有數(shù)據(jù)的方法:
@Repository
public interface UserRepository extends JPARepository<UserEntity, Long> {
List<UserEntity> findByName(String name);
}
定義一個UserService類,負(fù)責(zé)業(yè)務(wù)邏輯處理。如果不清楚@Transactional的實(shí)現(xiàn)方式,只考慮代碼邏輯的話,這段代碼看起來沒有問題。
定義一個入口方法createUserWrong1來調(diào)用另一個私有方法createUserPrivate,私有方法上標(biāo)記了@Transactional注解。當(dāng)傳入的用戶名包含test關(guān)鍵字時(shí)判斷為用戶名不合法,拋出異常,讓用戶創(chuàng)建操作失敗,期望事務(wù)可以回滾:
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
//一個公共方法供Controller調(diào)用,內(nèi)部調(diào)用事務(wù)性的私有方法
public int createUserWrong1(String name) {
try {
(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return u(name).size();
}
//標(biāo)記了@Transactional的private方法
@transactional
private void createUserPrivate(UserEntity entity) {
u(entity);
if ().contains("test"))
throw new RuntimeException("invalid username!");
}
//根據(jù)用戶名查詢用戶數(shù)
public int getUserCount(String name) {
return u(name).size();
}
}
下面是Controller的實(shí)現(xiàn),只是調(diào)用一下剛才定義的UserService中的入口方法createUserWrong1。
@Autowired
private UserService userService;
@GetMapping("wrong1")
public int wrong1(@RequestParam("name") String name) {
return u(name);
}
調(diào)用接口后發(fā)現(xiàn),即便用戶名不合法,用戶也能創(chuàng)建成功。刷新瀏覽器,多次發(fā)現(xiàn)有十幾個的非法用戶注冊。
這里給出@Transactional生效原則1,除非特殊配置(比如使用AspectJ靜態(tài)織入實(shí)現(xiàn)AOP),否則只有定義在public方法上的@Transactional才能生效。原因是,Spring默認(rèn)通過動態(tài)代理的方式實(shí)現(xiàn)AOP,對目標(biāo)方法進(jìn)行增強(qiáng),private方法無法代理到,Spring自然也無法動態(tài)增強(qiáng)事務(wù)處理邏輯。
你可能會說,修復(fù)方式很簡單,把標(biāo)記了事務(wù)注解的createUserPrivate方法改為public即可。在UserService中再建一個入口方法createUserWrong2,來調(diào)用這個public方法再次嘗試:
public int createUserWrong2(String name) {
try {
(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return u(name).size();
}
//標(biāo)記了@Transactional的public方法
@Transactional
public void createUserPublic(UserEntity entity) {
u(entity);
if ().contains("test"))
throw new RuntimeException("invalid username!");
}
測試發(fā)現(xiàn),調(diào)用新的createUserWrong2方法事務(wù)同樣不生效。這里,我給出@Transactional生效原則2,必須通過代理過的類從外部調(diào)用目標(biāo)方法才能生效。
Spring通過AOP技術(shù)對方法進(jìn)行增強(qiáng),要調(diào)用增強(qiáng)過的方法必然是調(diào)用代理后的對象。我們嘗試修改下UserService的代碼,注入一個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后測試發(fā)現(xiàn),在Controller中調(diào)用createUserRight方法可以驗(yàn)證事務(wù)是生效的,非法的用戶注冊操作可以回滾。
雖然在UserService內(nèi)部注入自己調(diào)用自己的createUserPublic可以正確實(shí)現(xiàn)事務(wù),但更合理的實(shí)現(xiàn)方式是,讓Controller直接調(diào)用之前定義的UserService的createUserPublic方法,因?yàn)樽⑷胱约赫{(diào)用自己很奇怪,也不符合分層實(shí)現(xiàn)的規(guī)范:
@GetMapping("right2")
public int right2(@RequestParam("name") String name) {
try {
u(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return u(name);
}
我們再通過一張圖來回顧下this自調(diào)用、通過self調(diào)用,以及在Controller中調(diào)用UserService三種實(shí)現(xiàn)的區(qū)別:
通過this自調(diào)用,沒有機(jī)會走到Spring的代理類;后兩種改進(jìn)方案調(diào)用的是Spring注入的UserService,通過代理調(diào)用才有機(jī)會對createUserPublic方法進(jìn)行動態(tài)增強(qiáng)。
這里,我還有一個小技巧,強(qiáng)烈建議你在開發(fā)時(shí)打開相關(guān)的Debug日志,以方便了解Spring事務(wù)實(shí)現(xiàn)的細(xì)節(jié),并及時(shí)判斷事務(wù)的執(zhí)行情況。
我們的Demo代碼使用JPA進(jìn)行數(shù)據(jù)庫訪問,可以這么開啟Debug日志:
logging.level.org.
開啟日志后,我們再比較下在UserService中通過this調(diào)用和在Controller中通過注入的UserService Bean調(diào)用createUserPublic區(qū)別。很明顯,this調(diào)用因?yàn)闆]有走代理,事務(wù)沒有在createUserPublic方法上生效,只在Repository的save方法層面生效:
//在UserService中通過this調(diào)用public的createUserPublic
[10:10:19.913] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :370 ] - Creating new transaction with name [org.]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
//在Controller中通過注入的UserService Bean調(diào)用createUserPublic
[10:10:47.750] [http-nio-45678-exec-6] [DEBUG] [o.s.orm.j :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo1.U]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
你可能還會考慮一個問題,這種實(shí)現(xiàn)在Controller里處理了異常顯得有點(diǎn)繁瑣,還不如直接把createUserWrong2方法加上@Transactional注解,然后在Controller中直接調(diào)用這個方法。這樣一來,既能從外部(Controller中)調(diào)用UserService中的方法,方法又是public的能夠被動態(tài)代理AOP增強(qiáng)。
你可以試一下這種方法,但很容易就會踩第二個坑,即因?yàn)闆]有正確處理異常,導(dǎo)致事務(wù)即便生效也不一定能回滾。
事務(wù)即便生效也不一定能回滾
通過AOP實(shí)現(xiàn)事務(wù)處理可以理解為,使用try…catch…來包裹標(biāo)記了@Transactional注解的方法,當(dāng)方法出現(xiàn)了異常并且滿足一定條件的時(shí)候,在catch里面我們可以設(shè)置事務(wù)回滾,沒有異常則直接提交事務(wù)。
這里的“一定條件”,主要包括兩點(diǎn)。
第一,只有異常傳播出了標(biāo)記了@Transactional注解的方法,事務(wù)才能回滾。在Spring的TransactionAspectSupport里有個 invokeWithinTransaction方法,里面就是處理事務(wù)的邏輯??梢钥吹?,只有捕獲到異常才能進(jìn)行后續(xù)事務(wù)處理:
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invoca();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
第二,默認(rèn)情況下,出現(xiàn)RuntimeException(非受檢異常)或Error的時(shí)候,Spring才會回滾事務(wù)。
打開Spring的DefaultTransactionAttribute類能看到如下代碼塊,可以發(fā)現(xiàn)相關(guān)證據(jù),通過注釋也能看到Spring這么做的原因,大概的意思是受檢異常一般是業(yè)務(wù)異常,或者說是類似另一種方法的返回值,出現(xiàn)這樣的異??赡軜I(yè)務(wù)還能完成,所以不會主動回滾;而Error或RuntimeException代表了非預(yù)期的結(jié)果,應(yīng)該回滾:
/**
* The default behavior is as with EJB: rollback on unchecked exception
* ({@link RuntimeException}), assuming an unexpected outcome outside of any
* business rules. Additionally, we also attempt to rollback on {@link Error} which
* is clearly an unexpected outcome as well. By contrast, a checked exception is
* considered a business exception and therefore a regular expected outcome of the
* transactional business method, i.e. a kind of alternative return value which
* still allows for regular completion of resource operations.
* <p>This is largely consistent with TransactionTemplate's default behavior,
* except that TransactionTemplate also rolls back on undeclared checked exceptions
* (a corner case). For declarative transactions, we expect checked exceptions to be
* intentionally declared as business exceptions, leading to a commit by default.
* @see org.
*/
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
接下來,我和你分享2個反例。
重新實(shí)現(xiàn)一下UserService中的注冊用戶操作:
- 在createUserWrong1方法中會拋出一個RuntimeException,但由于方法內(nèi)catch了所有異常,異常無法從方法傳播出去,事務(wù)自然無法回滾。
- 在createUserWrong2方法中,注冊用戶的同時(shí)會有一次otherTask文件讀取操作,如果文件讀取失敗,我們希望用戶注冊的數(shù)據(jù)庫操作回滾。雖然這里沒有捕獲異常,但因?yàn)閛therTask方法拋出的是受檢異常,createUserWrong2傳播出去的也是受檢異常,事務(wù)同樣不會回滾。
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
//異常無法傳播出方法,導(dǎo)致事務(wù)無法回滾
@Transactional
public void createUserWrong1(String name) {
try {
u(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
}
}
//即使出了受檢異常也無法讓事務(wù)回滾
@Transactional
public void createUserWrong2(String name) throws IOException {
u(new UserEntity(name));
otherTask();
}
//因?yàn)槲募淮嬖?,一定會拋出一個IOException
private void otherTask() throws IOException {
("file-that-not-exist"));
}
}
Controller中的實(shí)現(xiàn),僅僅是調(diào)用UserService的createUserWrong1和createUserWrong2方法,這里就貼出實(shí)現(xiàn)了。這2個方法的實(shí)現(xiàn)和調(diào)用,雖然完全避開了事務(wù)不生效的坑,但因?yàn)楫惓L幚聿划?dāng),導(dǎo)致程序沒有如我們期望的文件操作出現(xiàn)異常時(shí)回滾事務(wù)。
現(xiàn)在,我們來看下修復(fù)方式,以及如何通過日志來驗(yàn)證是否修復(fù)成功。針對這2種情況,對應(yīng)的修復(fù)方法如下。
第一,如果你希望自己捕獲異常進(jìn)行處理的話,也沒關(guān)系,可以手動設(shè)置讓當(dāng)前事務(wù)處于回滾狀態(tài):
@Transactional
public void createUserRight1(String name) {
try {
u(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
Tran().setRollbackOnly();
}
}
運(yùn)行后可以在日志中看到Rolling back字樣,確認(rèn)事務(wù)回滾了。同時(shí),我們還注意到“Transactional code has requested rollback”的提示,表明手動請求回滾:
[22:14:49.352] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.j :698 ] - Transactional code has requested rollback
[22:14:49.353] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.j :834 ] - Initiating transaction rollback
[22:14:49.353] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.j :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1906719643<open>)]
第二,在注解中聲明,期望遇到所有的Exception都回滾事務(wù)(來突破默認(rèn)不回滾受檢異常的限制):
@Transactional(rollbackFor = Exce)
public void createUserRight2(String name) throws IOException {
u(new UserEntity(name));
otherTask();
}
運(yùn)行后,同樣可以在日志中看到回滾的提示:
[22:10:47.980] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.j :834 ] - Initiating transaction rollback
[22:10:47.981] [http-nio-45678-exec-4] [DEBUG] [o.s.orm.j :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(1419329213<open>)]
在這個例子中,我們展現(xiàn)的是一個復(fù)雜的業(yè)務(wù)邏輯,其中有數(shù)據(jù)庫操作、IO操作,在IO操作出現(xiàn)問題時(shí)希望讓數(shù)據(jù)庫事務(wù)也回滾,以確保邏輯的一致性。在有些業(yè)務(wù)邏輯中,可能會包含多次數(shù)據(jù)庫操作,我們不一定希望將兩次操作作為一個事務(wù)來處理,這時(shí)候就需要仔細(xì)考慮事務(wù)傳播的配置了,否則也可能踩坑。
請確認(rèn)事務(wù)傳播配置是否符合自己的業(yè)務(wù)邏輯
有這么一個場景:一個用戶注冊的操作,會插入一個主用戶到用戶表,還會注冊一個關(guān)聯(lián)的子用戶。我們希望將子用戶注冊的數(shù)據(jù)庫操作作為一個獨(dú)立事務(wù)來處理,即使失敗也不會影響主流程,即不影響主用戶的注冊。
接下來,我們模擬一個實(shí)現(xiàn)類似業(yè)務(wù)邏輯的UserService:
@Autowired
private UserRepository userRepository;
@Autowired
private SubUserService subUserService;
@Transactional
public void createUserWrong(UserEntity entity) {
createMainUser(entity);
(entity);
}
private void createMainUser(UserEntity entity) {
u(entity);
log.info("createMainUser finish");
}
SubUserService的createSubUserWithExceptionWrong實(shí)現(xiàn)正如其名,因?yàn)樽詈笪覀儝伋隽艘粋€運(yùn)行時(shí)異常,錯誤原因是用戶狀態(tài)無效,所以子用戶的注冊肯定是失敗的。我們期望子用戶的注冊作為一個事務(wù)單獨(dú)回滾,不影響主用戶的注冊,這樣的邏輯可以實(shí)現(xiàn)嗎?
@Service
@Slf4j
public class SubUserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createSubUserWithExceptionWrong(UserEntity entity) {
log.info("createSubUserWithExceptionWrong start");
u(entity);
throw new RuntimeException("invalid status");
}
}
我們在Controller里實(shí)現(xiàn)一段測試代碼,調(diào)用UserService:
@GetMapping("wrong")
public int wrong(@RequestParam("name") String name) {
try {
u(new UserEntity(name));
} catch (Exception ex) {
log.error("createUserWrong failed, reason:{}", ex.getMessage());
}
return u(name);
}
調(diào)用后可以在日志中發(fā)現(xiàn)如下信息,很明顯事務(wù)回滾了,最后Controller打出了創(chuàng)建子用戶拋出的運(yùn)行時(shí)異常:
[22:50:42.866] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.j :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(103972212<open>)]
[22:50:42.869] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.j :620 ] - Closing JPA EntityManager [SessionImpl(103972212<open>)] after transaction
[22:50:42.869] [http-nio-45678-exec-8] [ERROR] [t.d.TransactionPropagationController:23 ] - createUserWrong failed, reason:invalid status
你馬上就會意識到,不對呀,因?yàn)檫\(yùn)行時(shí)異常逃出了@Transactional注解標(biāo)記的createUserWrong方法,Spring當(dāng)然會回滾事務(wù)了。如果我們希望主方法不回滾,應(yīng)該把子方法拋出的異常捕獲了。
也就是這么改,把包裹上catch,這樣外層主方法就不會出現(xiàn)異常了:
@Transactional
public void createUserWrong2(UserEntity entity) {
createMainUser(entity);
try{
(entity);
} catch (Exception ex) {
// 雖然捕獲了異常,但是因?yàn)闆]有開啟新事務(wù),而當(dāng)前事務(wù)因?yàn)楫惓R呀?jīng)被標(biāo)記為rollback了,所以最終還是會回滾。
log.error("create sub user error:{}", ex.getMessage());
}
}
運(yùn)行程序后可以看到如下日志:
[22:57:21.722] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.U2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
[22:57:21.739] [http-nio-45678-exec-3] [INFO ] [t.c. ] - createSubUserWithExceptionWrong start
[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :356 ] - Found thread-bound EntityManager [SessionImpl(1794007607<open>)] for JPA transaction
[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :471 ] - Participating in existing transaction
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :843 ] - Participating transaction failed - marking existing transaction as rollback-only
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :580 ] - Setting JPA transaction on EntityManager [SessionImpl(1794007607<open>)] rollback-only
[22:57:21.740] [http-nio-45678-exec-3] [ERROR] [.g.t.c. ] - create sub user error:invalid status
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :741 ] - Initiating transaction commit
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :529 ] - Committing JPA transaction on EntityManager [SessionImpl(1794007607<open>)]
[22:57:21.743] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.j :620 ] - Closing JPA EntityManager [SessionImpl(1794007607<open>)] after transaction
[22:57:21.743] [http-nio-45678-exec-3] [ERROR] [t.d.TransactionPropagationController:33 ] - createUserWrong2 failed, reason:Transaction silently rolled back because it has been marked as rollback-only
org.: Transaction silently rolled back because it has been marked as rollback-only
...
需要注意以下幾點(diǎn):
- 如第1行所示,對createUserWrong2方法開啟了異常處理;
- 如第5行所示,子方法因?yàn)槌霈F(xiàn)了運(yùn)行時(shí)異常,標(biāo)記當(dāng)前事務(wù)為回滾;
- 如第7行所示,主方法的確捕獲了異常打印出了create sub user error字樣;
- 如第9行所示,主方法提交了事務(wù);
- 奇怪的是,如第11行和12行所示,Controller里出現(xiàn)了一個UnexpectedRollbackException,異常描述提示最終這個事務(wù)回滾了,而且是靜默回滾的。之所以說是靜默,是因?yàn)閏reateUserWrong2方法本身并沒有出異常,只不過提交后發(fā)現(xiàn)子方法已經(jīng)把當(dāng)前事務(wù)設(shè)置為了回滾,無法完成提交。
這挺反直覺的。我們之前說,出了異常事務(wù)不一定回滾,這里說的卻是不出異常,事務(wù)也不一定可以提交。原因是,主方法注冊主用戶的邏輯和子方法注冊子用戶的邏輯是同一個事務(wù),子邏輯標(biāo)記了事務(wù)需要回滾,主邏輯自然也不能提交了。
看到這里,修復(fù)方式就很明確了,想辦法讓子邏輯在獨(dú)立事務(wù)中運(yùn)行,也就是改一下SubUserService注冊子用戶的方法,為注解加上propagation = Pro來設(shè)置REQUIRES_NEW方式的事務(wù)傳播策略,也就是執(zhí)行到這個方法時(shí)需要開啟新的事務(wù),并掛起當(dāng)前事務(wù):
@Transactional(propagation = Pro)
public void createSubUserWithExceptionRight(UserEntity entity) {
log.info("createSubUserWithExceptionRight start");
u(entity);
throw new RuntimeException("invalid status");
}
主方法沒什么變化,同樣需要捕獲異常,防止異常漏出去導(dǎo)致主事務(wù)回滾,重新命名為createUserRight:
@Transactional
public void createUserRight(UserEntity entity) {
createMainUser(entity);
try{
(entity);
} catch (Exception ex) {
// 捕獲異常,防止主方法回滾
log.error("create sub user error:{}", ex.getMessage());
}
}
改造后,重新運(yùn)行程序可以看到如下的關(guān)鍵日志:
- 第1行日志提示我們針對createUserRight方法開啟了主方法的事務(wù);
- 第2行日志提示創(chuàng)建主用戶完成;
- 第3行日志可以看到主事務(wù)掛起了,開啟了一個新的事務(wù),針對createSubUserWithExceptionRight方案,也就是我們的創(chuàng)建子用戶的邏輯;
- 第4行日志提示子方法事務(wù)回滾;
- 第5行日志提示子方法事務(wù)完成,繼續(xù)主方法之前掛起的事務(wù);
- 第6行日志提示主方法捕獲到了子方法的異常;
- 第8行日志提示主方法的事務(wù)提交了,隨后我們在Controller里沒看到靜默回滾的異常。
[23:17:20.935] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :370 ] - Creating new transaction with name [org.geekbang.]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
[23:17:21.079] [http-nio-45678-exec-1] [INFO ] [.g.t.c. ] - createMainUser finish
[23:17:21.082] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :420 ] - Suspending current transaction, creating new transaction with name [org.geekbang.]
[23:17:21.153] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :834 ] - Initiating transaction rollback
[23:17:21.160] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :1009] - Resuming suspended transaction after completion of inner transaction
[23:17:21.161] [http-nio-45678-exec-1] [ERROR] [.g.t.c. ] - create sub user error:invalid status
[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :741 ] - Initiating transaction commit
[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.j :529 ] - Committing JPA transaction on EntityManager [SessionImpl(396441411<open>)]
運(yùn)行測試程序看到如下結(jié)果,getUserCount得到的用戶數(shù)量為1,代表只有一個用戶也就是主用戶注冊完成了,符合預(yù)期:
重點(diǎn)回顧
今天,我針對業(yè)務(wù)代碼中最常見的使用數(shù)據(jù)庫事務(wù)的方式,即Spring聲明式事務(wù),與你總結(jié)了使用上可能遇到的三類坑,包括:
第一,因?yàn)榕渲貌徽_,導(dǎo)致方法上的事務(wù)沒生效。我們務(wù)必確認(rèn)調(diào)用@Transactional注解標(biāo)記的方法是public的,并且是通過Spring注入的Bean進(jìn)行調(diào)用的。
第二,因?yàn)楫惓L幚聿徽_,導(dǎo)致事務(wù)雖然生效但出現(xiàn)異常時(shí)沒回滾。Spring默認(rèn)只會對標(biāo)記@Transactional注解的方法出現(xiàn)了RuntimeException和Error的時(shí)候回滾,如果我們的方法捕獲了異常,那么需要通過手動編碼處理事務(wù)回滾。如果希望Spring針對其他異常也可以回滾,那么可以相應(yīng)配置@Transactional注解的rollbackFor和noRollbackFor屬性來覆蓋其默認(rèn)設(shè)置。
第三,如果方法涉及多次數(shù)據(jù)庫操作,并希望將它們作為獨(dú)立的事務(wù)進(jìn)行提交或回滾,那么我們需要考慮進(jìn)一步細(xì)化配置事務(wù)傳播方式,也就是@Transactional注解的Propagation屬性。
可見,正確配置事務(wù)可以提高業(yè)務(wù)項(xiàng)目的健壯性。但,又因?yàn)榻研詥栴}往往體現(xiàn)在異常情況或一些細(xì)節(jié)處理上,很難在主流程的運(yùn)行和測試中發(fā)現(xiàn),導(dǎo)致業(yè)務(wù)代碼的事務(wù)處理邏輯往往容易被忽略,因此我在代碼審查環(huán)節(jié)一直很關(guān)注事務(wù)是否正確處理。
如果你無法確認(rèn)事務(wù)是否真正生效,是否按照預(yù)期的邏輯進(jìn)行,可以嘗試打開Spring的部分Debug日志,通過事務(wù)的運(yùn)作細(xì)節(jié)來驗(yàn)證。也建議你在單元測試時(shí)盡量覆蓋多的異常場景,這樣在重構(gòu)時(shí),也能及時(shí)發(fā)現(xiàn)因?yàn)榉椒ǖ恼{(diào)用方式、異常處理邏輯的調(diào)整,導(dǎo)致的事務(wù)失效問題。
1.《如何處理偶發(fā)的Bug?終于找到答案了20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒處理正確》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識,僅代表作者本人觀點(diǎn),與本網(wǎng)站無關(guān),侵刪請聯(lián)系頁腳下方聯(lián)系方式。
2.《如何處理偶發(fā)的Bug?終于找到答案了20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒處理正確》僅供讀者參考,本網(wǎng)站未對該內(nèi)容進(jìn)行證實(shí),對其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。
3.文章轉(zhuǎn)載時(shí)請保留本站內(nèi)容來源地址,http://f99ss.com/gl/2194023.html