多庫多事務(wù)降低數(shù)據(jù)不一致概率

2020-11-26 16:28 更新

一、案例緣起

我們經(jīng)常使用事務(wù)來保證數(shù)據(jù)庫層面數(shù)據(jù)的ACID特性。

舉個栗子,用戶下了一個訂單,需要修改余額表,訂單表,流水表,于是會有類似的偽代碼:

start transaction;
         CURDtable t_account;  any Exception rollback;
         CURDtable t_order;       any Exceptionrollback;
         CURDtable t_flow;         any Exceptionrollback;

commit;

如果對余額表,訂單表,流水表的SQL操作全部成功,則全部提交,如果任何一個出現(xiàn)問題,則全部回滾,以保證數(shù)據(jù)的一致性。


互聯(lián)網(wǎng)的業(yè)務(wù)特點,數(shù)據(jù)量較大,并發(fā)量較大,經(jīng)常使用拆庫的方式提升系統(tǒng)的性能。如果進(jìn)行了拆庫,余額、訂單、流水可能分布在不同的數(shù)據(jù)庫上,甚至不同的數(shù)據(jù)庫實例上,此時就不能用事務(wù)來保證數(shù)據(jù)的一致性了。這種情況下如何保證數(shù)據(jù)的一致性,是今天要討論的話題。

二、補(bǔ)償事務(wù)

補(bǔ)償事務(wù)是一種在業(yè)務(wù)端實施業(yè)務(wù)逆向操作事務(wù),來保證業(yè)務(wù)數(shù)據(jù)一致性的方式。

舉個栗子,修改余額表事務(wù)為

int Do_AccountT(uid, money){
start transaction;
         //余額改變money這么多
         CURDtable t_account with money;       anyException rollback return NO;
commit;
return YES;
}

那么補(bǔ)償事務(wù)可以是:
int Compensate_AccountT(uid, money){
         //做一個money的反向操作
         returnDo_AccountT(uid, -1*money){
}

同理,訂單表操作為

Do_OrderT,新增一個訂單

Compensate_OrderT,刪除一個訂單


要保重余額與訂單的一致性,可能要寫這樣的代碼:
// 執(zhí)行第一個事務(wù)
int flag = Do_AccountT();
if(flag=YES){
         //第一個事務(wù)成功,則執(zhí)行第二個事務(wù)
         flag= Do_OrderT();
         if(flag=YES){
                  // 第二個事務(wù)成功,則成功
                   returnYES;
  }
  else{
         // 第二個事務(wù)失敗,執(zhí)行第一個事務(wù)的補(bǔ)償事務(wù)
         Compensate_AccountT();
  }
}

該方案的不足是:

(1)不同的業(yè)務(wù)要寫不同的補(bǔ)償事務(wù),不具備通用性

(2)沒有考慮補(bǔ)償事務(wù)的失敗

(3)如果業(yè)務(wù)流程很復(fù)雜,if/else會嵌套非常多層


例如,如果上面的例子加上流水表的修改,加上Do_FlowT和Compensate_FlowT,可能會變成一個這樣的if/else:
// 執(zhí)行第一個事務(wù)
int flag = Do_AccountT();
if(flag=YES){
         //第一個事務(wù)成功,則執(zhí)行第二個事務(wù)
         flag= Do_OrderT();
         if(flag=YES){
                  // 第二個事務(wù)成功,則執(zhí)行第三個事務(wù)
                   flag= Do_FlowT();
                   if(flag=YES){
                            //第三個事務(wù)成功,則成功
                            returnYES;
            }
            else{
                   // 第三個事務(wù)失敗,則執(zhí)行第二、第一個事務(wù)的補(bǔ)償事務(wù)
                    flag =Compensate_OrderT();
                    if … else … // 補(bǔ)償事務(wù)執(zhí)行失敗?
                      flag= Compensate_AccountT();
                       if … else … // 補(bǔ)償事務(wù)執(zhí)行失???
            }
   }
   else{
          // 第二個事務(wù)失敗,執(zhí)行第一個事務(wù)的補(bǔ)償事務(wù)
          Compensate_AccountT();
          if … else … // 補(bǔ)償事務(wù)執(zhí)行失???
   }
}

三、事務(wù)拆分分析與后置提交優(yōu)化

單庫是用這樣一個大事務(wù)保證一致性:

start transaction;
         CURDtable t_account;  any Exception rollback;
         CURDtable t_order;       any Exceptionrollback;
         CURDtable t_flow;         any Exceptionrollback;

commit;

拆分成了多個庫,大事務(wù)會變成三個小事務(wù):

start transaction1;
         //第一個庫事務(wù)執(zhí)行
         CURDtable t_account;  any Exception rollback;
         …
// 第一個庫事務(wù)提交
commit1;
start transaction2;
         //第二個庫事務(wù)執(zhí)行
         CURDtable t_order;       any Exceptionrollback;
         …
// 第二個庫事務(wù)提交
commit2;
start transaction3;
         //第三個庫事務(wù)執(zhí)行
         CURDtable t_flow;         any Exceptionrollback;
         …
// 第三個庫事務(wù)提交
commit3;

一個事務(wù),分成執(zhí)行與提交兩個階段,執(zhí)行的時間其實是很長的,而commit的執(zhí)行其實是很快的,于是整個執(zhí)行過程的時間軸如下:
3個事務(wù)順序執(zhí)行時間軸

第一個事務(wù)執(zhí)行200ms,提交1ms;

第二個事務(wù)執(zhí)行120ms,提交1ms;

第三個事務(wù)執(zhí)行80ms,提交1ms;

那在什么時候系統(tǒng)出現(xiàn)問題,會出現(xiàn)不一致呢?

回答第一個事務(wù)成功提交之后,最后一個事務(wù)成功提交之前,如果出現(xiàn)問題(例如服務(wù)器重啟,數(shù)據(jù)庫異常等),都可能導(dǎo)致數(shù)據(jù)不一致。

3個事務(wù)執(zhí)行出現(xiàn)異常
如果改變事務(wù)執(zhí)行與提交的時序,變成事務(wù)先執(zhí)行,最后一起提交,情況會變成什么樣呢:
3個事務(wù)執(zhí)行一起提交

第一個事務(wù)執(zhí)行200ms;

第二個事務(wù)執(zhí)行120ms;

第三個事務(wù)執(zhí)行80ms;

第一個事務(wù)提交1ms;

第二個事務(wù)提交1ms;

第三個事務(wù)提交1ms;


那在什么時候系統(tǒng)出現(xiàn)問題,會出現(xiàn)不一致呢?

問題的答案與之前相同:第一個事務(wù)成功提交之后,最后一個事務(wù)成功提交之前,如果出現(xiàn)問題(例如服務(wù)器重啟,數(shù)據(jù)庫異常等),都可能導(dǎo)致數(shù)據(jù)不一致。

3個事務(wù)執(zhí)行出現(xiàn)異常2

這個變化的意義是什么呢?

方案一總執(zhí)行時間是303ms,最后202ms內(nèi)出現(xiàn)異常都可能導(dǎo)致不一致;

方案二總執(zhí)行時間也是303ms,但最后2ms內(nèi)出現(xiàn)異常才會導(dǎo)致不一致;

雖然沒有徹底解決數(shù)據(jù)的一致性問題,但不一致出現(xiàn)的概率大大降低了!


事務(wù)提交后置降低了數(shù)據(jù)不一致的出現(xiàn)概率,會帶來什么副作用呢?

回答:事務(wù)提交時會釋放數(shù)據(jù)庫的連接,第一種方案,第一個庫事務(wù)提交,數(shù)據(jù)庫連接就釋放了,后置事務(wù)提交的方案,所有庫的連接,要等到所有事務(wù)執(zhí)行完才釋放。這就意味著,數(shù)據(jù)庫連接占用的時間增長了,系統(tǒng)整體的吞吐量降低了


四、總結(jié)

trx1.exec();
trx1.commit();
trx2.exec();
trx2.commit();
trx3.exec();
trx3.commit();
優(yōu)化為:
trx1.exec();
trx2.exec();
trx3.exec();
trx1.commit();
trx2.commit();

trx3.commit();

這個小小的改動(改動成本極低),不能徹底解決多庫分布式事務(wù)數(shù)據(jù)一致性問題,但能大大降低數(shù)據(jù)不一致的概率,帶來的副作用是數(shù)據(jù)庫連接占用時間會增長,吞吐量會降低。對于一致性與吞吐量的折衷,還需要業(yè)務(wù)架構(gòu)師謹(jǐn)慎權(quán)衡折衷。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號