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

如何處理偶發(fā)的Bug?總結(jié)很全面速看!20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒(méi)處理正確

今天,我來(lái)和你聊聊業(yè)務(wù)代碼中與數(shù)據(jù)庫(kù)事務(wù)相關(guān)的坑。

Spring針對(duì)Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API (JPA)等事務(wù)API,實(shí)現(xiàn)了一致的編程模型,而Spring的聲明式事務(wù)功能更是提供了極其方便的事務(wù)配置方式,配合Spring Boot的自動(dòng)配置,大多數(shù)Spring Boot項(xiàng)目只需要在方法上標(biāo)記@Transactional注解,即可一鍵開(kāi)啟方法的事務(wù)性配置。

據(jù)我觀察,大多數(shù)業(yè)務(wù)開(kāi)發(fā)同學(xué)都有事務(wù)的概念,也知道如果整體考慮多個(gè)數(shù)據(jù)庫(kù)操作要么成功要么失敗時(shí),需要通過(guò)數(shù)據(jù)庫(kù)事務(wù)來(lái)實(shí)現(xiàn)多個(gè)操作的一致性和原子性。但,在使用上大多僅限于為方法標(biāo)記@Transactional,不會(huì)去關(guān)注事務(wù)是否有效、出錯(cuò)后事務(wù)是否正確回滾,也不會(huì)考慮復(fù)雜的業(yè)務(wù)代碼中涉及多個(gè)子業(yè)務(wù)邏輯時(shí),怎么正確處理事務(wù)。

事務(wù)沒(méi)有被正確處理,一般來(lái)說(shuō)不會(huì)過(guò)于影響正常流程,也不容易在測(cè)試階段被發(fā)現(xiàn)。但當(dāng)系統(tǒng)越來(lái)越復(fù)雜、壓力越來(lái)越大之后,就會(huì)帶來(lái)大量的數(shù)據(jù)不一致問(wèn)題,隨后就是大量的人工介入查看和修復(fù)數(shù)據(jù)。

所以說(shuō),一個(gè)成熟的業(yè)務(wù)系統(tǒng)和一個(gè)基本可用能完成功能的業(yè)務(wù)系統(tǒng),在事務(wù)處理細(xì)節(jié)上的差異非常大。要確保事務(wù)的配置符合業(yè)務(wù)功能的需求,往往不僅僅是技術(shù)問(wèn)題,還涉及產(chǎn)品流程和架構(gòu)設(shè)計(jì)的問(wèn)題。今天這一講的標(biāo)題“20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒(méi)處理正確”中,20%這個(gè)數(shù)字在我看來(lái)還是比較保守的。

我今天要分享的內(nèi)容,就是幫助你在技術(shù)問(wèn)題上理清思路,避免因?yàn)槭聞?wù)處理不當(dāng)讓業(yè)務(wù)邏輯的實(shí)現(xiàn)產(chǎn)生大量偶發(fā)Bug。

小心Spring的事務(wù)可能沒(méi)有生效

在使用@Transactional注解開(kāi)啟聲明式事務(wù)時(shí), 第一個(gè)最容易忽略的問(wèn)題是,很可能事務(wù)并沒(méi)有生效。

實(shí)現(xiàn)下面的Demo需要一些基礎(chǔ)類,首先定義一個(gè)具有ID和姓名屬性的UserEntity,也就是一個(gè)包含兩個(gè)字段的用戶表:

@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ù)庫(kù)訪問(wèn),實(shí)現(xiàn)這樣一個(gè)Repository,新增一個(gè)根據(jù)用戶名查詢所有數(shù)據(jù)的方法:

@Repository public interface UserRepository extends JPARepository<UserEntity, Long> { List<UserEntity> findByName(String name); }

定義一個(gè)UserService類,負(fù)責(zé)業(yè)務(wù)邏輯處理。如果不清楚@Transactional的實(shí)現(xiàn)方式,只考慮代碼邏輯的話,這段代碼看起來(lái)沒(méi)有問(wèn)題。

定義一個(gè)入口方法createUserWrong1來(lái)調(diào)用另一個(gè)私有方法createUserPrivate,私有方法上標(biāo)記了@Transactional注解。當(dāng)傳入的用戶名包含test關(guān)鍵字時(shí)判斷為用戶名不合法,拋出異常,讓用戶創(chuàng)建操作失敗,期望事務(wù)可以回滾:

@Service @Slf4j public class UserService { @Autowired private UserRepository userRepository; //一個(gè)公共方法供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)有十幾個(gè)的非法用戶注冊(cè)。

這里給出@Transactional生效原則1,除非特殊配置(比如使用AspectJ靜態(tài)織入實(shí)現(xiàn)AOP),否則只有定義在public方法上的@Transactional才能生效。原因是,Spring默認(rèn)通過(guò)動(dòng)態(tài)代理的方式實(shí)現(xiàn)AOP,對(duì)目標(biāo)方法進(jìn)行增強(qiáng),private方法無(wú)法代理到,Spring自然也無(wú)法動(dòng)態(tài)增強(qiáng)事務(wù)處理邏輯。

你可能會(huì)說(shuō),修復(fù)方式很簡(jiǎn)單,把標(biāo)記了事務(wù)注解的createUserPrivate方法改為public即可。在UserService中再建一個(gè)入口方法createUserWrong2,來(lái)調(diào)用這個(gè)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!"); }

測(cè)試發(fā)現(xiàn),調(diào)用新的createUserWrong2方法事務(wù)同樣不生效。這里,我給出@Transactional生效原則2,必須通過(guò)代理過(guò)的類從外部調(diào)用目標(biāo)方法才能生效。

Spring通過(guò)AOP技術(shù)對(duì)方法進(jìn)行增強(qiáng),要調(diào)用增強(qiáng)過(guò)的方法必然是調(diào)用代理后的對(duì)象。我們嘗試修改下UserService的代碼,注入一個(gè)self,然后再通過(guò)self實(shí)例調(diào)用標(biāo)記有@Transactional注解的createUserPublic方法。設(shè)置斷點(diǎn)可以看到,self是由Spring通過(guò)CGLIB方式增強(qiáng)過(guò)的類:

  • CGLIB通過(guò)繼承方式實(shí)現(xiàn)代理類,private方法在子類不可見(jiàn),自然也就無(wú)法進(jìn)行事務(wù)增強(qiáng);
  • this指針代表對(duì)象自己,Spring不可能注入this,所以通過(guò)this訪問(wèn)方法必然不是代理。

把this改為self后測(cè)試發(fā)現(xiàn),在Controller中調(diào)用createUserRight方法可以驗(yàn)證事務(wù)是生效的,非法的用戶注冊(cè)操作可以回滾。

雖然在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); }

我們?cè)偻ㄟ^(guò)一張圖來(lái)回顧下this自調(diào)用、通過(guò)self調(diào)用,以及在Controller中調(diào)用UserService三種實(shí)現(xiàn)的區(qū)別:

通過(guò)this自調(diào)用,沒(méi)有機(jī)會(huì)走到Spring的代理類;后兩種改進(jìn)方案調(diào)用的是Spring注入的UserService,通過(guò)代理調(diào)用才有機(jī)會(huì)對(duì)createUserPublic方法進(jìn)行動(dòng)態(tài)增強(qiáng)。

這里,我還有一個(gè)小技巧,強(qiáng)烈建議你在開(kāi)發(fā)時(shí)打開(kāi)相關(guān)的Debug日志,以方便了解Spring事務(wù)實(shí)現(xiàn)的細(xì)節(jié),并及時(shí)判斷事務(wù)的執(zhí)行情況。

我們的Demo代碼使用JPA進(jìn)行數(shù)據(jù)庫(kù)訪問(wèn),可以這么開(kāi)啟Debug日志:

logging.level.org.

開(kāi)啟日志后,我們?cè)俦容^下在UserService中通過(guò)this調(diào)用和在Controller中通過(guò)注入的UserService Bean調(diào)用createUserPublic區(qū)別。很明顯,this調(diào)用因?yàn)闆](méi)有走代理,事務(wù)沒(méi)有在createUserPublic方法上生效,只在Repository的save方法層面生效:

//在UserService中通過(guò)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中通過(guò)注入的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

你可能還會(huì)考慮一個(gè)問(wèn)題,這種實(shí)現(xiàn)在Controller里處理了異常顯得有點(diǎn)繁瑣,還不如直接把createUserWrong2方法加上@Transactional注解,然后在Controller中直接調(diào)用這個(gè)方法。這樣一來(lái),既能從外部(Controller中)調(diào)用UserService中的方法,方法又是public的能夠被動(dòng)態(tài)代理AOP增強(qiáng)。

你可以試一下這種方法,但很容易就會(huì)踩第二個(gè)坑,即因?yàn)闆](méi)有正確處理異常,導(dǎo)致事務(wù)即便生效也不一定能回滾。

事務(wù)即便生效也不一定能回滾

通過(guò)AOP實(shí)現(xiàn)事務(wù)處理可以理解為,使用try…catch…來(lái)包裹標(biāo)記了@Transactional注解的方法,當(dāng)方法出現(xiàn)了異常并且滿足一定條件的時(shí)候,在catch里面我們可以設(shè)置事務(wù)回滾,沒(méi)有異常則直接提交事務(wù)。

這里的“一定條件”,主要包括兩點(diǎn)。

第一,只有異常傳播出了標(biāo)記了@Transactional注解的方法,事務(wù)才能回滾。在Spring的TransactionAspectSupport里有個(gè) 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才會(huì)回滾事務(wù)。

打開(kāi)Spring的DefaultTransactionAttribute類能看到如下代碼塊,可以發(fā)現(xiàn)相關(guān)證據(jù),通過(guò)注釋也能看到Spring這么做的原因,大概的意思是受檢異常一般是業(yè)務(wù)異常,或者說(shuō)是類似另一種方法的返回值,出現(xiàn)這樣的異??赡軜I(yè)務(wù)還能完成,所以不會(huì)主動(dòng)回滾;而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); }

接下來(lái),我和你分享2個(gè)反例。

重新實(shí)現(xiàn)一下UserService中的注冊(cè)用戶操作:

  • 在createUserWrong1方法中會(huì)拋出一個(gè)RuntimeException,但由于方法內(nèi)catch了所有異常,異常無(wú)法從方法傳播出去,事務(wù)自然無(wú)法回滾。
  • 在createUserWrong2方法中,注冊(cè)用戶的同時(shí)會(huì)有一次otherTask文件讀取操作,如果文件讀取失敗,我們希望用戶注冊(cè)的數(shù)據(jù)庫(kù)操作回滾。雖然這里沒(méi)有捕獲異常,但因?yàn)閛therTask方法拋出的是受檢異常,createUserWrong2傳播出去的也是受檢異常,事務(wù)同樣不會(huì)回滾。
@Service @Slf4j public class UserService { @Autowired private UserRepository userRepository; //異常無(wú)法傳播出方法,導(dǎo)致事務(wù)無(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ú)法讓事務(wù)回滾 @Transactional public void createUserWrong2(String name) throws IOException { u(new UserEntity(name)); otherTask(); } //因?yàn)槲募淮嬖冢欢〞?huì)拋出一個(gè)IOException private void otherTask() throws IOException { ("file-that-not-exist")); } }

Controller中的實(shí)現(xiàn),僅僅是調(diào)用UserService的createUserWrong1和createUserWrong2方法,這里就貼出實(shí)現(xiàn)了。這2個(gè)方法的實(shí)現(xiàn)和調(diào)用,雖然完全避開(kāi)了事務(wù)不生效的坑,但因?yàn)楫惓L幚聿划?dāng),導(dǎo)致程序沒(méi)有如我們期望的文件操作出現(xiàn)異常時(shí)回滾事務(wù)。

現(xiàn)在,我們來(lái)看下修復(fù)方式,以及如何通過(guò)日志來(lái)驗(yàn)證是否修復(fù)成功。針對(duì)這2種情況,對(duì)應(yīng)的修復(fù)方法如下。

第一,如果你希望自己捕獲異常進(jìn)行處理的話,也沒(méi)關(guān)系,可以手動(dòng)設(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”的提示,表明手動(dòng)請(qǐng)求回滾:

[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ù)(來(lái)突破默認(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>)]

在這個(gè)例子中,我們展現(xiàn)的是一個(gè)復(fù)雜的業(yè)務(wù)邏輯,其中有數(shù)據(jù)庫(kù)操作、IO操作,在IO操作出現(xiàn)問(wèn)題時(shí)希望讓數(shù)據(jù)庫(kù)事務(wù)也回滾,以確保邏輯的一致性。在有些業(yè)務(wù)邏輯中,可能會(huì)包含多次數(shù)據(jù)庫(kù)操作,我們不一定希望將兩次操作作為一個(gè)事務(wù)來(lái)處理,這時(shí)候就需要仔細(xì)考慮事務(wù)傳播的配置了,否則也可能踩坑。

請(qǐng)確認(rèn)事務(wù)傳播配置是否符合自己的業(yè)務(wù)邏輯

有這么一個(gè)場(chǎng)景:一個(gè)用戶注冊(cè)的操作,會(huì)插入一個(gè)主用戶到用戶表,還會(huì)注冊(cè)一個(gè)關(guān)聯(lián)的子用戶。我們希望將子用戶注冊(cè)的數(shù)據(jù)庫(kù)操作作為一個(gè)獨(dú)立事務(wù)來(lái)處理,即使失敗也不會(huì)影響主流程,即不影響主用戶的注冊(cè)。

接下來(lái),我們模擬一個(gè)實(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)樽詈笪覀儝伋隽艘粋€(gè)運(yùn)行時(shí)異常,錯(cuò)誤原因是用戶狀態(tài)無(wú)效,所以子用戶的注冊(cè)肯定是失敗的。我們期望子用戶的注冊(cè)作為一個(gè)事務(wù)單獨(dú)回滾,不影響主用戶的注冊(cè),這樣的邏輯可以實(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"); } }

我們?cè)贑ontroller里實(shí)現(xiàn)一段測(cè)試代碼,調(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

你馬上就會(huì)意識(shí)到,不對(duì)呀,因?yàn)檫\(yùn)行時(shí)異常逃出了@Transactional注解標(biāo)記的createUserWrong方法,Spring當(dāng)然會(huì)回滾事務(wù)了。如果我們希望主方法不回滾,應(yīng)該把子方法拋出的異常捕獲了。

也就是這么改,把包裹上catch,這樣外層主方法就不會(huì)出現(xiàn)異常了:

@Transactional public void createUserWrong2(UserEntity entity) { createMainUser(entity); try{ (entity); } catch (Exception ex) { // 雖然捕獲了異常,但是因?yàn)闆](méi)有開(kāi)啟新事務(wù),而當(dāng)前事務(wù)因?yàn)楫惓R呀?jīng)被標(biāo)記為rollback了,所以最終還是會(huì)回滾。 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行所示,對(duì)createUserWrong2方法開(kāi)啟了異常處理;
  • 如第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)了一個(gè)UnexpectedRollbackException,異常描述提示最終這個(gè)事務(wù)回滾了,而且是靜默回滾的。之所以說(shuō)是靜默,是因?yàn)閏reateUserWrong2方法本身并沒(méi)有出異常,只不過(guò)提交后發(fā)現(xiàn)子方法已經(jīng)把當(dāng)前事務(wù)設(shè)置為了回滾,無(wú)法完成提交。

這挺反直覺(jué)的。我們之前說(shuō),出了異常事務(wù)不一定回滾,這里說(shuō)的卻是不出異常,事務(wù)也不一定可以提交。原因是,主方法注冊(cè)主用戶的邏輯和子方法注冊(cè)子用戶的邏輯是同一個(gè)事務(wù),子邏輯標(biāo)記了事務(wù)需要回滾,主邏輯自然也不能提交了。

看到這里,修復(fù)方式就很明確了,想辦法讓子邏輯在獨(dú)立事務(wù)中運(yùn)行,也就是改一下SubUserService注冊(cè)子用戶的方法,為注解加上propagation = Pro來(lái)設(shè)置REQUIRES_NEW方式的事務(wù)傳播策略,也就是執(zhí)行到這個(gè)方法時(shí)需要開(kāi)啟新的事務(wù),并掛起當(dāng)前事務(wù):

@Transactional(propagation = Pro) public void createSubUserWithExceptionRight(UserEntity entity) { log.info("createSubUserWithExceptionRight start"); u(entity); throw new RuntimeException("invalid status"); }

主方法沒(méi)什么變化,同樣需要捕獲異常,防止異常漏出去導(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行日志提示我們針對(duì)createUserRight方法開(kāi)啟了主方法的事務(wù);
  • 第2行日志提示創(chuàng)建主用戶完成;
  • 第3行日志可以看到主事務(wù)掛起了,開(kāi)啟了一個(gè)新的事務(wù),針對(duì)createSubUserWithExceptionRight方案,也就是我們的創(chuàng)建子用戶的邏輯;
  • 第4行日志提示子方法事務(wù)回滾;
  • 第5行日志提示子方法事務(wù)完成,繼續(xù)主方法之前掛起的事務(wù);
  • 第6行日志提示主方法捕獲到了子方法的異常;
  • 第8行日志提示主方法的事務(wù)提交了,隨后我們?cè)贑ontroller里沒(méi)看到靜默回滾的異常。
[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)行測(cè)試程序看到如下結(jié)果,getUserCount得到的用戶數(shù)量為1,代表只有一個(gè)用戶也就是主用戶注冊(cè)完成了,符合預(yù)期:

重點(diǎn)回顧

今天,我針對(duì)業(yè)務(wù)代碼中最常見(jiàn)的使用數(shù)據(jù)庫(kù)事務(wù)的方式,即Spring聲明式事務(wù),與你總結(jié)了使用上可能遇到的三類坑,包括:

第一,因?yàn)榕渲貌徽_,導(dǎo)致方法上的事務(wù)沒(méi)生效。我們務(wù)必確認(rèn)調(diào)用@Transactional注解標(biāo)記的方法是public的,并且是通過(guò)Spring注入的Bean進(jìn)行調(diào)用的。

第二,因?yàn)楫惓L幚聿徽_,導(dǎo)致事務(wù)雖然生效但出現(xiàn)異常時(shí)沒(méi)回滾。Spring默認(rèn)只會(huì)對(duì)標(biāo)記@Transactional注解的方法出現(xiàn)了RuntimeException和Error的時(shí)候回滾,如果我們的方法捕獲了異常,那么需要通過(guò)手動(dòng)編碼處理事務(wù)回滾。如果希望Spring針對(duì)其他異常也可以回滾,那么可以相應(yīng)配置@Transactional注解的rollbackFor和noRollbackFor屬性來(lái)覆蓋其默認(rèn)設(shè)置。

第三,如果方法涉及多次數(shù)據(jù)庫(kù)操作,并希望將它們作為獨(dú)立的事務(wù)進(jìn)行提交或回滾,那么我們需要考慮進(jìn)一步細(xì)化配置事務(wù)傳播方式,也就是@Transactional注解的Propagation屬性。

可見(jiàn),正確配置事務(wù)可以提高業(yè)務(wù)項(xiàng)目的健壯性。但,又因?yàn)榻研詥?wèn)題往往體現(xiàn)在異常情況或一些細(xì)節(jié)處理上,很難在主流程的運(yùn)行和測(cè)試中發(fā)現(xiàn),導(dǎo)致業(yè)務(wù)代碼的事務(wù)處理邏輯往往容易被忽略,因此我在代碼審查環(huán)節(jié)一直很關(guān)注事務(wù)是否正確處理。

如果你無(wú)法確認(rèn)事務(wù)是否真正生效,是否按照預(yù)期的邏輯進(jìn)行,可以嘗試打開(kāi)Spring的部分Debug日志,通過(guò)事務(wù)的運(yùn)作細(xì)節(jié)來(lái)驗(yàn)證。也建議你在單元測(cè)試時(shí)盡量覆蓋多的異常場(chǎng)景,這樣在重構(gòu)時(shí),也能及時(shí)發(fā)現(xiàn)因?yàn)榉椒ǖ恼{(diào)用方式、異常處理邏輯的調(diào)整,導(dǎo)致的事務(wù)失效問(wèn)題。

1.《如何處理偶發(fā)的Bug?總結(jié)很全面速看!20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒(méi)處理正確》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識(shí),僅代表作者本人觀點(diǎn),與本網(wǎng)站無(wú)關(guān),侵刪請(qǐng)聯(lián)系頁(yè)腳下方聯(lián)系方式。

2.《如何處理偶發(fā)的Bug?總結(jié)很全面速看!20%的業(yè)務(wù)代碼的Spring聲明式事務(wù),可能都沒(méi)處理正確》僅供讀者參考,本網(wǎng)站未對(duì)該內(nèi)容進(jìn)行證實(shí),對(duì)其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。

3.文章轉(zhuǎn)載時(shí)請(qǐng)保留本站內(nèi)容來(lái)源地址,http://f99ss.com/gl/2199135.html

上一篇

關(guān)于109寶寶高級(jí)強(qiáng)力怎么樣,你需要知道這些夢(mèng)幻西游:109級(jí)強(qiáng)力大唐展示,無(wú)級(jí)別裝備并不是她的亮點(diǎn)?

下一篇

關(guān)于115如何退出圈子,你需要知道這些“垮掉的一代”文化地標(biāo)

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

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

如何處理偶發(fā)的Bug相關(guān)介紹,今天,我來(lái)和你聊聊業(yè)務(wù)代碼中與數(shù)據(jù)庫(kù)事務(wù)相關(guān)的坑。 Spring針對(duì)Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API (JP...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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