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