1. 程式人生 > 其它 >多庫多事務降低資料不一致概率

多庫多事務降低資料不一致概率

一、案例緣起

我們經常使用事務來保證資料庫層面資料的ACID特性。

舉個栗子,使用者下了一個訂單,需要修改餘額表,訂單表,流水錶,於是會有類似的虛擬碼:

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

如果對餘額表,訂單表,流水錶的SQL操作全部成功,則全部提交,如果任何一個出現問題,則全部回滾,以保證資料的一致性。

網際網路的業務特點,資料量較大,併發量較大,經常使用拆庫的方式提升系統的效能。如果進行了拆庫,餘額、訂單、流水可能分佈在不同的資料庫上,甚至不同的資料庫例項上,此時就不能用事務來保證資料的一致性了。這種情況下如何保證資料的一致性,是今天要討論的話題。

二、補償事務

補償事務是一種在業務端實施業務逆向操作事務,來保證業務資料一致性的方式。

舉個栗子,修改餘額表事務為

int Do_AccountT(uid, money){
start transaction;
         //餘額改變money這麼多
         CURDtable t_account with money;       anyException rollback return NO;
commit;
return YES;
}
那麼補償事務可以是:
int Compensate_AccountT(uid, money){
         //做一個money的反向操作
         returnDo_AccountT(uid, -1*money){
}
同理,訂單表操作為
Do_OrderT,新增一個訂單
Compensate_OrderT,刪除一個訂單
要保重餘額與訂單的一致性,可能要寫這樣的程式碼:
// 執行第一個事務
int flag = Do_AccountT();
if(flag=YES){
         //第一個事務成功,則執行第二個事務
         flag= Do_OrderT();
         if(flag=YES){
                  // 第二個事務成功,則成功
                   returnYES;
}
else{
         // 第二個事務失敗,執行第一個事務的補償事務
         Compensate_AccountT();
}
}

該方案的不足是:

(1)不同的業務要寫不同的補償事務,不具備通用性

(2)沒有考慮補償事務的失敗

(3)如果業務流程很複雜,if/else會巢狀非常多層

例如,如果上面的例子加上流水錶的修改,加上Do_FlowT和Compensate_FlowT,可能會變成一個這樣的if/else:

// 執行第一個事務
int flag = Do_AccountT();
if(flag=YES){
         //第一個事務成功,則執行第二個事務
         flag= Do_OrderT();
         if(flag=YES){
                  // 第二個事務成功,則執行第三個事務
                   flag= Do_FlowT();
                   if(flag=YES){
                            //第三個事務成功,則成功
                            returnYES;
}
else{
         // 第三個事務失敗,則執行第二、第一個事務的補償事務
         flag =Compensate_OrderT();
         if … else … // 補償事務執行失敗?
                  flag= Compensate_AccountT();
                   if … else … // 補償事務執行失敗?
}
}
else{
         // 第二個事務失敗,執行第一個事務的補償事務
         Compensate_AccountT();
         if … else … // 補償事務執行失敗?
}
}

三、事務拆分分析與後置提交優化

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

start transaction;
         CURDtable t_account;  any Exception rollback;
         CURDtable t_order;       any Exceptionrollback;
         CURDtable t_flow;         any Exceptionrollback;
commit;
拆分成了多個庫,大事務會變成三個小事務:
start transaction1;
         //第一個庫事務執行
         CURDtable t_account;  any Exception rollback;
         …
// 第一個庫事務提交
commit1;
start transaction2;
         //第二個庫事務執行
         CURDtable t_order;       any Exceptionrollback;
         …
// 第二個庫事務提交
commit2;
start transaction3;
         //第三個庫事務執行
         CURDtable t_flow;         any Exceptionrollback;
         …
// 第三個庫事務提交
commit3;

一個事務,分成執行與提交兩個階段,執行的時間其實是很長的,而commit的執行其實是很快的,於是整個執行過程的時間軸如下:

第一個事務執行200ms,提交1ms;

第二個事務執行120ms,提交1ms;

第三個事務執行80ms,提交1ms;

那在什麼時候系統出現問題,會出現不一致呢?

回答:第一個事務成功提交之後,最後一個事務成功提交之前,如果出現問題(例如伺服器重啟,資料庫異常等),都可能導致資料不一致。

如果改變事務執行與提交的時序,變成事務先執行,最後一起提交,情況會變成什麼樣呢:

第一個事務執行200ms;

第二個事務執行120ms;

第三個事務執行80ms;

第一個事務執行1ms;

第二個事務執行1ms;

第三個事務執行1ms;

那在什麼時候系統出現問題,會出現不一致呢?

問題的答案與之前相同:第一個事務成功提交之後,最後一個事務成功提交之前,如果出現問題(例如伺服器重啟,資料庫異常等),都可能導致資料不一致。

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

方案一總執行時間是303ms,最後202ms內出現異常都可能導致不一致;

方案二總執行時間也是303ms,但最後2ms內出現異常才會導致不一致;

雖然沒有徹底解決資料的一致性問題,但不一致出現的概率大大降低了!

事務提交後置降低了資料不一致的出現概率,會帶來什麼副作用呢?

回答:事務提交時會釋放資料庫的連線,第一種方案,第一個庫事務提交,資料庫連線就釋放了,後置事務提交的方案,所有庫的連線,要等到所有事務執行完才釋放。這就意味著,資料庫連線佔用的時間增長了,系統整體的吞吐量降低了。

四、總結

trx1.exec();
trx1.commit();
trx2.exec();
trx2.commit();
trx3.exec();
trx3.commit();

優化為:

trx1.exec();
trx2.exec();
trx3.exec();
trx1.commit();
trx2.commit();
trx3.commit();

這個小小的改動(改動成本極低),不能徹底解決多庫分散式事務資料一致性問題,但能大大降低資料不一致的概率,帶來的副作用是資料庫連線佔用時間會增長,吞吐量會降低。對於一致性與吞吐量的折衷,還需要業務架構師謹慎權衡折衷。