數(shù)據(jù)庫事務(wù)
在編程的世界里,數(shù)據(jù)非常重要,數(shù)據(jù)庫擔(dān)任了很重要的角色,數(shù)據(jù)庫擁有的ACID特性,我們只管聲明事務(wù),通過sql對(duì)數(shù)據(jù)庫進(jìn)行批量操作,就能夠達(dá)到目標(biāo),其背后是數(shù)據(jù)庫做了很多工作,幫我們處理了很多異常,比如數(shù)據(jù)庫機(jī)器斷電,如何保證強(qiáng)一致性呢,原來是數(shù)據(jù)庫會(huì)有兩個(gè)文件,數(shù)據(jù)庫文件和日志文件,調(diào)用方法開啟事務(wù),執(zhí)行的sql,都會(huì)存儲(chǔ)在日志文件中,捕捉到調(diào)用方發(fā)起的commit命令后,才將日志中存儲(chǔ)的事務(wù)sql執(zhí)行到數(shù)據(jù)庫,在調(diào)用方提交事務(wù)后,機(jī)器斷電的情況下,這是事務(wù)sql已經(jīng)存在于日志文件中,在機(jī)器重啟后,通過檢查日志文件,對(duì)已提交狀態(tài)但未完成的事務(wù)進(jìn)行后續(xù)處理,提取事務(wù)sql恢復(fù)執(zhí)行來保證強(qiáng)一致性。
XA Transaction
在單機(jī)環(huán)境下,通過數(shù)據(jù)庫事務(wù),我們就能很完美的解決一致性的問題。隨著系統(tǒng)訪問量的上升,單機(jī)數(shù)據(jù)庫慢慢出現(xiàn)性能瓶頸,這是會(huì)對(duì)單體服務(wù)進(jìn)行拆分,同時(shí)對(duì)數(shù)據(jù)庫也進(jìn)行拆分,伴隨著垂直分庫,進(jìn)入到分布式領(lǐng)域,出現(xiàn)了新的問題。原先要執(zhí)行的業(yè)務(wù)操作sql都是在一個(gè)數(shù)據(jù)庫中完成,但如今卻分布在不同的物理庫上,無法通過原有的事務(wù)來處理,數(shù)據(jù)庫廠商引入了2pc理論(兩階段提交)即XA,引入?yún)f(xié)調(diào)者,參與者事務(wù),在開啟全局事務(wù)時(shí),協(xié)調(diào)者會(huì)鎖住整個(gè)事務(wù),現(xiàn)在各個(gè)分庫執(zhí)行precommit預(yù)提交,協(xié)調(diào)者檢測到所有的commit都通過后,通知各個(gè)分庫執(zhí)行commit,這種方式有個(gè)致命缺點(diǎn),會(huì)鎖住整個(gè)事務(wù),這期間相應(yīng)的表都不能訪問,隨著并發(fā)量的上升,性能會(huì)急劇下降。這是通過犧牲一定的可用性來換去一致性的做法
MQ消息
以訂單扣庫存生成訂單為例
//業(yè)務(wù)邏輯
try{
discontStock(skuId, quantity);
saveOrderDb();
}catch(Execption e){
try{
returnStock(skuId, quantity);
}catch(e){
try{
sendReturnStockMsg();
}catch(e){
saveReturnStockMsgToDeadLetter();
}
}
}
//通過定時(shí)任務(wù)進(jìn)行消息重發(fā)
task.ScanDeadLetterForRePushMsg()
缺點(diǎn):
- 1.訂單在下單失敗后,需要干很多雜活,下單是關(guān)鍵業(yè)務(wù),會(huì)影響損耗一部分性能
- 2.訂單服務(wù)在下單失敗,catch回滾資源階段宕機(jī)了,這個(gè)情況下,不能保證數(shù)據(jù)一致性,需要人工介入修依賴方數(shù)據(jù)
補(bǔ)償事務(wù)TCC
為解決性能問題,引入了事務(wù)的補(bǔ)償機(jī)制,和XA正好相反,著力于提高可用性,屬于3pc,根據(jù)CAP和BASE原理,通過Try、Confirm、Cancel來實(shí)現(xiàn),核心思想是為 每個(gè)操作都注冊(cè)一對(duì)確認(rèn)和取消操作。在整個(gè)try成功則執(zhí)行各系統(tǒng)的confirm方法,失敗,則執(zhí)行各系統(tǒng)的cancel方法。只要try成功,confirm階段一定成功,會(huì)通過重試來保證:
- try階段: 業(yè)務(wù)檢查和預(yù)留系統(tǒng)資源
- confirm階段:try成功,就會(huì)開始執(zhí)行confirm,并且通過重試保證confirm一定能成功,這里不允許出錯(cuò)
- cancel階段:try階段出錯(cuò)后執(zhí)行cancel,并且通過重試保證cancel邏輯,一定能被調(diào)用成功。
基于tcc_transaction框架二次開發(fā)來適應(yīng)業(yè)務(wù)
- 參考:https://github.com/changmingxie/tcc-transaction/tree/master
- 原理:通過在try方法的接口上添加注解Compensable,注解信息提供confirm和cancel方法調(diào)用位置,使用aop攔截try方法,提取compensable注解信息,完成為事務(wù),注冊(cè)確認(rèn)和取消操作的操作。在事務(wù)發(fā)起方的try階段完成后,aop根據(jù) try階段是否拋異常來判定進(jìn)入事務(wù)confirm or cancel階段,完成狀態(tài)流轉(zhuǎn)
- 場景分析:目標(biāo)方法調(diào)用前 做事務(wù)狀態(tài)存儲(chǔ),如try階段,事務(wù)存儲(chǔ)成功,目標(biāo)方法執(zhí)行前宕機(jī),會(huì)由事務(wù)發(fā)起方進(jìn)行rollback操作,此時(shí)要求目標(biāo)機(jī)器在 confirm和cancel階段實(shí)現(xiàn)業(yè)務(wù)冪等;
- 優(yōu)點(diǎn):
- 提供了對(duì)場景各種傳播特性的支持: required require_new supports mandatory
- 缺點(diǎn):
- 1.要求事務(wù)參與方,提供事務(wù)的存儲(chǔ)方式,還需要在理解的原理的基礎(chǔ)上進(jìn)行配置(比如:配置當(dāng)前參與者事務(wù)的存儲(chǔ)位置,手動(dòng)依賴spring相關(guān)的xml配置),相對(duì)復(fù)雜;
- 2.事務(wù)管理操作完全依賴于業(yè)務(wù)系統(tǒng)的 aop邏輯,會(huì)給業(yè)務(wù)系統(tǒng)造成一定性能損耗,而且在業(yè)務(wù)系統(tǒng)宕機(jī)時(shí),會(huì)中斷事務(wù)管理,雖然業(yè)務(wù)重啟也能恢復(fù),但會(huì)延長事務(wù)數(shù)據(jù)流轉(zhuǎn)到最終態(tài)的時(shí)間;
- 3.要求tcc三階段操作的入?yún)⑾嗤?,使用起來不太靈活。
針對(duì)上述問題,進(jìn)行了二次開發(fā)
改進(jìn):
- 通過一個(gè)tcc服務(wù)來做tcc事務(wù)管理,aop的功能弱化為在try執(zhí)行后調(diào)用tcc服務(wù)上傳try階段的狀態(tài)
- 調(diào)整confirm 和cancel方法參數(shù)為 transId\branchId,由事務(wù)參與方自己維護(hù),業(yè)務(wù)參數(shù)到 transId\branchId的映射
- 簡化為只在try方法調(diào)用之后才通過aop調(diào)用tcc服務(wù)進(jìn)行事務(wù)的上傳操作,事務(wù)狀態(tài)維護(hù)交由tcc服務(wù)來管理
- 增強(qiáng):添加熔斷降級(jí)邏輯,假如調(diào)用tcc服務(wù)上傳try階段狀態(tài)失敗了,先嘗試重試幾次,記錄失敗次數(shù),到達(dá)一定次數(shù)(150次)了觸發(fā)熔斷,直接通過aop去進(jìn)行參與者事務(wù)的confirm or cancel邏輯調(diào)用(有損)
實(shí)現(xiàn):
@Description("事務(wù)實(shí)體類")
public class Entity extends BaseEntity implements Serializable {
@Description("id")
public long id;
@Description("分布式事務(wù)事務(wù)id")
public String transId;
@Description("分布式事務(wù)事務(wù)分支id")
public String branchId;
@Description("分布式事務(wù)步驟id")
public String stepId;
@Description("分布式事務(wù)服務(wù)名")
public String serviceName;
@Description("分布式事務(wù)失敗方法名")
public String cancelName;
@Description("分布式事務(wù)成功方法名")
public String confirmName;
@Description("是否成功")
public boolean flag;
@Description("分布式事務(wù)調(diào)用方法")
public List<String> invokeList = new CopyOnWriteArrayList<>();
@Description("分布式事務(wù)是否操作")
public boolean isTccOperator;
@Description("分布式事務(wù)校驗(yàn)碼")
public String checkSum;
@Description("分布式事務(wù)操作異常信息")
public String errorMsg;
@Description("分布式事務(wù)創(chuàng)建時(shí)間")
public Date date;
}
- 1.實(shí)現(xiàn)一個(gè)aop攔截注解Compensable,獲取try對(duì)應(yīng)的confirm和cancel方法,以及try調(diào)用結(jié)果,提交給tccService管理
- 2.tccService 在接受到參與者事務(wù)后,狀態(tài)保存到redis,在檢測到事務(wù)發(fā)起方的try階段提交結(jié)果后,判斷走整個(gè)事務(wù)走confirm or cancel邏輯,confirm or cancel邏輯由tcc服務(wù)發(fā)起dubbo泛化調(diào)用來完成,每完成一組confirm or cancel調(diào)用,立即更新redis參與者事務(wù)的狀態(tài)。
- 3.實(shí)現(xiàn)一個(gè)定時(shí)任務(wù),用redis scan掃描未完成的事務(wù),拉取到服務(wù)中,檢測事務(wù)狀態(tài)整個(gè)事務(wù)中有出現(xiàn)參與者在try階段失敗,走cancel邏輯,沒有參與者失敗,走confirm邏輯,在完成后,完成事務(wù)統(tǒng)計(jì)和添加redis事務(wù)key前綴為completed_, 這樣做的原因是,redis是單線程執(zhí)行的,可以避免執(zhí)行scan命令過長,影響性能。
可用性分析:
- 1.在事務(wù)執(zhí)行過程中,tccService(redis)宕機(jī)了,在重啟時(shí),通過定時(shí)任務(wù)用ScheduledThreadPoolExecutor,用redis scan掃描未完成的事務(wù),拉取到服務(wù)中,檢測事務(wù)狀態(tài)整個(gè)事務(wù)中有出現(xiàn)參與者在try階段失敗,走cancel邏輯,沒有參與者失敗,走confirm邏輯,通過這樣的手段保證數(shù)據(jù)的最終一致性
- 2.業(yè)務(wù)系統(tǒng)有一臺(tái)宕機(jī)了,因?yàn)闋顟B(tài)已經(jīng)上傳到redis中,并且通過tcc服務(wù)來管理,并不會(huì)影響庫存的歸還,因此也能保證最終一致性