丝袜人妻一区二区三区_少妇福利无码视频_亚洲理论片在线观看_一级毛片国产A级片

當(dāng)前位置:首頁 > 話題廣場 > 攻略專題 > 游戲問答

如何處理偶發(fā)的Bug?終于找到答案了20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒處理正確

今天,我來和你聊聊業(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

上一篇

11平臺等級如何到3看這里!象棋游戲平臺等級分設(shè)定

下一篇

10萬大寫怎么寫?總結(jié)很全面速看!10個一是10,10個十是100,老師判錯,家長質(zhì)疑?

如何處理偶發(fā)的Bug?我來告訴你答案20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒處理正確

如何處理偶發(fā)的Bug?我來告訴你答案20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒處理正確

如何處理偶發(fā)的Bug相關(guān)介紹,今天,我來說說商業(yè)代碼中與數(shù)據(jù)庫事務(wù)相關(guān)的坑。 Spring為事務(wù)API(如Java事務(wù)處理API(JTA)、JDBC、Hibernate和Java持久性API(JPA))實(shí)施了一致的編程模型,Spr...

關(guān)于如何處理偶發(fā)的Bug,你需要知道這些解Bug之路-記一次線上請求偶爾變慢的排查

關(guān)于如何處理偶發(fā)的Bug,你需要知道這些解Bug之路-記一次線上請求偶爾變慢的排查

如何處理偶發(fā)的Bug相關(guān)介紹,解決錯誤的方法-記住一次在線請求,解決偶爾減慢的問題 前言 最近解決了比較棘手的問題,調(diào)查過程很有趣,所以我以這篇文章報(bào)道為素材寫的。 Bug現(xiàn)場 這是一個偶發(fā)的性能問題。在每天幾百萬筆交易請求中,平...

如何處理偶發(fā)的Bug?我來告訴你答案解Bug之路-記一次線上請求偶爾變慢的排查

如何處理偶發(fā)的Bug?我來告訴你答案解Bug之路-記一次線上請求偶爾變慢的排查

如何處理偶發(fā)的Bug相關(guān)介紹,解決錯誤的方法-記住一次在線請求,解決偶爾減慢的問題 前言 最近解決了比較棘手的問題。調(diào)查過程很有趣,所以把這篇文章資料寫成了素材。 Bug現(xiàn)場 這是一個偶發(fā)的性能問題。在每天幾百萬比交易請求中,平均...

如何處理偶發(fā)的Bug?總結(jié)很全面速看!解Bug之路-記一次存儲故障的排查過程

如何處理偶發(fā)的Bug?總結(jié)很全面速看!解Bug之路-記一次存儲故障的排查過程

如何處理偶發(fā)的Bug相關(guān)介紹,高可用性真的不能忽略細(xì)節(jié)。平時(shí)運(yùn)行良好的系統(tǒng),如果該硬件出現(xiàn)故障,可能會引發(fā)潛在的錯誤。 偏偏這些故障在應(yīng)用層的表現(xiàn)稀奇古怪,很難讓人聯(lián)想到是硬件出了問題,特別是偶發(fā)性出現(xiàn)的問題更難排查。今天,筆者就...

如何處理偶發(fā)的Bug看這里!阿里研究員:線下環(huán)境為何不穩(wěn)定?怎么破

如何處理偶發(fā)的Bug看這里!阿里研究員:線下環(huán)境為何不穩(wěn)定?怎么破

如何處理偶發(fā)的Bug相關(guān)介紹,簡介:為什么離線環(huán)境的不穩(wěn)定性是必然的?我們該怎么辦?如何使它盡可能穩(wěn)定? 這篇文章想說兩件事。 為什么離線環(huán)境[1]的不穩(wěn)定性不可避免?我們該怎么辦?如何使它盡可能穩(wěn)定?此外,還討論了如何理解離線環(huán)...

如何處理偶發(fā)的Bug看這里!解Bug之路-記一次存儲故障的排查過程

如何處理偶發(fā)的Bug看這里!解Bug之路-記一次存儲故障的排查過程

如何處理偶發(fā)的Bug相關(guān)介紹,高可用性真的不能忽略細(xì)節(jié)。平時(shí)運(yùn)行良好的系統(tǒng),如果該硬件出現(xiàn)故障,可能會引發(fā)潛在的錯誤。 偏偏這些故障在應(yīng)用層的表現(xiàn)稀奇古怪,很難讓人聯(lián)想到是硬件出了問題,特別是偶發(fā)性出現(xiàn)的問題更難排查。今天,筆者就...

關(guān)于如何處理偶發(fā)的Bug,你需要知道這些Win10又出新bug了!更新補(bǔ)丁解決了舊毛病 迎來了新問題

關(guān)于如何處理偶發(fā)的Bug,你需要知道這些Win10又出新bug了!更新補(bǔ)丁解決了舊毛病 迎來了新問題

如何處理偶發(fā)的Bug相關(guān)介紹,一些Windows 10用戶反饋說,微軟在2月最后一周發(fā)布的選擇性更新有問題。 解決了舊Bug,添加了新Bug,工程師這個月又有的忙了。 偶發(fā)問題還是日常BUG呢?微軟每次公布新的更新出現(xiàn)新問題估計(jì)大...

如何處理偶發(fā)的Bug?總結(jié)很全面速看!Win10又出新bug了!更新補(bǔ)丁解決了舊毛病 迎來了新問題

如何處理偶發(fā)的Bug?總結(jié)很全面速看!Win10又出新bug了!更新補(bǔ)丁解決了舊毛病 迎來了新問題

如何處理偶發(fā)的Bug相關(guān)介紹,一些Windows 10用戶反饋說,微軟在2月最后一周發(fā)布的選擇性更新有問題。 解決了舊Bug,添加了新Bug,工程師這個月又有的忙了。 偶發(fā)問題還是日常BUG呢?微軟每次公布新的更新出現(xiàn)新問題估計(jì)大...