網(wǎng)站ip訪問做圖表中國十大新聞網(wǎng)站排名
大家好,我是墨哥(隱墨星辰)。今天的內(nèi)容來源于兩個(gè)線上問題,主要和大家聊聊為什么支付系統(tǒng)中基本只使用事務(wù)模板方法,而不使用聲明式事務(wù)@Transaction注解,以及使用afterCommit()出現(xiàn)連接未按預(yù)期釋放導(dǎo)致的性能問題。
1. 為什么不使用@Transaction注解
以前寫管理平臺的代碼時(shí),經(jīng)常使用@Transaction注解,也就是所謂的聲明式事務(wù),簡單而實(shí)用,但是在做支付后,基本上沒有使用@Transaction,全部使用事務(wù)模板來做。主要有兩個(gè)考慮:
1)事務(wù)的粒度控制不夠靈活,容易出現(xiàn)長事務(wù)
@Transactional注解通常應(yīng)用于方法級別,這意味著被注解的方法將作為一個(gè)整體運(yùn)行在事務(wù)上下文中。在復(fù)雜的支付流程中,需要做各種運(yùn)算處理,很多前置處理是不需要放在事務(wù)里面的。
而使用事務(wù)模板的話,就可以更精細(xì)的控制事務(wù)的開始和結(jié)束,以及更細(xì)粒度的錯(cuò)誤處理邏輯。
@Transactional
public PayOrder process(PayRequest request) {validate(request);PayOrder payOrder = buildOrder(request);save(payOrder);// 其它處理otherProcess(payOrder);
}
比如上面的校驗(yàn),構(gòu)建訂單,其它處理都不需要放在事務(wù)中。
如果把@Transactional從process()中拿走,放到save()方法,也會面臨另外的問題:otherProcess()依賴數(shù)據(jù)庫保存成功后才能執(zhí)行,如果保存失敗,不能執(zhí)行otherProcess()處理。全部考慮進(jìn)來后,使用注解處理起來就很麻煩。
2)事務(wù)傳播行為的復(fù)雜性
@Transactional注解支持不同的事務(wù)傳播行為,雖然這提供了靈活性,但在實(shí)際應(yīng)用中,錯(cuò)誤的事務(wù)傳播配置可能導(dǎo)致難以追蹤的問題,如意外的事務(wù)提交或回滾。
而且經(jīng)常有多層子函數(shù)調(diào)用,很容易子函數(shù)有一個(gè)耗時(shí)操作(比如RPC調(diào)用或請求外部應(yīng)用),一方面可能出事長事務(wù),另一方面還可能因?yàn)橥庹{(diào)拋異步,導(dǎo)致事務(wù)回滾,數(shù)據(jù)庫中都沒有記錄保存。
以前就在生產(chǎn)上碰到過類似的問題,因?yàn)樵诟阜椒ㄊ褂昧?#64;Transactional注解,子函數(shù)拋出異常,去數(shù)據(jù)庫找問題單據(jù),竟然沒有記錄,翻代碼一行行看,才發(fā)現(xiàn)問題。
2. afterCommit存在的問題及解法
有一次參與線上壓測,在流量上去后,應(yīng)用持續(xù)報(bào)獲取數(shù)據(jù)庫連接超時(shí),排查很久才找到原因,問題非常經(jīng)典,值得和大家聊聊。
無論在支付系統(tǒng),還是電商系統(tǒng),還是其它各種業(yè)務(wù)系統(tǒng),都存在這樣的需求:在一個(gè)事務(wù)中既保存多個(gè)數(shù)據(jù)庫表,又要外發(fā)請求,且這個(gè)外發(fā)請求耗時(shí)很長。
比如:方法A保存數(shù)據(jù)庫表A,方法B保存數(shù)據(jù)庫表B,并且要外發(fā)給其它系統(tǒng)且耗時(shí)長,方法C要保存數(shù)據(jù)庫表C。這三個(gè)方法需要在一個(gè)事務(wù)里面。
我見過三種方案:
方案一:不管三七二十一,就直接放在一個(gè)事務(wù)中。請求量不大時(shí),看不出長事務(wù)的影響。
方案二:知道使用Spring提供的模板方法:TransactionSynchronizationAdapter.afterCommit()。外發(fā)請求耗時(shí)長過長時(shí),在大并發(fā)下仍然有連接未能及時(shí)釋放的問題。
方案三:自己實(shí)現(xiàn)事務(wù)模板方法,在Spring提交事務(wù)并釋放連接后,再執(zhí)行耗時(shí)長的外發(fā)。
第一種沒什么好說的,下面介紹方案二和方案三,兩者區(qū)別如下圖所示:
在支付系統(tǒng)中,經(jīng)常需要做一些流程編排,這些流程操作需要放在一個(gè)事務(wù)中,比如先保存主單據(jù),再保存流水單據(jù),然后外發(fā)銀行請求扣款,有同學(xué)寫的代碼類似這樣:
主方法偽代碼(流程引擎入口):
public void process(FlowContext context) {// 獲取流程處理鏈List<FlowProcess> flows = fetchFlow(context);for (FlowProcess flow : flows) {// 使用事務(wù)模板dataSourceManager.getTransactionTemplate().execute(status -> {// 執(zhí)行子流程flow.execute(context); // 更新主單信息context.getPayOrder().putJournal(context.getJournal());context.getPayOrder().transToNextStatus(context.getJournal().getTargetStatus());save(context.getPayOrder());return true;});}
}
其中一個(gè)外發(fā)銀行子流程偽代碼
public void execute(FlowContext context) {Journal journal = buildJournal(context);// 子函數(shù)里面保存了3張表的數(shù)據(jù)save(journal);TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCommit() {// 事務(wù)提交后,再發(fā)送給外部銀行g(shù)atewayService.sendToChannel(journal);}});
}
預(yù)期是事務(wù)提交后再調(diào)用發(fā)給銀行。
但是實(shí)際情況卻是,Spring提交事務(wù)后,調(diào)用了afterCommit(),但是并沒有釋放連接,導(dǎo)致在外發(fā)銀行的長達(dá)1000多毫秒的時(shí)間內(nèi),數(shù)據(jù)庫連接一直在保持,而不是提交事務(wù)后馬上歸還了連接,加上線上服務(wù)器的連接數(shù)只分配了30個(gè)給每臺應(yīng)用。這就意味著最大并發(fā)也小于30。
通過查看AbstractPlatformTransactionManager.java,發(fā)現(xiàn)是先調(diào)用:riggerAfterCommit(status),然后才清理并釋放連接:cleanupAfterCompletion(status)。
private void processCommit(DefaultTransactionStatus status) throws TransactionException {try {// 其它代碼省略... ...// Trigger afterCommit callbacks, with an exception thrown there// propagated to callers but the transaction still considered as committed.try {triggerAfterCommit(status);}// 其它代碼省略... ...}finally {cleanupAfterCompletion(status);}}
解決辦法:自己創(chuàng)建一個(gè)事務(wù)模板,實(shí)現(xiàn)afterCommit()。
public class FlowTransactionTemplate { public static <R> R execute(FlowContext context, Supplier<R> callback) { TransactionTemplate template = context.getTransactionTemplate();Assert.notNull(template, "transactionTemplate cannot be null"); PlatformTransactionManager transactionManager = template.getTransactionManager();Assert.notNull(transactionManager, "transactionManager cannot be null"); boolean commit = false;try {TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // Corrected "TranscationStatus" to "TransactionStatus"R result = null;try {result = callback.get();} catch (Exception e) {transactionManager.rollback(status); throw e;}transactionManager.commit(status);commit = true;return result;} finally {if (commit) {invokeAfterCommit(context);}}}private static void invokeAfterCommit(FlowContext context) {try {context.invokeAfterCommit();} catch (Exception e) {// 打印日志... ...}}
}
FlowContext加上事務(wù)提交后的執(zhí)行的鉤子方法,在鉤子方法中實(shí)現(xiàn)一些長耗時(shí)工作:
public class FlowContext {// 其它代碼不變... ...private List<AfterCommitHook> afterCommitHooks = new ArrayList<>();public void registerAfterCommitHook(AfterCommitHook hook) {afterCommitHooks.add(hook);}public void invokeAfterCommit() {try {for(AfterCommitHook hook : afterCommitHooks) {hook.afterCommit();}} catch (Exception e) {// 異常處理... ...} finally {// 鉤子已執(zhí)行完,清理掉afterCommitHooks.clear();}}public static abstract class AfterCommitHook {public abstract void afterCommit();}
}
主流程修改為直接調(diào)用:FlowTranscationTemplate.execute。
public void process(FlowContext context) {context.setTransactionTemplate(dataSourceManager.getTransactionTemplate());List<FlowProcess> flows = fetchFlow(context);for (FlowProcess flow : flows) {// 把Spring模板方法修改自己的模板方法,其它不變FlowTransactionTemplate.execute(context, () -> {flow.execute(context);context.getPayOrder().putJournal(context.getJournal());context.getPayOrder().transToNextStatus(context.getJournal().getTargetStatus());save(context.getPayOrder());return true;});}
}
子流程修改為把a(bǔ)fterCommit要做的事注冊到流程上下文中:
public void execute(FlowContext context) {Journal journal = buildJournal(context);// 子函數(shù)里面保存了3張表的數(shù)據(jù)save(journal);// 把外發(fā)動(dòng)作注冊到流程上下文中的鉤子方法中,// 而不是直接使用Spring原生的TransactionSynchronizationAdapter.afterCommit()// 其它保持不變context.registerAfterCommitHook(() -> {// 事務(wù)提交后發(fā)給銀行g(shù)atewayService.sendToChannel(journal);});
}
這樣處理的優(yōu)點(diǎn)有幾個(gè):
- 清晰的事務(wù)邊界管理:通過顯式控制事務(wù)的提交和回調(diào)執(zhí)行,增加了代碼的可控性。
- 資源使用優(yōu)化:確保數(shù)據(jù)庫連接在不需要時(shí)能夠及時(shí)釋放,提升了資源的使用效率。
- 靈活的后續(xù)操作擴(kuò)展:允許注冊多個(gè)回調(diào),方便地添加事務(wù)提交后需要執(zhí)行的操作,增強(qiáng)了代碼的擴(kuò)展性和復(fù)用性。
有個(gè)注意的點(diǎn),就是確保invokeAfterCommit的穩(wěn)健性,代碼里是通過捕獲異常打印日志,避免對其它操作有影響。
3. 擴(kuò)展:長事務(wù)
長事務(wù)指的是在數(shù)據(jù)庫管理和應(yīng)用開發(fā)中,持續(xù)時(shí)間較長的事務(wù)處理過程。一般來說,在分布式應(yīng)用中,每個(gè)服務(wù)器分配的連接數(shù)是有限的,比如每個(gè)服務(wù)器20個(gè)連接,這就要求我們必要盡量減少長事務(wù),以便處理更多請求。
典型的方案有:
1)非事務(wù)類操作,就放在事務(wù)外面。比如前置處理,先請求下游獲取資源,做各種校驗(yàn),全部通過后,再啟動(dòng)事務(wù)。還有就是使用hook的方式,等事務(wù)提交后,再請求外部耗時(shí)的服務(wù)。
2)事務(wù)拆分。把一個(gè)長事務(wù)拆分為多個(gè)短事務(wù)。
3)異步處理。有點(diǎn)類似hook的方案。
4. 結(jié)束語
Spring事務(wù)管理提供了強(qiáng)大而靈活的機(jī)制來處理復(fù)雜的業(yè)務(wù)邏輯,但是每個(gè)特性和工具的使用都需要對其行為有深入的理解,而不能想當(dāng)然。比如文中的afterCommit就是這樣一個(gè)典型例子。
自定義事務(wù)模板的實(shí)踐向我們展示了,雖然@Transcation注解很方便,但在一些特殊場景下,需要我們深入了解框架的工作原理并結(jié)合實(shí)際業(yè)務(wù)需求,既高效地利用Spring提供的工具,同時(shí)也規(guī)避潛在的坑點(diǎn)。
希望本文能夠幫助讀者更好地理解和應(yīng)用Spring事務(wù)管理中的afterCommit鉤子,以及如何在對資源或性能要求很嚴(yán)格的情況下,比如支付場景,如何定義自己的事務(wù)模板,幫助我們構(gòu)建更健壯、更高效的應(yīng)用。
這是《百圖解碼支付系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)》專欄系列文章中的第(30)篇。和墨哥(隱墨星辰)一起深入解碼支付系統(tǒng)的方方面面。
歡迎轉(zhuǎn)載。
Github(PDF文檔全集,不定時(shí)更新):GitHub - yinmo-sc/Decoding-Payment-System-Book: 百圖解碼支付系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)
精選
專欄地址:百圖解碼支付系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)
《百圖解碼支付系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)》專欄介紹
《百圖解碼支付系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)》專欄大綱及文章鏈接匯總(進(jìn)度更新于2023.2.4)
領(lǐng)域相關(guān)(部分):
支付行業(yè)黑話:支付系統(tǒng)必知術(shù)語一網(wǎng)打盡
跟著圖走,學(xué)支付:在線支付系統(tǒng)設(shè)計(jì)的圖解教程
圖解收單平臺:打造商戶收款的高效之道
圖解結(jié)算平臺:準(zhǔn)確高效給商戶結(jié)款
圖解收銀臺:支付系統(tǒng)承上啟下的關(guān)鍵應(yīng)用
圖解支付引擎:資產(chǎn)流動(dòng)的樞紐
圖解渠道網(wǎng)關(guān):不只是對接渠道的接口(一)
技術(shù)專題(部分):
交易流水號的藝術(shù):掌握支付系統(tǒng)的業(yè)務(wù)ID生成指南
揭密支付安全:為什么你的交易無法被篡改
金融密語:揭秘支付系統(tǒng)的加解密藝術(shù)
支付系統(tǒng)日志設(shè)計(jì)完全指南:構(gòu)建高效監(jiān)控和問題排查體系的關(guān)鍵基石
避免重復(fù)扣款:分布式支付系統(tǒng)的冪等性原理與實(shí)踐
支付系統(tǒng)的心臟:簡潔而精妙的狀態(tài)機(jī)設(shè)計(jì)與核心代碼實(shí)現(xiàn)
精確掌控并發(fā):固定時(shí)間窗口算法在分布式環(huán)境下并發(fā)流量控制的設(shè)計(jì)與實(shí)現(xiàn)
精確掌控并發(fā):滑動(dòng)時(shí)間窗口算法在分布式環(huán)境下并發(fā)流量控制的設(shè)計(jì)與實(shí)現(xiàn)