分布式系統(tǒng)的幾種一致性問題(下)

文接上篇累提。 在上篇里我們介紹了皱碘, 消息時(shí)序的一致性 和 冗余表的一致性财剖。 消息時(shí)序我們是利用LAMPORT時(shí)間戳來解決,允許大方向是消息有序小范圍允許亂序的思想寸谜。而且主要還是以服務(wù)器端的時(shí)間為準(zhǔn)竟稳。 在冗余表的一致性里,有2個(gè)重要的思想熊痴,一個(gè)是看先寫哪個(gè)失敗之后對(duì)業(yè)務(wù)影響更加小他爸,另外一個(gè)是看對(duì)服務(wù)的性能要求有多高,而決定是同步還是異步果善。最后我們看如何對(duì)增量數(shù)據(jù)維護(hù)一個(gè)額外的修復(fù)機(jī)制诊笤,是拉還是推。

這篇文章是最后2個(gè)問題巾陕,分布式事務(wù)的一致性和庫(kù)存扣減的一致性讨跟。

7. 分布式事務(wù)的一致性

一、案例緣起

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

舉個(gè)栗子晾匠,用戶下了一個(gè)訂單,需要修改余額表馆类,訂單表混聊,流水表弹谁,于是會(huì)有類似的偽代碼:

start transaction;

     CURDtable t_account;  any Exception rollback;

     CURDtable t_order;       any Exceptionrollback;

     CURDtable t_flow;         any Exceptionrollback;

commit;

如果對(duì)余額表乾巧,訂單表,流水表的SQL操作全部成功预愤,則全部提交沟于,如果任何一個(gè)出現(xiàn)問題,則全部回滾植康,以保證數(shù)據(jù)的一致性旷太。

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

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

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

舉個(gè)栗子屿脐,修改余額表事務(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){

     //做一個(gè)money的反向操作

     returnDo_AccountT(uid, -1*money){

}

同理,訂單表操作為

Do_OrderT宪卿,新增一個(gè)訂單

Compensate_OrderT摄悯,刪除一個(gè)訂單

要保重余額與訂單的一致性,可能要寫這樣的代碼:

// 執(zhí)行第一個(gè)事務(wù)

int flag = Do_AccountT();

if(flag=YES){

     //第一個(gè)事務(wù)成功愧捕,則執(zhí)行第二個(gè)事務(wù)

     flag= Do_OrderT();

     if(flag=YES){

              // 第二個(gè)事務(wù)成功奢驯,則成功

               returnYES;

}

else{

     // 第二個(gè)事務(wù)失敗,執(zhí)行第一個(gè)事務(wù)的補(bǔ)償事務(wù)

     Compensate_AccountT();

}

}

該方案的不足是:

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

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

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

例如,如果上面的例子加上流水表的修改邮偎,加上Do_FlowT和Compensate_FlowT管跺,可能會(huì)變成一個(gè)這樣的if/else:

// 執(zhí)行第一個(gè)事務(wù)

int flag = Do_AccountT();

if(flag=YES){

     //第一個(gè)事務(wù)成功,則執(zhí)行第二個(gè)事務(wù)

     flag= Do_OrderT();

     if(flag=YES){

              // 第二個(gè)事務(wù)成功禾进,則執(zhí)行第三個(gè)事務(wù)

               flag= Do_FlowT();

               if(flag=YES){

                        //第三個(gè)事務(wù)成功豁跑,則成功

                        returnYES;

}

else{

     // 第三個(gè)事務(wù)失敗,則執(zhí)行第二泻云、第一個(gè)事務(wù)的補(bǔ)償事務(wù)

     flag =Compensate_OrderT();

     if … else … // 補(bǔ)償事務(wù)執(zhí)行失斖摹?

              flag= Compensate_AccountT();

               if … else … // 補(bǔ)償事務(wù)執(zhí)行失敵璐俊卸夕?

}

}

else{

     // 第二個(gè)事務(wù)失敗,執(zhí)行第一個(gè)事務(wù)的補(bǔ)償事務(wù)

     Compensate_AccountT();

     if … else … // 補(bǔ)償事務(wù)執(zhí)行失斊殴稀快集?

}

}

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

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

start transaction;

     CURDtable t_account;  any Exception rollback;

     CURDtable t_order;       any Exceptionrollback;

     CURDtable t_flow;         any Exceptionrollback;

commit;

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

start transaction1;

     //第一個(gè)庫(kù)事務(wù)執(zhí)行

     CURDtable t_account;  any Exception rollback;

     …

// 第一個(gè)庫(kù)事務(wù)提交

commit1;

start transaction2;

     //第二個(gè)庫(kù)事務(wù)執(zhí)行

     CURDtable t_order;       any Exceptionrollback;

     …

// 第二個(gè)庫(kù)事務(wù)提交

commit2;

start transaction3;

     //第三個(gè)庫(kù)事務(wù)執(zhí)行

     CURDtable t_flow;         any Exceptionrollback;

     …

// 第三個(gè)庫(kù)事務(wù)提交

commit3;

一個(gè)事務(wù)个初,分成執(zhí)行與提交兩個(gè)階段乖寒,執(zhí)行的時(shí)間其實(shí)是很長(zhǎng)的,而commit的執(zhí)行其實(shí)是很快的院溺,于是整個(gè)執(zhí)行過程的時(shí)間軸如下:

image

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

第二個(gè)事務(wù)執(zhí)行120ms覆获,提交1ms马澈;

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

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

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

image

如果改變事務(wù)執(zhí)行與提交的時(shí)序,變成事務(wù)先執(zhí)行睦尽,最后一起提交器净,情況會(huì)變成什么樣呢:

image

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

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

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

第一個(gè)事務(wù)執(zhí)行1ms;

第二個(gè)事務(wù)執(zhí)行1ms沿量;

第三個(gè)事務(wù)執(zhí)行1ms浪慌;

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

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

image

這個(gè)變化的意義是什么呢芥被?

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

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

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

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

回答:事務(wù)提交時(shí)會(huì)釋放數(shù)據(jù)庫(kù)的連接顶捷,第一種方案挂绰,第一個(gè)庫(kù)事務(wù)提交,數(shù)據(jù)庫(kù)連接就釋放了服赎,后置事務(wù)提交的方案葵蒂,所有庫(kù)的連接,要等到所有事務(wù)執(zhí)行完才釋放重虑。這就意味著践付,數(shù)據(jù)庫(kù)連接占用的時(shí)間增長(zhǎng)了,系統(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();

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

我的思考和啟示

  1. 上述方法對(duì)現(xiàn)有事務(wù)的機(jī)制沒有改動(dòng)把失敗概率盡可能降低嗜价。
  2. 如果真的發(fā)生失敗落萎,需要補(bǔ)救機(jī)制。這時(shí)就會(huì)比較麻煩炭剪。
  3. 我想到的做法是练链,針對(duì)這3個(gè)改動(dòng)的事務(wù)每次COMMIT成功后打一條LOG。 如果有中間失敗奴拦,需要人去做手動(dòng)回滾了媒鼓。
  4. 如果可以的話,是否能引入二階段提交呢错妖?

8. 庫(kù)存一致性

image

業(yè)務(wù)復(fù)雜绿鸣、數(shù)據(jù)量大、并發(fā)量大的業(yè)務(wù)場(chǎng)景下暂氯,典型的互聯(lián)網(wǎng)架構(gòu)潮模,一般會(huì)分為這么幾層:

  • 調(diào)用層,一般是處于端上的browser或者APP

  • 站點(diǎn)層痴施,一般是拼裝html或者json返回的web-server層

  • 服務(wù)層擎厢,一般是提供RPC調(diào)用接口的service層

  • 數(shù)據(jù)層究流,提供固化數(shù)據(jù)存儲(chǔ)的db

對(duì)于庫(kù)存業(yè)務(wù),一般有個(gè)庫(kù)存服務(wù)动遭,提供庫(kù)存的查詢芬探、扣減、設(shè)置等RPC接口:

image
  • 庫(kù)存查詢厘惦,stock-service本質(zhì)上執(zhí)行的是

    select num from stock where sid=$sid

  • 庫(kù)存扣減偷仿,stock-service本質(zhì)上執(zhí)行的是

    update stock set num=num-reduce where sid=sid

  • 庫(kù)存設(shè)置,stock-service本質(zhì)上執(zhí)行的是

    update stock set num=num_new where sid=sid

用戶下單前宵蕉,一般會(huì)對(duì)庫(kù)存進(jìn)行查詢酝静,有足夠的存量才允許扣減:

image

如上圖所示,通過查詢接口羡玛,得到庫(kù)存是5别智。

用戶下單時(shí),接著會(huì)對(duì)庫(kù)存進(jìn)行扣減:

image

如上圖所示缝左,購(gòu)買3單位的商品亿遂,通過扣減接口,最終得到庫(kù)存是2渺杉。

希望設(shè)計(jì)往往有容錯(cuò)機(jī)制蛇数,例如“重試”,如果通過扣減接口來修改庫(kù)存是越,在重試時(shí)耳舅,可能會(huì)得到錯(cuò)誤的數(shù)據(jù),導(dǎo)致重復(fù)扣減:

image

如上圖所示倚评,如果數(shù)據(jù)庫(kù)層面有重試容錯(cuò)機(jī)制浦徊,可能導(dǎo)致一次扣減執(zhí)行兩次,最終得到一個(gè)負(fù)數(shù)的錯(cuò)誤庫(kù)存天梧。

重試導(dǎo)致錯(cuò)誤的根本原因盔性,是因?yàn)椤翱蹨p”操作是一個(gè)非冪等的操作,不能夠重復(fù)執(zhí)行呢岗,改成設(shè)置操作則不會(huì)有這個(gè)問題:

image

如上圖所示冕香,同樣是購(gòu)買3單位的商品,通過設(shè)置庫(kù)存操作后豫,即使有重試容錯(cuò)機(jī)制悉尾,也不會(huì)得到錯(cuò)誤的庫(kù)存,設(shè)置庫(kù)存是一個(gè)冪等操作挫酿。

在并發(fā)量很大的情況下构眯,還會(huì)有其他的問題:

image

如上圖所示,兩個(gè)并發(fā)的操作早龟,查詢庫(kù)存惫霸,都得到了庫(kù)存是5猫缭。

接下來用戶發(fā)生了并發(fā)的購(gòu)買動(dòng)作(秒殺類業(yè)務(wù)特別容易出現(xiàn)):

image

如上圖所示:

  • 用戶1購(gòu)買了3個(gè)庫(kù)存,于是庫(kù)存要設(shè)置為2

  • 用戶2購(gòu)買了2個(gè)庫(kù)存它褪,于是庫(kù)存要設(shè)置為3

  • 這兩個(gè)設(shè)置庫(kù)存的接口并發(fā)執(zhí)行饵骨,庫(kù)存會(huì)先變成2翘悉,再變成3茫打,導(dǎo)致數(shù)據(jù)不一致(實(shí)際賣出了5件商品,但庫(kù)存只扣減了2妖混,最后一次設(shè)置庫(kù)存會(huì)覆蓋和掩蓋前一次并發(fā)操作)

根本原因是老赤,設(shè)置操作發(fā)生的時(shí)候,沒有檢查庫(kù)存與查詢出來的庫(kù)存有沒有變化制市,理論上:

  • 庫(kù)存為5時(shí)抬旺,用戶1的庫(kù)存設(shè)置才能成功

  • 庫(kù)存為5時(shí),用戶2的庫(kù)存設(shè)置才能成功

實(shí)際執(zhí)行的時(shí)候:

  • 庫(kù)存為5祥楣,用戶1的set stock 2確實(shí)應(yīng)該成功

  • 庫(kù)存變?yōu)?了开财,用戶2的set stock 3應(yīng)該失敗掉

升級(jí)修改很容易,將庫(kù)存設(shè)置接口误褪,stock-service上執(zhí)行的:

update stock set num=y where sid=sid

升級(jí)為:

update stock set num=num_new where sid=sid and num=$num_old

這正是大家常說的“Compare And Set”(CAS)责鳍,是一種常見的降低讀寫鎖沖突,保證數(shù)據(jù)一致性的方法兽间。

總結(jié)

在業(yè)務(wù)復(fù)雜历葛,數(shù)據(jù)量大,并發(fā)量大的情況下嘀略,庫(kù)存扣減容易引發(fā)數(shù)據(jù)的不一致恤溶,常見的優(yōu)化方案有兩個(gè):

  • 調(diào)用“設(shè)置庫(kù)存”接口,能夠保證數(shù)據(jù)的冪等性

  • 在實(shí)現(xiàn)“設(shè)置庫(kù)存”接口時(shí)帜羊,需要加上原有庫(kù)存的比較咒程,才允許設(shè)置成功,能解決高并發(fā)下庫(kù)存扣減的一致性問題

我的思考和啟示

  1. 設(shè)置可以保證冪等性讼育,COMPARE AND SET 解決并發(fā)下的問題帐姻。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市窥淆,隨后出現(xiàn)的幾起案子卖宠,更是在濱河造成了極大的恐慌,老刑警劉巖忧饭,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扛伍,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡词裤,警方通過查閱死者的電腦和手機(jī)刺洒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門鳖宾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逆航,你說我怎么就攤上這事鼎文。” “怎么了因俐?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵拇惋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我抹剩,道長(zhǎng)撑帖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任澳眷,我火速辦了婚禮胡嘿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钳踊。我一直安慰自己衷敌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布拓瞪。 她就那樣靜靜地躺著缴罗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吴藻。 梳的紋絲不亂的頭發(fā)上瞒爬,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音沟堡,去河邊找鬼侧但。 笑死,一個(gè)胖子當(dāng)著我的面吹牛航罗,可吹牛的內(nèi)容都是我干的禀横。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼粥血,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼柏锄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起复亏,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤趾娃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后缔御,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抬闷,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了笤成。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片评架。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖炕泳,靈堂內(nèi)的尸體忽然破棺而出纵诞,到底是詐尸還是另有隱情,我是刑警寧澤培遵,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布浙芙,位于F島的核電站,受9級(jí)特大地震影響荤懂,放射性物質(zhì)發(fā)生泄漏茁裙。R本人自食惡果不足惜塘砸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一节仿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掉蔬,春花似錦廊宪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蛉迹,卻和暖如春傅寡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背北救。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工荐操, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人珍策。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓托启,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親攘宙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屯耸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容