一、案例緣起
我們經常使用事務來保證數據庫層面數據的ACID特性。
舉個栗子,用戶下了一個訂單,需要修改余額表,訂單表,流水表,于是會有類似的偽代碼:
start transaction;
CURDtable t_account; any Exception rollback;
CURDtable t_order; any Exceptionrollback;
CURDtable t_flow; any Exceptionrollback;
commit;
如果對余額表,訂單表,流水表的SQL操作全部成功,則全部提交,如果任何一個出現問題,則全部回滾,以保證數據的一致性。
互聯網的業(yè)務特點,數據量較大,并發(fā)量較大,經常使用拆庫的方式提升系統的性能。
如果進行了拆庫,余額、訂單、流水可能分布在不同的數據庫上,甚至不同的數據庫實例上,此時就不能用事務來保證數據的一致性了。這種情況下如何保證數據的一致性,是今天要討論的話題。
二、補償事務
補償事務是一種在業(yè)務端實施業(yè)務逆向操作事務,來保證業(yè)務數據一致性的方式。
舉個栗子,修改余額表事務為
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,刪除一個訂單
要保重余額與訂單的一致性,可能要寫這樣的代碼:
// 執(zhí)行第一個事務
int flag = Do_AccountT();
if(flag=YES){
//第一個事務成功,則執(zhí)行第二個事務
flag= Do_OrderT();
if(flag=YES){
// 第二個事務成功,則成功
returnYES;
}
else{
// 第二個事務失敗,執(zhí)行第一個事務的補償事務
Compensate_AccountT();
}
}
該方案的不足是:
(1)不同的業(yè)務要寫不同的補償事務,不具備通用性
(2)沒有考慮補償事務的失敗
(3)如果業(yè)務流程很復雜,if/else會嵌套非常多層
例如,如果上面的例子加上流水表的修改,加上Do_FlowT和Compensate_FlowT,可能會變成一個這樣的if/else:
// 執(zhí)行第一個事務
int flag = Do_AccountT();
if(flag=YES){
//第一個事務成功,則執(zhí)行第二個事務
flag= Do_OrderT();
if(flag=YES){
// 第二個事務成功,則執(zhí)行第三個事務
flag= Do_FlowT();
if(flag=YES){
//第三個事務成功,則成功
returnYES;
}
else{
// 第三個事務失敗,則執(zhí)行第二、第一個事務的補償事務
flag =Compensate_OrderT();
if … else … // 補償事務執(zhí)行失???
flag= Compensate_AccountT();
if … else … // 補償事務執(zhí)行失???
}
}
else{
// 第二個事務失敗,執(zhí)行第一個事務的補償事務
Compensate_AccountT();
if … else … // 補償事務執(zhí)行失???
}
}
三、事務拆分分析與后置提交優(yōu)化
單庫是用這樣一個大事務保證一致性:
start transaction;
CURDtable t_account; any Exception rollback;
CURDtable t_order; any Exceptionrollback;
CURDtable t_flow; any Exceptionrollback;
commit;
拆分成了多個庫,大事務會變成三個小事務:
start transaction1;
//第一個庫事務執(zhí)行
CURDtable t_account; any Exception rollback;
…
// 第一個庫事務提交
commit1;
start transaction2;
//第二個庫事務執(zhí)行
CURDtable t_order; any Exceptionrollback;
…
// 第二個庫事務提交
commit2;
start transaction3;
//第三個庫事務執(zhí)行
CURDtable t_flow; any Exceptionrollback;
…
// 第三個庫事務提交
commit3;
一個事務,分成執(zhí)行與提交兩個階段,執(zhí)行的時間其實是很長的,而commit的執(zhí)行其實是很快的,于是整個執(zhí)行過程的時間軸如下:
第一個事務執(zhí)行200ms,提交1ms;
第二個事務執(zhí)行120ms,提交1ms;
第三個事務執(zhí)行80ms,提交1ms;
那在什么時候系統出現問題,會出現不一致呢?
回答:第一個事務成功提交之后,最后一個事務成功提交之前,如果出現問題(例如服務器重啟,數據庫異常等),都可能導致數據不一致。
如果改變事務執(zhí)行與提交的時序,變成事務先執(zhí)行,最后一起提交,情況會變成什么樣呢:
第一個事務執(zhí)行200ms;
第二個事務執(zhí)行120ms;
第三個事務執(zhí)行80ms;
第一個事務提交1ms;
第二個事務提交1ms;
第三個事務提交1ms;
那在什么時候系統出現問題,會出現不一致呢?
問題的答案與之前相同:第一個事務成功提交之后,最后一個事務成功提交之前,如果出現問題(例如服務器重啟,數據庫異常等),都可能導致數據不一致。
這個變化的意義是什么呢?
方案一總執(zhí)行時間是303ms,最后202ms內出現異常都可能導致不一致;
方案二總執(zhí)行時間也是303ms,但最后2ms內出現異常才會導致不一致;
雖然沒有徹底解決數據的一致性問題,但不一致出現的概率大大降低了!
事務提交后置降低了數據不一致的出現概率,會帶來什么副作用呢?
回答:事務提交時會釋放數據庫的連接,第一種方案,第一個庫事務提交,數據庫連接就釋放了,后置事務提交的方案,所有庫的連接,要等到所有事務執(zhí)行完才釋放。這就意味著,數據庫連接占用的時間增長了,系統整體的吞吐量降低了。
四、總結
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();
這個小小的改動(改動成本極低),不能徹底解決多庫分布式事務數據一致性問題,但能大大降低數據不一致的概率,帶來的副作用是數據庫連接占用時間會增長,吞吐量會降低。對于一致性與吞吐量的折衷,還需要業(yè)務架構師謹慎權衡折衷。
更多建議: