618
技術社區[雲棲]
淺談分布式事務
現今互聯網界,分布式係統和微服務架構盛行。一個簡單操作,在服務端非常可能是由多個服務和數據庫實例協同完成的。在一致性要求較高的場景下,多個獨立操作之間的一致性問題顯得格外棘手。
基於水平擴容能力和成本考慮,傳統的強一致的解決方案(e.g.單機事務)紛紛被拋棄。其理論依據就是響當當的CAP原理。往往為了可用性和分區容錯性,忍痛放棄強一致支持,轉而追求最終一致性。
分布式係統的特性
在分布式係統中,同時滿足CAP定律中的一致性 Consistency、可用性 Availability和分區容錯性 Partition Tolerance三者是不可能的。在絕大多數的場景,都需要犧牲強一致性來換取係統的高可用性,係統往往隻需要保證最終一致性。
CAP理解:
Consistency:強一致性就是在客戶端任何時候看到各節點的數據都是一致的(All nodes see the same data at the same time)。
Availability:高可用性就是在任何時候都可以讀寫(Reads and writes always succeed)。
Partition Tolerance:分區容錯性是在網絡故障、某些節點不能通信的時候係統仍能繼續工作(The system continue to operate despite arbitrary message loss or failure of part of the the system)。以實際效果而言,分區相當於對通信的時限要求。係統如果不能在時限內達成數據一致性,就意味著發生了分區的情況,必須就當前操作在C和A之間做出選擇。
ACID理解:
Atomicity 原子性:一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
Consistency 一致性:在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞。
Isolation 隔離性:數據庫允許多個並發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致。
Durability 持久性:事務處理結束後,對數據的修改就是永久的,即便係統故障也不會丟失。
分布式事務的基本介紹
分布式事務服務(Distributed Transaction Service,DTS)是一個分布式事務框架,用來保障在大規模分布式環境下事務的最終一致性。
CAP理論告訴我們在分布式存儲係統中,最多隻能實現上麵的兩點。而由於當前的網絡硬件肯定會出現延遲丟包等問題,所以分區容忍性是我們必須需要實現的,所以我們隻能在一致性和可用性之間進行權衡。
為了保障係統的可用性,互聯網係統大多將強一致性需求轉換成最終一致性的需求,並通過係統執行冪等性的保證,保證數據的最終一致性。
數據一致性理解:
強一致性:當更新操作完成之後,任何多個後續進程或者線程的訪問都會返回最新的更新過的值。這種是對用戶最友好的,就是用戶上一次寫什麼,下一次就保證能讀到什麼。根據 CAP 理論,這種實現需要犧牲可用性。
弱一致性:係統並不保證後續進程或者線程的訪問都會返回最新的更新過的值。係統在數據寫入成功之後,不承諾立即可以讀到最新寫入的值,也不會具體的承諾多久之後可以讀到。
最終一致性:弱一致性的特定形式。係統保證在沒有後續更新的前提下,係統最終返回上一次更新操作的值。在沒有故障發生的前提下,不一致窗口的時間主要受通信延遲,係統負載和複製副本的個數影響。DNS 是一個典型的最終一致性係統。
常用的分布式技術說明
- 本地消息表
這種實現方式的思路是源於ebay,其基本的設計思想是將遠程分布式事務拆分成一係列的本地事務。
舉個經典的跨行轉賬的例子來描述。
第一步偽代碼如下,扣款1W,通過本地事務保證了憑證消息插入到消息表中。
第二步,通知對方銀行賬戶上加1W了,通常采用兩種方式:
采用時效性高的MQ,由對方訂閱消息並監聽,有消息時自動觸發事件。
采用定時輪詢掃描的方式,去檢查消息表的數據。
- 消息中間件
非事務性的消息中間件
還是以上述提到的跨行轉賬為例,我們很難保證在扣款完成之後對MQ投遞消息的操作就一定能成功。這樣一致性似乎很難保證。
try {
bool result = dao.update(model); // 操作數據庫失敗,會拋出異常
if (result) {
mq.send(model); // 如果mq方式執行失敗,會拋出異常
}
} catch(Exception e) {
rollback(); // 如果發生異常,則回滾
}
我們來分析下可能的情況:
操作數據庫成功,向MQ中投遞消息也成功,皆大歡喜。
操作數據庫失敗,不會向MQ中投遞消息了。
操作數據庫成功,但是向MQ中投遞消息時失敗,向外拋出了異常,剛剛執行的更新數據庫的操作將被回滾。
從上麵分析的幾種情況來看,基本上能保證發送者發送消息的可靠性。我們再來分析下消費者端麵臨的問題:
消息出列後,消費者對應的業務操作要執行成功。如果業務執行失敗,消息不能失效或者丟失。需要保證消息與業務操作一致。
盡量避免消息重複消費。如果重複消費,也不能因此影響業務結果。
支持事務的消息中間件
除了上麵介紹的通過異常捕獲和回滾的方式外,還有沒有其他的思路呢?
阿裏巴巴的RocketMQ中間件就支持一種事務消息機製,能夠確保本地操作和發送消息達到本地事務一樣的效果。
第一階段,RocketMQ在執行本地事務之前,會先發送一個Prepared消息,並且會持有這個消息的地址。
第二階段,執行本地事物操作。
第三階段,確認消息發送,通過第一階段拿到的地址去訪問消息,並修改狀態,如果本地事務成功,則修改狀態為已提交,否則修改狀態為已回滾。
但是如果第三階段的確認消息發送失敗了怎麼辦?RocketMQ會定期掃描消息集群中的事物消息,如果發現了prepare狀態的消息,它會向消息發送者確認本地事務是否已執行成功,如果成功是回滾還是繼續發送確認消息呢。RocketMQ會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。
目前主流的開源MQ(ActiveMQ、RabbitMQ、Kafka)均未實現對事務消息的支持,比較遺憾的是,RocketMQ事務消息部分的代碼也並未開源,需要自己去實現。
理解2PC和3PC協議
為了解決分布式一致性問題,前人在性能和數據一致性的反反複複權衡過程中總結了許多典型的協議和算法。其中比較著名的有二階提交協議(2 Phase Commitment Protocol),三階提交協議(3 Phase Commitment Protocol)。
2PC
分布式事務最常用的解決方案就是二階段提交。在分布式係統中,每個節點雖然可以知曉自己的操作時成功或者失敗,卻無法知道其他節點的操作的成功或失敗。當一個事務跨越多個節點時,為了保持事務的ACID特性,需要引入一個作為協調者的組件來統一掌控所有參與者節點的操作結果並最終指示這些節點是否要把操作結果進行真正的提交。
因此,二階段提交的算法思路可以概括為:參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作。
所謂的兩個階段是指:第一階段:準備階段(投票階段)和第二階段:提交階段(執行階段)。
第一階段:投票階段
該階段的主要目的在於打探數據庫集群中的各個參與者是否能夠正常的執行事務,具體步驟如下:
1. 協調者向所有的參與者發送事務執行請求,並等待參與者反饋事務執行結果。
事務參與者收到請求之後,執行事務,但不提交,並記錄事務日誌。
參與者將自己事務執行情況反饋給協調者,同時阻塞等待協調者的後續指令。
第二階段:事務提交階段
在第一階段協調者的詢盤之後,各個參與者會回複自己事務的執行情況,這時候存在三種可能:
1. 所有的參與者回複能夠正常執行事務。
一個或多個參與者回複事務執行失敗。
協調者等待超時。
對於第一種情況,協調者將向所有的參與者發出提交事務的通知,具體步驟如下:
- 協調者向各個參與者發送commit通知,請求提交事務。
參與者收到事務提交通知之後,執行commit操作,然後釋放占有的資源。
參與者向協調者返回事務commit結果信息。
對於第二、三種情況,協調者均認為參與者無法正常成功執行事務,為了整個集群數據的一致性,所以要向各個參與者發送事務回滾通知,具體步驟如下:
協調者向各個參與者發送事務rollback通知,請求回滾事務。
參與者收到事務回滾通知之後,執行rollback操作,然後釋放占有的資源。
參與者向協調者返回事務rollback結果信息。
兩階段提交協議解決的是分布式數據庫數據強一致性問題,其原理簡單,易於實現,但是缺點也是顯而易見的,主要缺點如下:
單點問題:協調者在整個兩階段提交過程中扮演著舉足輕重的作用,一旦協調者所在服務器宕機,那麼就會影響整個數據庫集群的正常運行,比如在第二階段中,如果協調者因為故障不能正常發送事務提交或回滾通知,那麼參與者們將一直處於阻塞狀態,整個數據庫集群將無法提供服務。
同步阻塞:兩階段提交執行過程中,所有的參與者都需要聽從協調者的統一調度,期間處於阻塞狀態而不能從事其他操作,這樣效率及其低下。
數據不一致性:兩階段提交協議雖然為分布式數據強一致性所設計,但仍然存在數據不一致性的可能,比如在第二階段中,假設協調者發出了事務commit的通知,但是因為網絡問題該通知僅被一部分參與者所收到並執行了commit操作,其餘的參與者則因為沒有收到通知一直處於阻塞狀態,這時候就產生了數據的不一致性。
3PC
針對兩階段提交存在的問題,三階段提交協議通過引入一個“預詢盤”階段,以及超時策略來減少整個集群的阻塞時間,提升係統性能。三階段提交的三個階段分別為:can_commit,pre_commit,do_commit。
第一階段:can_commit
該階段協調者會去詢問各個參與者是否能夠正常執行事務,參與者根據自身情況回複一個預估值,相對於真正的執行事務,這個過程是輕量的,具體步驟如下:
1. 協調者向各個參與者發送事務詢問通知,詢問是否可以執行事務操作,並等待回複。
- 各個參與者依據自身狀況回複一個預估值,如果預估自己能夠正常執行事務就返回確定信息,並進入預備狀態,否則返回否定信息。
第二階段:pre_commit
本階段協調者會根據第一階段的詢盤結果采取相應操作,詢盤結果主要有三種:
1. 所有的參與者都返回確定信息。
2. 一個或多個參與者返回否定信息。
- 協調者等待超時。
針對第一種情況,協調者會向所有參與者發送事務執行請求,具體步驟如下:
- 協調者向所有的事務參與者發送事務執行通知。
參與者收到通知後,執行事務,但不提交。
參與者將事務執行情況返回給客戶端。
在上麵的步驟中,如果參與者等待超時,則會中斷事務。 針對第二、三種情況,協調者認為事務無法正常執行,於是向各個參與者發出abort通知,請求退出預備狀態,具體步驟如下:
協調者向所有事務參與者發送abort通知
參與者收到通知後,中斷事務
第三階段:do_commit
如果第二階段事務未中斷,那麼本階段協調者將會依據事務執行返回的結果來決定提交或回滾事務,分為三種情況:
1. 所有的參與者都能正常執行事務。
2. 一個或多個參與者執行事務失敗。
- 協調者等待超時。
針對第一種情況,協調者向各個參與者發起事務提交請求,具體步驟如下:
針對第二、三種情況,協調者認為事務無法正常執行,於是向各個參與者發送事務回滾請求,具體步驟如下:
- 協調者向所有參與者發送事務rollback通知。
所有參與者在收到通知之後執行rollback操作,並釋放占有的資源。
參與者向協調者反饋事務提交結果。
在本階段如果因為協調者或網絡問題,導致參與者遲遲不能收到來自協調者的commit或rollback請求,那麼參與者將不會如兩階段提交中那樣陷入阻塞,而是等待超時後繼續commit。相對於兩階段提交雖然降低了同步阻塞,但仍然無法避免數據的不一致性。
本文來源 linkedkeeper.com (文/張鬆然)”
最後更新:2017-09-13 18:02:44