今天,我來(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