分布式事务?No, 最终一致性
分布式一致性
一、寫在前面
現(xiàn)今互聯(lián)網(wǎng)界,分布式系統(tǒng)和微服務(wù)架構(gòu)盛行。
一個(gè)簡(jiǎn)單操作,在服務(wù)端非??赡苁怯啥鄠€(gè)服務(wù)和數(shù)據(jù)庫(kù)實(shí)例協(xié)同完成的。
在互聯(lián)網(wǎng)金融等一致性要求較高的場(chǎng)景下,多個(gè)獨(dú)立操作之間的一致性問題顯得格外棘手。
基于水平擴(kuò)容能力和成本考慮,傳統(tǒng)的強(qiáng)一致的解決方案(e.g.單機(jī)事務(wù))紛紛被拋棄。其理論依據(jù)就是響當(dāng)當(dāng)?shù)腃AP原理。
我們往往為了可用性和分區(qū)容錯(cuò)性,忍痛放棄強(qiáng)一致支持,轉(zhuǎn)而追求最終一致性。大部分業(yè)務(wù)場(chǎng)景下,我們是可以接受短暫的不一致的。
本文主要討論一些最終一致性相關(guān)的實(shí)現(xiàn)思路。
二、最終一致性解決方案
這個(gè)時(shí)候一般都會(huì)去舉一個(gè)例子:A給B轉(zhuǎn)100元。
當(dāng)然,A跟B很不幸的被分在了不同的數(shù)據(jù)庫(kù)實(shí)例上。甚者這兩個(gè)人可能是在不同機(jī)構(gòu)開的戶。
下面討論基本都是圍繞這個(gè)場(chǎng)景的。更復(fù)雜的場(chǎng)景需要各位客官發(fā)揮下超人的想象力和擴(kuò)展能力了。
談到最終一致性,人們首先想到的應(yīng)該是2PC解決方案。
1. 兩階段提交
兩階段提交需要有一個(gè)協(xié)調(diào)者,來協(xié)調(diào)兩個(gè)操作之間的操作流程。當(dāng)參與方為更多時(shí),其邏輯其實(shí)就比較復(fù)雜了。
而參與者需要實(shí)現(xiàn)兩階段提交協(xié)議。Pre commit階段需要鎖住相關(guān)資源,commit或rollback時(shí)分別進(jìn)行實(shí)際提交或釋放資源。
看似還不錯(cuò)。但是考慮到各種異常情況那就比較痛苦了。
舉個(gè)例子:如下圖,執(zhí)行到提交階段,調(diào)用A的commit接口超時(shí)了,協(xié)調(diào)者該如何做?
我們一般會(huì)假設(shè)預(yù)提交成功后,提交或回滾肯定是成功的(由參與者保障)。
上述情況協(xié)調(diào)者只能選擇繼續(xù)重試。這也就要求下游接口必須實(shí)現(xiàn)冪等(關(guān)于冪等的實(shí)現(xiàn)下面我們單獨(dú)再討論下)。
一般,下游出現(xiàn)故障,不是短時(shí)重試能解決的。所以,我們一般也需要有定時(shí)去處理中間狀態(tài)的邏輯。
這個(gè)地方,其實(shí)如果有個(gè)支持重試的MQ,可以扔到MQ。在實(shí)踐中,我們?cè)?jīng)也嘗試自己實(shí)現(xiàn)了一個(gè)基于MySQL的重試隊(duì)列。下面還會(huì)聊到這一點(diǎn)。
另外,我們也利用了一些外部重試機(jī)制。比如支付場(chǎng)景,微信和支付寶都有非常可靠的通知機(jī)制。
我們?cè)谕ㄖ幚斫涌谥凶鲆恍┲卦嚥呗?。如果重試失?#xff0c;就返回微信或支付寶失敗。
這樣第三方還會(huì)接著回調(diào)我們(懷疑他們可能發(fā)現(xiàn)了我廠回調(diào)成功率比其他商戶要低^_^),不過作為小廠,利用一些大廠成熟的機(jī)制還是可取的。
2. 異步確保(沒有事務(wù)消息)
“異步確?!边@個(gè)詞不一定是準(zhǔn)確的,還沒找到更合適的詞,抱歉。
異步化不只是為了一致性,有時(shí)候更多的考慮響應(yīng)時(shí)間,下游穩(wěn)定性等因素。本節(jié)只討論通過異步方案,如何實(shí)現(xiàn)最終一致性。
該方案關(guān)鍵是要有個(gè)消息表。另外,一般會(huì)有個(gè)隊(duì)列,而且我們一般都會(huì)假設(shè)這個(gè)MQ不丟消息。不過很不幸此MQ還不支持事務(wù)消息。
基本思路就是:
當(dāng)然如果進(jìn)一步簡(jiǎn)化,那么MQ也可以不要的。直接用一個(gè)腳本處理,一些低頻場(chǎng)景,也沒啥大問題。當(dāng)然離線掃表這個(gè)事情,總讓人不爽。業(yè)務(wù)量不大且也出初期相信很多人干活兒這事兒。
另外,對(duì)一致性要求不高的或者有其他兜底方案的場(chǎng)景(比如較為頻繁的對(duì)賬補(bǔ)賬機(jī)制),我們就不需要關(guān)心消息的confirm等情況,只要扔給消息,就認(rèn)為萬事大吉,一般也是可取的。
上面我們除了處理業(yè)務(wù)邏輯,還做了很多繁瑣的事情。把這些雜活兒都扔給一個(gè)中間件多好!這就是阿里等大廠做的事務(wù)消息中間件了(比如Notify,RockitMQ的事務(wù)消息,請(qǐng)看下節(jié))。
3. 異步確保(事務(wù)消息)
事務(wù)消息實(shí)際上是一個(gè)很理想的想法。
理想是:我們只要把消息扔到MQ,那么這個(gè)消息肯定會(huì)被消費(fèi)成功。生產(chǎn)方不用擔(dān)心消息發(fā)送失敗,也不用擔(dān)心消息會(huì)丟失。
回到現(xiàn)實(shí),消費(fèi)方,如果消息處理失敗了,還有機(jī)會(huì)繼續(xù)消費(fèi),直到成功為止(消費(fèi)方邏輯bug導(dǎo)致消費(fèi)失敗情況不在本文討論范圍內(nèi))。
但遺憾的是市面上大部分MQ都不支持事務(wù)消息,其中包括看起來可以一統(tǒng)江湖的kafka。
RocketMQ號(hào)稱支持,但是還沒開源。阿里云據(jù)說免費(fèi)提供,沒玩過(羨慕下阿里等大廠內(nèi)部猿類們)。不過從網(wǎng)上公開的資料看,用起來還是有些不爽的地方。這是后話了,畢竟解決了很多問題。
事務(wù)消息,關(guān)鍵一點(diǎn)是把上小節(jié)中繁瑣的消息狀態(tài)和重發(fā)等用中間件形式封裝了。
我廠目前還沒提供成熟的支持事務(wù)消息的MQ。下面以網(wǎng)傳RMQ為例,說明事務(wù)消息大概是怎么玩的:
RMQ的事務(wù)消息相對(duì)于普通MQ,相當(dāng)于提供了2PC的提交接口。
生產(chǎn)方需要先發(fā)送一個(gè)prepared消息給RMQ。如果操作1失敗,返回失敗。
然后執(zhí)行本地事務(wù),如果成功了需要發(fā)送Confirm消息給RMQ。2失敗,則調(diào)用RMQ cancel接口。
那問題是3失敗了(或者超時(shí))該如何處理呢?
別急,RMQ考慮到這個(gè)問題了。 RMQ會(huì)要求你實(shí)現(xiàn)一個(gè)check的接口。生產(chǎn)方需要實(shí)現(xiàn)該接口,并告知RMQ自己本地事務(wù)是否執(zhí)行成功(第4步)。RMQ會(huì)定時(shí)輪訓(xùn)所有處于pre狀態(tài)的消息,并調(diào)用對(duì)應(yīng)的check接口,以決定此消息是否可以提交。
當(dāng)然第5步也可能會(huì)失敗。這時(shí)候需要RMQ支持消息重試。處理失敗的消息果斷時(shí)間再進(jìn)行重試,直到成功為止(超過重試次數(shù)后會(huì)進(jìn)死信隊(duì)列,可能得人肉處理了,因?yàn)闆]用過所以細(xì)節(jié)不是很了解)。
支持消息重試,這一點(diǎn)也很重要。消息重試機(jī)制也不僅僅在這里能用到,還有其他一些特殊的場(chǎng)景,我們會(huì)依賴他。下一小節(jié),我們簡(jiǎn)單探討一下這個(gè)問題。
RMQ還是很強(qiáng)大的。我們認(rèn)為這個(gè)程度的一致性已經(jīng)能夠滿足絕大部分互聯(lián)網(wǎng)應(yīng)用場(chǎng)景。代價(jià)是生產(chǎn)方做了不少額外的事情,但相比沒有事務(wù)消息情況,確實(shí)解放了不少勞動(dòng)力。
P.S. 據(jù)說阿里內(nèi)部因?yàn)闅v史原因,用notify比RMQ要多,他們倆基本原理類似。
4. 補(bǔ)償交易(Compensating Transaction)
補(bǔ)償交易,其核心思想是:針對(duì)每個(gè)操作,都要注冊(cè)一個(gè)與其對(duì)應(yīng)的補(bǔ)償操作。一般來說操作本身和其補(bǔ)償(撤銷)操作會(huì)在一個(gè)事務(wù)里完成。
當(dāng)其后續(xù)操作失敗后,需要按相反順序完成前面注冊(cè)的所有撤銷操作。
跟2PC比,他的核心價(jià)值應(yīng)該是少了鎖資源的代價(jià)。流程也相對(duì)簡(jiǎn)單一點(diǎn)。但實(shí)際操作中,補(bǔ)償操作不太好定義,其中間狀態(tài)處理也會(huì)比較棘手。
比如A:-100(補(bǔ)償為A:+100), B:+100。那么如果B:+100失敗后就需要執(zhí)行A:+100。
曾經(jīng)有位大牛同事(也是我灰常崇拜的一位技術(shù)控)一直熱衷于這個(gè)思路,相信有些場(chǎng)景用補(bǔ)償交易模式也是個(gè)不錯(cuò)的選擇。
他更多是不斷思考如何讓補(bǔ)償看起來跟注冊(cè)個(gè)單庫(kù)事務(wù)一樣簡(jiǎn)單。做到業(yè)務(wù)無感知。
因?yàn)楸救藳]有相關(guān)實(shí)戰(zhàn)經(jīng)驗(yàn),所以留個(gè)鏈接在這里,供大家擴(kuò)展閱讀。偷懶了,截個(gè)此文中的一張圖。
5. 消息重試
上面多次提到消息重試。如果說事務(wù)消息重點(diǎn)解決了生產(chǎn)者和MQ之間的一致性問題,那么重試機(jī)制對(duì)于確保消費(fèi)者和MQ之間的一致性是至關(guān)重要的。
重試可以是pull模式,也可以是push模式。我廠目前已經(jīng)提供push模式的消息重試,這個(gè)還是要贊一下的!
消息重試,重試顧名思義是要解決消息一次性傳遞過程中的失敗場(chǎng)景。舉個(gè)例子,支付寶回調(diào)商戶,然后商戶系統(tǒng)掛了,怎么辦?答案是重試!
一般來說,消息如果消費(fèi)失敗,就會(huì)被放到重試隊(duì)列。如果是延遲時(shí)間固定(比如每次延遲2s),那么只需要按失敗的順序進(jìn)隊(duì)列就好了,然后對(duì)隊(duì)首的消息,只有當(dāng)延遲時(shí)間到達(dá)才能被消費(fèi)。
這里會(huì)有個(gè)水位的概念。如果按時(shí)間作為水位,那么期望執(zhí)行時(shí)間大于當(dāng)前時(shí)間的消息才是高于水位以上的。其他消息對(duì)consumer不可見。
如果要實(shí)現(xiàn)每個(gè)消息延遲時(shí)間不一樣,之前想過一種基于隊(duì)列的方案是,按秒的維度建多個(gè)隊(duì)列。按執(zhí)行時(shí)間入到不同的隊(duì)列,一天86400個(gè)隊(duì)列(一般丑陋)。然后cosumer按時(shí)間消費(fèi)不同隊(duì)列。
當(dāng)然如果不依賴隊(duì)列可以有更靈活的方案。
之前做支付時(shí)候,做了個(gè)基于DB的延時(shí)隊(duì)列。每次消息進(jìn)去時(shí)候,都會(huì)把下次執(zhí)行時(shí)間設(shè)置一下。再對(duì)這個(gè)時(shí)間做個(gè)索引....
略土,but it works。畢竟失敗的消息不該很多,所以DB容量也不用太在意。很多時(shí)候,能跑起來的,簡(jiǎn)單的架構(gòu)會(huì)得到更多人喜愛。
我廠提供了一種基于redis的延時(shí)隊(duì)列,可以支持消息重試。用到的主要數(shù)據(jù)結(jié)構(gòu)是redis的zset,按消息處理時(shí)間排序。
當(dāng)然實(shí)現(xiàn)起來也沒說的那么簡(jiǎn)單。MQ遇到的持久化問題,內(nèi)存數(shù)據(jù)丟失問題,重試次數(shù)控制,消息追溯等等都需要有一些額外的開發(fā)量。
綜上,如果MQ能夠提供消息重試特性,那就不要自己折騰了。這里還是有不少坑的。
6. 冪等(接口支持重入)
即使沒有MQ,重試也是無處不在的。所以冪等問題不是因?yàn)橛玫組Q后引入的,而是老問題。
冪等怎么做?
如果是單條insert操作,我們一般會(huì)依賴唯一鍵。如果一個(gè)事務(wù)里包含一個(gè)單條insert,那也可以依賴這條insert做冪等,當(dāng)insert拋異常就回滾事務(wù)。
如果是update操作,那么狀態(tài)機(jī)控制和版本控制異常重要。這里要多加小心。
再?gòu)?fù)雜點(diǎn)的,可以考慮引入一個(gè)log表。該log對(duì)操作id(消息id?)進(jìn)行唯一鍵控制。然后整個(gè)操作用事務(wù)控制。當(dāng)插入log失敗時(shí)整個(gè)事務(wù)回滾就好了。
有人會(huì)說先查log表或者利用redis等緩存,加鎖。我想說的是這個(gè)基本上都不work。除非在事務(wù)里進(jìn)行查尋。所以建議,所幸讓代碼簡(jiǎn)單點(diǎn),直接插入,依賴數(shù)據(jù)庫(kù)唯一鍵沖突回滾掉就好了。
用唯一鍵擋重入是目前為止個(gè)人覺得最有安全感的方式。當(dāng)然對(duì)數(shù)據(jù)庫(kù)會(huì)有一些額外性能損耗。問題就變成了有多大的并發(fā),其中又有多大是需要重試的?
我相信Fasion IO卡+分庫(kù)分表之后,想達(dá)到數(shù)據(jù)庫(kù)性能瓶頸還是有點(diǎn)難度的(主要是針對(duì)金融類場(chǎng)景)。
三、后記
本文略虛,當(dāng)然目前最終一致性沒有一個(gè)放之四海而皆準(zhǔn)的成功實(shí)踐。需要大家根據(jù)不同的業(yè)務(wù)特性和發(fā)展階段,選則適當(dāng)?shù)姆绞絹韺?shí)現(xiàn)。
糾結(jié)最終一致性問題,其實(shí)萬惡之源是因?yàn)镽PC本身會(huì)失敗,會(huì)有結(jié)果不確定的情況。
隱約感覺本人職業(yè)生涯大部分時(shí)間都會(huì)跟各種失敗和timeout搏斗了。
本文重點(diǎn)討論利用MQ實(shí)現(xiàn)最終一致性。主要原因有:
1. 目前市面上的MQ都相對(duì)非常強(qiáng)大,幾乎都號(hào)稱可以做到不丟數(shù)據(jù)。相信未來對(duì)事務(wù)消息應(yīng)該也會(huì)更加普及。
2. 異步化幾乎是不同處理能力(響應(yīng)時(shí)間、吞吐量)和穩(wěn)定性(99.99%的服務(wù)依賴99.9%的服務(wù))的服務(wù)之間解耦的畢竟之路。
當(dāng)然前面的討論還很淺顯。能力有限,希望能夠不斷完善此文,請(qǐng)各位看到的客觀不吝賜教。
下一篇,希望能夠跟大家share一下,最近在做的一個(gè)項(xiàng)目。其主要目的利用現(xiàn)有還未支持事務(wù)消息的MQ,在業(yè)務(wù)層實(shí)現(xiàn)類事務(wù)消息邏輯,并且盡量不讓代碼變成一坨。
本人在知乎處女文,會(huì)有人看到嗎?
from:https://zhuanlan.zhihu.com/p/25933039
總結(jié)
以上是生活随笔為你收集整理的分布式事务?No, 最终一致性的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring 事务机制详解
- 下一篇: 分布式系统的事务处理