最近在研究分布式數(shù)據(jù)庫相關(guān)的技術(shù)焰宣,對于數(shù)據(jù)庫來說匕积,不管是單機數(shù)據(jù)庫還是分布式數(shù)據(jù)庫闸天,事務(wù)都是一個繞不去的坎苞氮。不光是數(shù)據(jù)庫,對于微服務(wù)架構(gòu)贷帮,不同服務(wù)之間也會涉及到分布式事務(wù)的處理撵枢。本文先介紹事務(wù)的基本概念和原理锄禽,然后介紹單機事務(wù)的實現(xiàn)方案沃但,最后介紹分布式事務(wù)的實現(xiàn)方案宵晚。
學(xué)習事務(wù)的過程中參考了不少文章,這些文章比本文更有價值钝凶,結(jié)尾有這些文章的地址耕陷。
什么是事務(wù)
描述原理和實現(xiàn)之前哟沫,我們首先需要了解究竟什么是事務(wù)嗜诀。就像開會发皿,先劃定議題穴墅,介紹背景知識玄货,才能更高效的討論松捉。維基百科中對事務(wù)的描述如下:
數(shù)據(jù)庫事務(wù)通常包含了一個序列的對數(shù)據(jù)庫的讀/寫操作。包含有以下兩個目的:
為數(shù)據(jù)庫操作序列提供了一個從失敗中恢復(fù)到正常狀態(tài)的方法以舒,同時提供了數(shù)據(jù)庫即使在異常狀態(tài)下仍能保持一致性的方法蔓钟。
當多個應(yīng)用程序在并發(fā)訪問數(shù)據(jù)庫時滥沫,可以在這些應(yīng)用程序之間提供一個隔離方法,以防止彼此的操作互相干擾缀辩。
當事務(wù)被提交給了數(shù)據(jù)庫管理系統(tǒng)(DBMS)臀玄,則DBMS需要確保該事務(wù)中的所有操作都成功完成且其結(jié)果被永久保存在數(shù)據(jù)庫中健无,如果事務(wù)中有的操作沒有成功完成叠穆,則事務(wù)中的所有操作都需要回滾硼被,回到事務(wù)執(zhí)行前的狀態(tài)祷嘶;同時,該事務(wù)對數(shù)據(jù)庫或者其他事務(wù)的執(zhí)行無影響,所有的事務(wù)都好像在獨立的運行状勤。
從上面的描述中可以看出持搜,事務(wù)有幾個重要的特性葫盼,一是原子性,要么全部成功孩灯,要么全部回滾峰档,二是一致性掀亩,即使在異常狀態(tài)也能保持數(shù)據(jù)的一致归榕,三是隔離性刹泄,一個事務(wù)的執(zhí)行不會影響其他事務(wù)。要實現(xiàn)這幾個特性姆蘸,是數(shù)據(jù)庫事務(wù)的主要難點逞敷。其實準確地說,是有四個特性牛柒,也就是常說的ACID皮壁。維基百科對ACID的描述如下:
Atomicity(原子性):一個事務(wù)(Transaction)中的所有操作,或者全部完成畏腕,或者全部不完成描馅,不會結(jié)束在中間某個環(huán)節(jié)恋日。事務(wù)在執(zhí)行過程中發(fā)生錯誤岂膳,會被回滾(Rollback)到事務(wù)開始前的狀態(tài),就像這個事務(wù)從來沒有執(zhí)行過一樣簸喂。即喻鳄,事務(wù)不可分割、不可約簡颜曾。
Consistency(一致性):在事務(wù)開始之前和事務(wù)結(jié)束以后泛豪,數(shù)據(jù)庫的完整性沒有被破壞吕粹。這表示寫入的資料必須完全符合所有的預(yù)設(shè)約束匹耕、觸發(fā)器驶赏、級聯(lián)回滾等煤傍。
Isolation(隔離性):數(shù)據(jù)庫允許多個并發(fā)事務(wù)同時對其數(shù)據(jù)進行讀寫和修改的能力五续,隔離性可以防止多個事務(wù)并發(fā)執(zhí)行時由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致疙驾。事務(wù)隔離分為不同級別,包括未提交讀(Read Uncommitted)扳肛、提交讀(Read Committed)、可重復(fù)讀(Repeatable Read)和串行化(Serializable)旋讹。
Durability(持久性):事務(wù)處理結(jié)束后轿衔,對數(shù)據(jù)的修改就是永久的沉迹,即便系統(tǒng)故障也不會丟失。
注意傳統(tǒng)數(shù)據(jù)庫的一致性是指數(shù)據(jù)庫本身的完整性害驹,主要是指數(shù)據(jù)庫的約束條件和觸發(fā)器等沒有被破壞鞭呕,比如設(shè)置約束某個字段值不能小于0宛官,事務(wù)會保證始終符合這個check約束條件葫松。數(shù)據(jù)庫中的一致性和CAP中的一致性不是一個概念。CAP中的一致性是指分布式系統(tǒng)中不同副本上的數(shù)據(jù)是一致的底洗。到了分布式事務(wù)中腋么,和傳統(tǒng)數(shù)據(jù)庫相比,對于一致性的定義就不太一樣了亥揖,分布式系統(tǒng)可能沒有預(yù)設(shè)約束和觸發(fā)器等功能珊擂,尤其是針對微服務(wù)的分布式事務(wù)。對于分布式事務(wù)费变,一致性指整體數(shù)據(jù)的完整性摧扇,在事務(wù)執(zhí)行前后,系統(tǒng)的數(shù)據(jù)是完整的挚歧。比如A給B轉(zhuǎn)賬100塊錢扛稽,在事務(wù)結(jié)束后,A和B的賬戶總和應(yīng)該跟事務(wù)前的總和是一樣的滑负。
單機事務(wù)
傳統(tǒng)單機情況下庇绽,數(shù)據(jù)庫是怎么實現(xiàn)原子性和隔離性的呢锡搜?以MySQL數(shù)據(jù)庫為例,原子性通過undo日志實現(xiàn)瞧掺,持久性通過redo日志實現(xiàn)耕餐,隔離性通過鎖、MVCC(Multi Version Concurrency Control)和undo日志實現(xiàn)辟狈。下面分別做介紹肠缔。
redo日志
介紹redo日志之前,需要先簡單說一下MySQL的數(shù)據(jù)管理方式哼转。MySQL中的數(shù)據(jù)并不是每次都是從磁盤中讀取的明未,而是在內(nèi)存中有一個緩存Buffer Pool,用于存放磁盤中的熱點數(shù)據(jù)頁壹蔓。讀取數(shù)據(jù)時先從Buffer Pool中讀取趟妥,如果沒有的話再從磁盤中讀取,然后保存在Buffer Pool中佣蓉。寫入數(shù)據(jù)時也是先寫到Buffer Pool披摄,然后再把Buffer Pool中的數(shù)據(jù)定期寫入磁盤。
雖然Buffer Pool提高了MySQL效率勇凭,但是會導(dǎo)致一個問題疚膊,如果在寫入磁盤前,MySQL宕機了虾标,Buffer Pool中的還沒寫盤的數(shù)據(jù)就丟失了寓盗,所以MySQL設(shè)計了redo日志來解決這個問題。
redo日志由內(nèi)存中的redo log buffer和redo日志文件組成璧函。修改數(shù)據(jù)時傀蚌,先寫redo日志添加到內(nèi)存中的redo log buffer,然后修改Buffer Pool中的數(shù)據(jù)蘸吓。提交這次事務(wù)時善炫,可以選擇是否將redo log buffer中的日志刷新到磁盤盯漂。用戶可以通過innodb_flush_log_at_trx_commit參數(shù)來控制寫盤時機棠笑,有三種取值:
- 0,事務(wù)提交時不寫盤,由線程每秒寫盤一次制跟。
- 1,事務(wù)提交時調(diào)用fsync強制寫盤酱虎。
- 2雨膨,事務(wù)提交時寫入文件系統(tǒng)緩存,由操作系統(tǒng)決定何時將緩存寫入磁盤读串。
如果設(shè)置為0聊记,MySQL服務(wù)器進程宕機時有可能丟失數(shù)據(jù)撒妈;如果設(shè)置為2,操作系統(tǒng)宕機時有可能丟失數(shù)據(jù)排监。
redo日志并不一定是提交才會寫盤狰右,如果innodb_flush_log_at_trx_commit設(shè)置為0,即使還沒提交舆床,也可能寫盤棋蚌。
如果每次修改數(shù)據(jù)都需要寫redo日志到磁盤,那為什么不把Buffer Pool中的數(shù)據(jù)直接寫磁盤呢挨队?原因主要有兩個:
- 直接刷新數(shù)據(jù)是一個隨機IO谷暮,每次修改的數(shù)據(jù)在不同的數(shù)據(jù)頁中,而redo日志是連續(xù)的盛垦,寫盤是順序IO湿弦。
- 直接刷新數(shù)據(jù)是以數(shù)據(jù)頁為單位,MySQL默認是16KB腾夯,即使修改的數(shù)據(jù)只有一個字節(jié)也需要寫16KB颊埃。而redo日志只包含修改的數(shù)據(jù),數(shù)據(jù)量要少很多俯在。
MySQL中redo日志以塊(block)為單位存儲竟秫,每塊的大小為512B,格式如下:
每個block由12字節(jié)頭部log block header跷乐,492字節(jié)日志內(nèi)容log block和8字節(jié)尾部log block tailer組成肥败。
log block日志內(nèi)容中保存是具體redo日志,格式如下:
- redo_log_type: redo日志類型
- space: 表空間ID
- page_no: 頁偏移量
- redo log body: 根據(jù)日志類型的不同愕提,存儲的內(nèi)容格式也不一樣
在描述數(shù)據(jù)恢復(fù)過程之前馒稍,還需要介紹一下MySQL中有個Log Sequence Number(LSN),8個字節(jié)浅侨,是一個遞增的值纽谒,表示當前redo日志總共有多少個字節(jié)。LSN保存在redo日志文件和每個數(shù)據(jù)頁的頭部如输。寫redo文件時鼓黔,MySQL會把當前的LSN一起寫入文件,然后修改內(nèi)存當前數(shù)據(jù)頁的LSN不见。等到數(shù)據(jù)頁寫盤時澳化,LSN也會一起保存在該數(shù)據(jù)頁對應(yīng)的磁盤中,而且當前LSN值會寫入數(shù)據(jù)文件ibdata的第一個page中作為整個數(shù)據(jù)文件的checkpoint稳吮。
MySQL啟動時缎谷,首先檢查當前redo日志文件中的LSN和數(shù)據(jù)文件中的checkpoint對應(yīng)的LSN,如果兩個一樣灶似,說明沒有數(shù)據(jù)丟失列林。如果checkpoint LSN小于redo LSN瑞你,說明有數(shù)據(jù)丟失,從checkpoint LSN開始希痴,遍歷每個redo日志者甲,找到對應(yīng)的數(shù)據(jù)頁,如果數(shù)據(jù)頁的LSN小于redo日志中的LSN砌创,需要對這一頁進行數(shù)據(jù)恢復(fù)过牙。理論上redo日志中所有在checkpoint之后的事務(wù)都需要恢復(fù),為什么這里還要比較每一頁的LSN纺铭?這是因為MySQL刷臟頁時寇钉,是先把所有臟頁寫入磁盤,最后再寫入checkpoint LSN舶赔。有可能臟頁已經(jīng)寫入磁盤扫倡,但是在寫入checkpoint LSN前宕機,這就需要在恢復(fù)事務(wù)時判斷數(shù)據(jù)頁中的LSN竟纳,避免重復(fù)恢復(fù)撵溃。這里只介紹了redo日志在數(shù)據(jù)恢復(fù)時的使用,實際上還要結(jié)合binlog一起使用锥累,這就更復(fù)雜了缘挑,不詳細展開。
undo日志
undo日志用于事務(wù)的回滾和MVCC桶略,分別對應(yīng)原子性和隔離性语淘。MySQL中修改數(shù)據(jù)時,并不是簡單地在當前數(shù)據(jù)上修改际歼,而是先把修改前的數(shù)據(jù)保存在undo log中惶翻,然后修改當前數(shù)據(jù)并且在當前數(shù)據(jù)中增加一個指針指向修改前的數(shù)據(jù)。如下圖所示鹅心,undo日志組成了一個鏈表:
圖中undo列表由三個SQL操作組成吕粗,左上角為當前記錄的內(nèi)容,第二個方塊是最后一條SQL語句對應(yīng)的undo日志旭愧,日志中保存了事務(wù)ID(TRX_ID)和修改前字段的內(nèi)容("B")颅筋,最后一個方塊是insert語句對應(yīng)的undo日志。
根據(jù)不同的操作類型输枯,undo日志的格式不一樣议泵,下面以update操作為例介紹對應(yīng)的undo日志格式。
- next: 2B用押,表示下一條undo日志位置
- type_cmpl: 1B肢簿,表示undo日志類型
- undo_no: 序號靶剑,用來區(qū)分一個事務(wù)中多個undo日志的順序
- table_id: 表ID
- info_bits: 一些標記位
- DATA_TRX_ID: 這次修改對應(yīng)的事務(wù)ID
- DATA_ROLL_PTR: 回滾指針蜻拨,記錄當前數(shù)據(jù)上一個版本在回滾段中的位置
- update vector: 表示修改的數(shù)據(jù)
- start: 表示上一條undo日志位置
除了上面介紹的字段池充,undo日志還有一個undo header頭部信息,其中一個字段是TRX_UNDO_STATE缎讼,表示undo日志的狀態(tài)收夸。取值有下面幾個:
- TRX_UNDO_ACTIVE: 初使狀態(tài)
- TRX_UNDO_CACHED:
- TRX_UNDO_TO_FREE: 可以釋放
- TRX_UNDO_TO_PURGE: 可以清理
- TRX_UNDO_PREPARED: 準備狀態(tài),還未提交
undo日志在事務(wù)未提交前是TRX_UNDO_PREPARED狀態(tài)血崭,事務(wù)提交后卧惜,根據(jù)不同的操作類型轉(zhuǎn)換成TRX_UNDO_CACHED,TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE狀態(tài)夹纫,表示滿足一定條件后可以釋放咽瓷,事務(wù)如果需要回滾的話,必須是TRX_UNDO_ACTIVE或者TRX_UNDO_PREPARED狀態(tài)舰讹。此時從undo日志中取出上一次的數(shù)據(jù)作為當前數(shù)據(jù)的值茅姜。需要說明的是寫undo日志本身也會產(chǎn)生相應(yīng)的redo日志。
MVCC
MVCC的全稱是Multi Version Concurrency Control多版本并發(fā)控制月匣。它的作用是解決不同事務(wù)之間并發(fā)執(zhí)行的時候钻洒,數(shù)據(jù)修改的隔離性問題。數(shù)據(jù)庫有四種隔離級別锄开,由低到高如下:
- 未提交讀(READ UNCOMMITED):允許讀取其他事務(wù)未提交的修改
- 已提交讀(READ COMMITED):只能讀取其他事務(wù)已提交的修改
- 可重復(fù)讀(REPEATABLE READ):同一個事務(wù)內(nèi)素标,多次讀取操作得到的每個數(shù)據(jù)行的內(nèi)容是一樣的
- 可串行化(SERIALIZABLE):事務(wù)執(zhí)行不受其他事務(wù)的影響,就像各個事務(wù)之間是按順序執(zhí)行的
這里解釋一下可串行化萍悴⊥吩猓可串行化是指多個事務(wù)執(zhí)行是按照某種順序執(zhí)行的,每個事務(wù)都是一個原子操作癣诱,一個事務(wù)執(zhí)行過程中不會看到另一個事務(wù)的中間狀態(tài)任岸,但不保證這個順序一定是時間上的先后順序。比如事務(wù)ABC先后請求狡刘,實際執(zhí)行時可能是ACB的順序享潜。可串行化和線性一致性(Linearizable)不是一個概念嗅蔬。線性一致性是指對于同一個對象剑按,操作的執(zhí)行順序是和時間順序一致的,一個操作在時間順序上發(fā)生在前面澜术,那后面的操作一定可以看到前面操作的結(jié)果艺蝴。把可串行化和線性一致性結(jié)合起來,就是嚴格可串行化(Strict Serializable)鸟废,既滿足可串行化猜敢,也滿足線性一致性,是最高的一致性模型。
不同的隔離級別可以解決不同級別的讀問題缩擂,如下:
隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
未提交讀 | 可能發(fā)生 | 可能發(fā)生 | 可能發(fā)生 |
提交讀 | - | 可能發(fā)生 | 可能發(fā)生 |
可重復(fù)讀 | - | - | 可能發(fā)生 |
可序列化 | - | - | - |
臟讀鼠冕、不可重復(fù)讀、幻讀的解釋如下:
臟讀
當一個事務(wù)允許讀取另外一個事務(wù)修改但未提交的數(shù)據(jù)時胯盯,就可能發(fā)生臟讀懈费。
舉個例子:
事務(wù) 1 | 事務(wù) 2 |
---|---|
/* Query 1 */ SELECT age FROM users WHERE id = 1; /* will read 20 */ |
|
/* Query 2 */ UPDATE users SET age = 21 WHERE id = 1; /* No commit here */ |
|
/* Query 1 */ SELECT age FROM users WHERE id = 1; /* will read 21 */ |
|
ROLLBACK; /* lock-based DIRTY READ */ |
不可重復(fù)讀
在一次事務(wù)中,當一行數(shù)據(jù)獲取兩遍得到不同的結(jié)果表示發(fā)生了“不可重復(fù)讀”.
事務(wù) 1 | 事務(wù) 2 |
---|---|
/* Query 1 */ SELECT * FROM users WHERE id = 1; |
|
/* Query 2 */ UPDATE users SET age = 21 WHERE id = 1; COMMIT; /* in multiversion concurrency control, or lock-based READ COMMITTED */ |
|
/* Query 1 */ SELECT * FROM users WHERE id = 1; COMMIT; /* lock-based REPEATABLE READ */ |
幻讀
在事務(wù)執(zhí)行過程中博脑,當兩個完全相同的查詢語句執(zhí)行得到不同的結(jié)果集憎乙。這種現(xiàn)象稱為“幻讀(phantom read)”
事務(wù) 1 | 事務(wù) 2 |
---|---|
/* Query 1 */ SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
|
/* Query 2 */ INSERT INTO users VALUES ( 3, 'Bob', 27 ); COMMIT; |
|
/* Query 1 */ SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
對于隔離性,一種方法是基于鎖叉趣。這種方案的問題是性能可能比較低泞边,尤其是讀操作也要加鎖的時候。另一種方案是MVCC疗杉。MVCC用于替代讀鎖繁堡,寫依舊需要加鎖。MVCC和undo日志是如何支持不同的隔離級別乡数,解決讀問題的呢椭蹄?
對于未提交讀,只要讀取記錄當前版本的值就行了净赴。
對于已提交讀绳矩,在執(zhí)行事務(wù)中每個查詢語句的時候,MySQL會生成一個叫做視圖read view的數(shù)據(jù)結(jié)構(gòu)玖翅,包含以下內(nèi)容:
- m_ids: 所有正在執(zhí)行的事務(wù)ID翼馆,這些事務(wù)還未提交
- min_trx_id: 生成read view時正在執(zhí)行的最小事務(wù)ID
- max_trx_id: 生成read view時系統(tǒng)應(yīng)該分配的下一個事務(wù)ID
- creator_trx_id: 生成read view時事務(wù)本身的ID
訪問數(shù)據(jù)時,根據(jù)以下規(guī)則判斷某個版本的數(shù)據(jù)是否可見:
- 如果當前版本數(shù)據(jù)的事務(wù)ID和creator_trx_id相同金度,說明是當前事務(wù)修改的記錄应媚,此時該版本數(shù)據(jù)可見。
- 如果當前版本數(shù)據(jù)的事務(wù)ID小于min_trx_id猜极,說明是已經(jīng)提交的事務(wù)作的修改中姜,該版本數(shù)據(jù)可見。
- 如果當前版本數(shù)據(jù)的事務(wù)ID大于等于max_trx_id跟伏,說明是該版本的事務(wù)是在read view創(chuàng)建之后生成的丢胚,該版本數(shù)據(jù)不可見
- 如果當前版本數(shù)據(jù)的事務(wù)ID在min_trx_id和max_trx_id之間,并且在m_ids內(nèi)受扳,該版本數(shù)據(jù)不可見携龟,如果不在m_ids內(nèi),該版本數(shù)據(jù)可見勘高。
如果當前版本數(shù)據(jù)不可見峡蟋,使用前面介紹的undo日志坟桅,根據(jù)ROLL PTR回滾指針找到上一個版本的數(shù)據(jù),判斷上一個版本的數(shù)據(jù)是否可見蕊蝗,如果不可見仅乓,沿著undo日志鏈表找到符合條件的數(shù)據(jù)版本。
對于可重復(fù)讀匿又,和已提交讀的差別在于已提交讀是在事務(wù)中每條查詢語句執(zhí)行的時候生成read view,而可重復(fù)讀是在事務(wù)一開始的時候就生成read view建蹄。
MVCC解決了臟讀碌更、不可重復(fù)讀問題,以及部分幻讀問題洞慎。為什么說是部分痛单?對于前面幻讀舉的例子,事務(wù)1的兩條select都是讀的同一版本的數(shù)據(jù)劲腿,因為事務(wù)2插入的數(shù)據(jù)版本號不符合事務(wù)1的讀取范圍旭绒,所以不會讀到,這種情況的幻讀MVCC可以處理焦人。但是另一種幻讀則處理不了挥吵,這涉及到快照讀和當前讀的概念』ㄍ郑快照讀就是前面介紹的使用undo日志來選擇一個合適的版本來讀取忽匈,select操作使用這種方式。而insert矿辽、update和delete則使用當前讀丹允,對于這幾個修改操作,必須使用最新的數(shù)據(jù)進行修改袋倔。舉個例子:
事務(wù) 1 | 事務(wù) 2 |
---|---|
/* Query 1 */ SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
|
/* Query 2 */ INSERT INTO users VALUES ( 3, 'Bob', 27 ); COMMIT; |
|
/* Query 1 */ UPDATE users SET name = 'Tom' where id = 3; |
|
/* Query 1 */ SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
事務(wù)1查詢讀到兩條數(shù)據(jù)雕蔽,事務(wù)2插入id為3的記錄后,事務(wù)1執(zhí)行更新操作宾娜,使用當前讀獲取的最新數(shù)據(jù)批狐,將id為3的記錄名字改成了Tom,然后事務(wù)1執(zhí)行第二次查詢前塔,這時是可以讀到id為3的數(shù)據(jù)的贾陷,兩次select讀到的數(shù)據(jù)不一樣。因為事務(wù)1進行update操作嘱根,數(shù)據(jù)的版本號是事務(wù)1自己的事務(wù)ID髓废,所以第二次select能讀到id為3的記錄。是不是很復(fù)雜该抒?
事務(wù)流程
介紹完了redo日志慌洪,undo日志后顶燕,我們完整描述一下事務(wù)流程。
準備階段:
- 分配事務(wù)ID
- 如果隔離級別是REPEATABLE READ冈爹,創(chuàng)建read view
- 分配undo日志涌攻,把修改之前的數(shù)據(jù)寫入undo日志
- 為undo日志的修改創(chuàng)建redo日志,寫入redo log buffer
- 在buffer pool中修改數(shù)據(jù)頁频伤,回滾指針指向undo日志
- 為數(shù)據(jù)頁的修改創(chuàng)建redo日志恳谎,寫入redo log buffer
提交階段:
- 寫binlog到磁盤
- 修改undo日志狀態(tài)為"purge"
- 根據(jù)innodb_flush_log_at_trx_commit判斷是否需要立即把redo log buffer寫入磁盤
- buffer pool中的數(shù)據(jù)頁根據(jù)規(guī)則等待合適的時機寫入磁盤
恢復(fù)或者回滾階段:
- 根據(jù)redo日志中的LSN和事務(wù)ID,數(shù)據(jù)文件中的LSN和binlog中的事務(wù)ID決定需要做恢復(fù)還是回滾
- 如果事務(wù)已提交憋肖,但數(shù)據(jù)頁還未寫入磁盤因痛,需要恢復(fù),根據(jù)redo日志中的字段對數(shù)據(jù)頁進行恢復(fù)
- 如果需要回滾岸更,根據(jù)undo日志中的上一個版本數(shù)據(jù)進行回滾
分布式事務(wù)協(xié)議
分布式事務(wù)有兩種形式鸵膏,一種是分布式數(shù)據(jù)庫事務(wù),另一種分布式微服務(wù)事務(wù)怎炊。
第一種分布式數(shù)據(jù)庫事務(wù)由傳統(tǒng)單機事務(wù)進化而來谭企。當單機數(shù)據(jù)庫無法支撐所有負載時,必然要將數(shù)據(jù)拆分到多臺數(shù)據(jù)庫评肆。如果一次操作涉及到多臺數(shù)據(jù)庫债查,那就需要一種方案來維護整個數(shù)據(jù)庫集群的事務(wù)特性,也就是ACID特性瓜挽。
第二種分布式微服務(wù)事務(wù)由分布在不同機器上的服務(wù)組成攀操。最近微服務(wù)架構(gòu)興起,不同的服務(wù)被拆分到不同的服務(wù)器進程中秸抚。比如一個網(wǎng)絡(luò)購物服務(wù)速和,由訂單服務(wù)器,支付服務(wù)器剥汤,倉儲服務(wù)器和物流服務(wù)器組成颠放。每個服務(wù)器使用的數(shù)據(jù)存儲方案可能不同,有的用MySQL吭敢,有的用Redis碰凶。如何讓網(wǎng)絡(luò)購物服務(wù)完整執(zhí)行,而不會導(dǎo)致支付了但倉庫中沒有剩余庫存鹿驼,這是分布式服務(wù)事務(wù)需要處理的問題之一欲低。
分布式事務(wù)和單機事務(wù)相比,處理的問題更為困難畜晰。
第一個問題是因為有多個事務(wù)參與者砾莱。傳統(tǒng)單機事務(wù),如果宕機凄鼻,整個事務(wù)的執(zhí)行都會失敗腊瑟,原子性比較好處理聚假。但是分布式事務(wù),參與者分布在不同的機器上闰非,可能第一個參與者成功鎖住資源膘格,而第二個參與者對資源加鎖失敗,也可能第一個參與者提交事務(wù)成功财松,但是第二個參與者提交時機器宕機瘪贱,處理起來困難。
第二個問題是網(wǎng)絡(luò)超時導(dǎo)致的問題辆毡。服務(wù)器A告訴服務(wù)器B提交事務(wù)菜秦,然后超時了,沒有收到服務(wù)器B的響應(yīng)胚迫。這時有可能服務(wù)器B沒收到請求喷户,也可能服務(wù)器B收到請求唾那,成功處理访锻,發(fā)送給A的響應(yīng)丟失。如何區(qū)分這兩種情況然后進行處理也是困難的地方闹获。
第三個問題是距離導(dǎo)致的延遲問題期犬。傳統(tǒng)單機事務(wù)不存在網(wǎng)絡(luò)延遲,所有操作都在一臺機器上處理避诽。但是分布式事務(wù)由于服務(wù)器的物理距離帶來了網(wǎng)絡(luò)延時龟虎,這可能會導(dǎo)致實現(xiàn)方案的差異。比如前面介紹中我們提到過事務(wù)ID的概念沙庐,在分布式系統(tǒng)中鲤妥,如何保證事務(wù)ID的唯一性,以及區(qū)分并發(fā)事務(wù)之間的先后順序拱雏?一種方案是提供一個全局的服務(wù)來分配事務(wù)ID棉安,單調(diào)遞增。這種方案在同城部署的時候還好铸抑,因為同城的網(wǎng)絡(luò)往返RTT大概在1ms內(nèi)贡耽,但是如果是全球部署的系統(tǒng),比如Google Spanner鹊汛,一個服務(wù)器在北美洲蒲赂,另一個服務(wù)器在歐洲,跨洲往返RTT可能是200ms刁憋,這個延遲顯然是無法接受的滥嘴,所以這種全局事務(wù)ID服務(wù)器方案不行。
那么分布式事務(wù)應(yīng)該如何實現(xiàn)至耻,接下來我們討論原理和常見的幾種方案氏涩。
兩階段提交
兩階段提交中有兩個角色届囚,一個是參與者,用于管理本地資源是尖,實現(xiàn)本地事務(wù)意系。另一個是協(xié)調(diào)者,用于管理分布式事務(wù)饺汹,協(xié)調(diào)事務(wù)各個參與者之間的操作蛔添。
流程如下:
Coordinator Participant
prepare*
QUERY TO COMMIT
-------------------------------->
VOTE YES/NO prepare*/abort*
<-------------------------------
commit*/abort*
COMMIT/ROLLBACK
-------------------------------->
ACKNOWLEDGMENT commit*/abort*
<--------------------------------
end
An * next to the record type means that the record is forced to stable storage.
第一階段Prepare
協(xié)調(diào)者分配事務(wù)ID,寫到磁盤兜辞,然后詢問所有參與者是否可以執(zhí)行事務(wù)
參與者執(zhí)行事務(wù)迎瞧,對資源加鎖,寫redo/undo日志到磁盤
如果參與者執(zhí)行事務(wù)成功逸吵,回復(fù)Yes凶硅,如果執(zhí)行失敗,回復(fù)No
第二階段Commit/Abort
分兩種情況
所有參與者回復(fù)Yes扫皱,執(zhí)行Commit
協(xié)調(diào)者寫Commit日志到磁盤足绅,然后向所有參與者發(fā)送Commit請求
參與者執(zhí)行Commit操作,寫Commit日志到磁盤韩脑,釋放資源
參與者回復(fù)協(xié)調(diào)者ACK完成消息
協(xié)調(diào)者收到所有參與者完成消息后氢妈,完成事務(wù)
至少有一個參與者回復(fù)No,執(zhí)行Abort
協(xié)調(diào)者寫Abort日志到磁盤段多,然后向所有參與者發(fā)送Abort請求
參與者使用Undo日志回滾事務(wù)首量,寫Abort日志到磁盤,釋放資源
參與者回復(fù)協(xié)調(diào)者回滾完成消息
協(xié)調(diào)者收到所有參與者回滾完成消息后进苍,取消事務(wù)
整個兩階段流程加缘,看著挺簡單的,但是麻煩的地方在于如何處理服務(wù)器宕機和網(wǎng)絡(luò)超時問題觉啊,我們來分析一下整個過程如何處理這兩個問題拣宏。首先有個原則是如果協(xié)調(diào)者已經(jīng)寫Commit日志到磁盤,各個參與者就應(yīng)該提交事務(wù)柄延,不允許回滾蚀浆。
網(wǎng)絡(luò)超時問題
協(xié)調(diào)者發(fā)送完P(guān)repare請求后,在規(guī)定時間內(nèi)沒有收到所有參與者的回復(fù)搜吧。此時簡單的做法是協(xié)調(diào)者發(fā)送Abort請求給所有參與者市俊,參與者執(zhí)行回滾操作。因為可能所有的參與者都Prepare成功滤奈,只是協(xié)調(diào)者沒有收到回復(fù)消息摆昧,這種做法選擇了正確性,犧牲了性能蜒程。
-
參與者等待協(xié)調(diào)者的Commit請求超時绅你。
- 如果該參與者Prepare階段回復(fù)的是No伺帘,此時可以直接Abort事務(wù)。因為協(xié)調(diào)者收到No回復(fù)后忌锯,給所有參與者發(fā)送的也是Abort請求伪嫁。
- 如果該參與者Prepare階段回復(fù)的是Yes就比較麻煩了。因為不知道協(xié)調(diào)者發(fā)送的是Commit還是Abort偶垮,該參與者不能直接執(zhí)行Commit或者Abort张咳。此時該參與者有兩種做法。
- 方案一:向協(xié)調(diào)者查詢事務(wù)狀態(tài)似舵。這種做法可能作用不大脚猾,因為很可能協(xié)調(diào)者宕機或者協(xié)調(diào)者到該參與者之間的網(wǎng)絡(luò)不通,這時候查詢也起不到什么作用砚哗。
- 方案二:執(zhí)行終止協(xié)議(Termination Protocol)龙助,超時的參與者向其他參與者查詢事務(wù)狀態(tài)。
如果有參與者回復(fù)的是No蛛芥,所有參與者執(zhí)行Abort操作提鸟。
如果有參與者回復(fù)說還沒有收到Prepare請求,所有參與者執(zhí)行Abort操作常空。
-
如果所有參與者回復(fù)的是Yes沽一,此時參與者可以執(zhí)行Commit操作嗎盖溺?不能漓糙,原因有兩個。
- 如果之后協(xié)調(diào)者的Commit請求又被參與者收到了烘嘱,此時參與者需要能識別出這個事務(wù)已經(jīng)Commit了昆禽,不能重復(fù)Commit,也就是需要支持冪等蝇庭,當然這個問題還比較好處理醉鳖。
- 即使所有參與者都回復(fù)的是Yes,協(xié)調(diào)者如果在接收回復(fù)階段超時了哮内,然后寫Abort日志盗棵,之后宕機了。此時參與者執(zhí)行Commit操作會破壞原子性北发。那怎么辦呢纹因?有三種辦法:
- 第一種辦法是什么也不做,告警琳拨,等待人工處理而钞。
- 第二種辦法是等待網(wǎng)絡(luò)恢復(fù)或者協(xié)調(diào)者宕機重啟比原。
- 第三種辦法是保證協(xié)調(diào)者的可用性(Availablity),主協(xié)調(diào)者宕機后有其他的協(xié)調(diào)者能繼續(xù)服務(wù)饥伊。
協(xié)調(diào)者等待參與者Commit/Abort之后的ACK超時。根據(jù)上面的討論痊夭,參與者一定會執(zhí)行Commit/Abort操作,此時協(xié)調(diào)者可以認為事務(wù)已經(jīng)完成了,返回結(jié)果給客戶端偷俭。事實上,協(xié)調(diào)者寫完Commit/Abort日志缰盏,發(fā)送Commit/Abort請求給參與者后社搅,就可以直接返回結(jié)果給客戶端,不必等待最后的ACK乳规。
宕機問題
宕機問題都有可能轉(zhuǎn)化為超時問題形葬,宕機前如果寫了redo/undo日志,重啟后需要額外處理暮的。
- 協(xié)調(diào)者發(fā)出Prepare請求前宕機笙以。此時事務(wù)還未開始,不會有影響冻辩。
- 參與者在Prepare階段宕機猖腕。此時協(xié)調(diào)者超時,處理方式和上文網(wǎng)絡(luò)超時第一條相同恨闪。參與者重啟后需要向協(xié)調(diào)者查詢事務(wù)狀態(tài)倘感。
- 在協(xié)調(diào)者寫Commit/Abort日志前,協(xié)調(diào)者宕機咙咽。協(xié)調(diào)者重啟后老玛,進入Prepare超時處理流程,處理方式和上文網(wǎng)絡(luò)超時第一條相同钧敞。
- 在協(xié)調(diào)者寫Commit/Abort日志后蜡豹,協(xié)調(diào)者宕機。協(xié)調(diào)者重啟后溉苛,需要給參與者發(fā)送日志中決定的Commit/Abort請求镜廉。
- 在協(xié)調(diào)者寫Commit/Abort日志后,參與者宕機愚战。此時參與者在重啟后需要向協(xié)調(diào)者查詢事務(wù)狀態(tài)娇唯,執(zhí)行對應(yīng)的操作。
通過上面的討論寂玲,我們可以看到由于事務(wù)狀態(tài)分布在協(xié)調(diào)者和各個參與者之間塔插,要保證兩階段提交的一致性是非常困難的。如果能夠保證協(xié)調(diào)者的可用性(Availablity)敢茁,比如采用主備或者Paxos來實現(xiàn)佑淀,同時還需要保證各個參與者宕機后能夠重啟恢復(fù),那么正確實現(xiàn)兩階段提交會簡單不少,讀者可以再分析一遍上述情況伸刃。
上面只討論了原子性谎砾、一致性和持久性,沒討論隔離性的實現(xiàn)捧颅,隔離性可以考慮采用鎖方案景图,實現(xiàn)簡單,但性能可能比較差碉哑,另一種就是前文介紹的MVCC方案挚币,性能會好不少,但實現(xiàn)復(fù)雜扣典。
兩階段提交協(xié)議本身存在的問題
即使解決了前面說的一些問題妆毕,兩階段提交協(xié)議還是存在一個問題,這個問題是協(xié)議本身存在的贮尖,這就是參與者資源阻塞笛粘。
阻塞問題分成兩個方面,一個方面是整個事務(wù)過程中湿硝,參與者上的相應(yīng)資源會鎖住薪前。設(shè)想一下在Prepare階段,如果有9個參與者关斜,其中有一個沒有條件完成事務(wù)示括,其他8個參與者還是需要鎖住資源,寫redo/undo日志痢畜,然后在Commit階段垛膝,這8個參與者又需要回滾。另一個方面是如果協(xié)調(diào)者或者參與者宕機裁着,必須等待超時或者服務(wù)器重啟后發(fā)起Commit或者Abort后才能釋放資源繁涂。
三階段提交
針對兩階段提交的問題拱她,有人提出了三階段提交二驰。三階段提交比兩階段提交多了一個PreCommit階段,流程如下:
Coordinator Participant
can_commit
QUERY
-------------------------------->
VOTE YES/NO check
<-------------------------------
pre_commit*
PREPARE TO COMMIT
-------------------------------->
VOTE YES/NO prepare*/abort*
<-------------------------------
do_commit*/abort*
COMMIT/ROLLBACK
-------------------------------->
ACKNOWLEDGMENT commit*/abort*
<--------------------------------
end
An * next to the record type means that the record is forced to stable storage.
第一階段CanCommit
- 協(xié)調(diào)者詢問所有參與者是否可以執(zhí)行事務(wù)
- 參與者檢查是否可以執(zhí)行事務(wù)秉沼,如果可以執(zhí)行事務(wù)成功桶雀,回復(fù)Yes,如果不能唬复,回復(fù)No
第二階段PreCommit
分兩種情況
CanCommit階段所有參與者回復(fù)
- 協(xié)調(diào)者向所有參與者發(fā)送PreCommit請求
- 參與者執(zhí)行事務(wù)矗积,對資源加鎖,寫redo/undo日志到磁盤
- 如果參與者執(zhí)行事務(wù)成功敞咧,回復(fù)Yes棘捣,如果執(zhí)行失敗,回復(fù)No
CanCommit至少有一個參與者回復(fù)No
- 協(xié)調(diào)者向所有參與者發(fā)送Abort請求
- 參與者取消事務(wù)
第三階段DoCommit
分兩種情況
PreCommit階段所有參與者回復(fù)Yes
- 協(xié)調(diào)者向所有參與者發(fā)送Commit請求
- 參與者執(zhí)行Commit操作休建,釋放資源
- 參與者回復(fù)協(xié)調(diào)者ACK完成消息
- 協(xié)調(diào)者收到所有參與者完成消息后乍恐,完成事務(wù)
PreCommit至少有一個參與者回復(fù)No
- 協(xié)調(diào)者向所有參與者發(fā)送Rollback請求
- 參與者使用Undo日志回滾事務(wù)评疗,釋放資源
- 參與者回復(fù)協(xié)調(diào)者回滾完成消息
- 協(xié)調(diào)者收到所有參與者回滾完成消息后,取消事務(wù)
三階段提交和兩階段提交相比茵烈,優(yōu)點在于增加了CanCommit階段百匆,這個階段資源不會加鎖,如果有某個參與者不能執(zhí)行事務(wù)呜投,不會阻塞其他參與者加匈。但是三階段依然存在事務(wù)原子性和網(wǎng)絡(luò)分區(qū)問題。而且三階段增加了一個請求仑荐,整個事務(wù)的延遲會增加雕拼。
分布式事務(wù)模型
分布式事務(wù)實現(xiàn)上主要有四種模型:XA模型、TCC模型粘招、Saga模型和MQ模型悲没。
XA模型
XA模型是由X/Open組織制定的分布式事務(wù)規(guī)范和接口。
XA模型中有三種角色:
- AP(Application Program): 客戶端程序男图,定義事務(wù)的內(nèi)容
- TM(Transaction Manager): 事務(wù)的管理者示姿,也即兩階段提交的協(xié)調(diào)者
- RM(Resource Manager): 資源管理者,也即兩階段提交的參與者
XA接口規(guī)范如下:
XA模型的原子性通過兩階段提交實現(xiàn)逊笆,隔離性可以通過鎖或者MVCC實現(xiàn)栈戳,但是這里的鎖和MVCC不是單機下的,而是分布式鎖和分布式MVCC难裆,實現(xiàn)起來并不容易子檀。
XA模型嚴格保障事務(wù)ACID特性。事務(wù)執(zhí)行過程中需要將資源鎖定乃戈,這樣可能會導(dǎo)致性能低下褂痰。因此eBay架構(gòu)師Dan Pritchett提出了BASE理論。BASE是三個短語的縮寫:
- Basically Available: 基本可用症虑,允許損失部分可用性
- Soft state: 軟狀態(tài)缩歪,允許數(shù)據(jù)存在中間狀態(tài)
- Eventually consistent: 最終一致性,數(shù)據(jù)最終會達到一個一致的狀態(tài)
BASE通過犧牲強一致性來獲得可用性和系統(tǒng)性能的提升谍憔。下面介紹的TCC匪蝙、Saga、MQ都屬于BASE理論模型习贫,滿足最終一致性逛球。
TCC模型
TCC模型最早由Pat Helland于2007年發(fā)表的一篇名為《Life beyond Distributed Transactions:An Apostate’s Opinion》的論文提出。模型中苫昌,事務(wù)參與者需要實現(xiàn)Try, Confirm, Cancel三個接口颤绕。
- Try: 參與者檢查資源是否有效,預(yù)留資源。
- Confirm: 參與者提交資源奥务。
- Cancel: 參與者執(zhí)行回滾操作涕烧,恢復(fù)預(yù)留資源。
舉個例子汗洒,A向B轉(zhuǎn)賬100塊錢议纯。事務(wù)由兩個參與者PA(Participator A)和PB(Participator B)組成,PA負責給A減100塊錢溢谤,PB負責給B加100塊錢瞻凤。TCC模型流程如下:
Try階段
- PA檢查A賬戶是否有足夠的余額,凍結(jié)A賬戶的100塊世杀,寫日志到磁盤阀参。這個階段不需要鎖住A賬戶,其他事務(wù)可以對A賬戶操作瞻坝,可以看到這100塊蛛壳,但是不能對這100塊錢操作。
- PB檢查B賬戶的合法性所刀。
如果PA和PB在Try階段都返回成功衙荐,進入Confirm階段
- PA減A賬戶的100塊,寫日志到磁盤浮创。
- PB加100塊到B賬戶忧吟,寫日志到磁盤。
如果PA或者PB在Try階段返回失敗斩披,進入Cancel階段
- PA恢復(fù)A賬戶的100塊溜族,寫日志到磁盤,結(jié)束事務(wù)垦沉。
- PB結(jié)束事務(wù)煌抒。
TCC模型因為沒有在Try階段加鎖,所以性能高于兩階段提交厕倍。不同事務(wù)可以并發(fā)執(zhí)行寡壮,只要參與者管理的剩余資源足夠。具體實現(xiàn)時绑青,需要注意以下幾個問題:
- 每個參與者需要考慮如何將自身的業(yè)務(wù)拆分成Try, Confirm, Cancel這三個接口诬像。
- 如果Try成功,需要保證Confirm一定能成功闸婴。
- 需要保證三個接口的冪等性。由于網(wǎng)絡(luò)超時芍躏,請求可能會重發(fā)邪乍,這時參與者需要保證操作的冪等性,不能重復(fù)執(zhí)行同一個請求。
- 需要處理空Cancel操作庇楞。如果參與者沒有收到Try請求榜配,協(xié)調(diào)者可能觸發(fā)Cancel請求的發(fā)送,這時協(xié)調(diào)者需要處理這種沒有收到Try請求吕晌,反而收到Cancel請求的情況蛋褥。
- 需要處理先收到Cancel請求,后收到Try請求睛驳。如果由于網(wǎng)絡(luò)超時烙心,參與者沒有收到Try請求,協(xié)調(diào)者可能觸發(fā)Cancel請求的發(fā)送乏沸,參與者先收到Cancel請求淫茵,然后之前超時的Try請求又發(fā)送到參與者。
- 和兩階段提交一樣蹬跃,TCC模型也會遇到網(wǎng)絡(luò)分區(qū)匙瘪,服務(wù)器宕機等問題。
TCC模型使用兩階段提交來實現(xiàn)原子性蝶缀,但無法滿足隔離性丹喻。不同事務(wù)并發(fā)執(zhí)行的時候,隔離性只能滿足讀未提交級別(Read Uncommited)翁都,而且是由參與者在Try接口中預(yù)留資源的方式實現(xiàn)的驻啤。
Saga模型
Saga模型由Hector & Kenneth于1987年提出。這個模型中荐吵,每個事務(wù)參與者需要提供一個正向執(zhí)行模塊和逆向回滾模塊骑冗,執(zhí)行模塊用于執(zhí)行事務(wù)的正常操作,回滾模塊用于在失敗時執(zhí)行回滾操作先煎。
執(zhí)行流程如下贼涩,其中Ti表示每個參與者的正向執(zhí)行模塊,Ci表示每個參與者的逆向回滾模塊:
- 成功流程:T1 -> T2 -> T3 -> ... -> Tn
- 失敗流程:T1 -> T2 -> ... Ti (failed) -> Ci -> ... -> C2 -> C1
具體實現(xiàn)時薯蝎,根據(jù)是否有協(xié)調(diào)者遥倦,有兩種實現(xiàn)方式:
- 基于事件的分布式方案(Events/Choreography):沒有集中式協(xié)調(diào)者,每個參與者訂閱其他參與者的事件占锯,執(zhí)行自己的業(yè)務(wù)袒哥,生成新的事件供其他參與者使用。
- 基于命令的協(xié)調(diào)者方案(Command/Orchestrator):由集中式協(xié)調(diào)者控制事務(wù)流程消略,協(xié)調(diào)者發(fā)送命令給各個參與者堡称,參與者執(zhí)行事務(wù)后發(fā)送結(jié)果給協(xié)調(diào)者。
舉個例子來說明這兩者的不同艺演。一次完整的網(wǎng)絡(luò)購物由四個服務(wù)組成:訂單服務(wù)(Order Service)却紧、支付服務(wù)(Payment Service)桐臊、庫存服務(wù)(Stock Service)和送貨服務(wù)(Delivery Service)組成。
基于事件的分布式方案的成功流程如下:
- 訂單服務(wù)創(chuàng)建訂單晓殊,發(fā)出訂單創(chuàng)建事件ORDER_CREATED_EVENT断凶。
- 支付服務(wù)訂閱ORDER_CREATED_EVENT,執(zhí)行支付操作巫俺,發(fā)出支付完成事件BILLED_ORDER_EVENT认烁。
- 庫存服務(wù)訂閱BILLED_ORDER_EVENT,扣除庫存介汹,發(fā)出貨物準備完成事件ORDER_PREPARED_EVENT却嗡。
- 送貨服務(wù)訂閱ORDER_PREPARED_EVENT,送貨痴昧,發(fā)出貨物交付事件ORDER_DELIVERED_EVENT稽穆。
- 訂單服務(wù)訂閱ORDER_DELIVERED_EVENT,標記事務(wù)完成赶撰。
基于事件的分布式方案的失敗流程如下:
庫存服務(wù)發(fā)現(xiàn)庫存不足舌镶,發(fā)出庫存不足事件PRODUCT_OUT_OF_STOCK_EVENT。
支付服務(wù)訂閱PRODUCT_OUT_OF_STOCK_EVENT豪娜,給用戶返回錢餐胀。
訂單服務(wù)訂閱PRODUCT_OUT_OF_STOCK_EVENT,標記訂單失敗瘤载。
基于命令的協(xié)調(diào)者方案的成功流程如下:
- 訂單服務(wù)創(chuàng)建訂單否灾,發(fā)送訂單事務(wù)給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送支付命令給支付服務(wù)鸣奔,支付服務(wù)執(zhí)行支付墨技,返回結(jié)果給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送準備訂單命令給庫存服務(wù)挎狸,庫存服務(wù)扣除庫存扣汪,返回結(jié)果給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送送貨命令給送貨服務(wù)锨匆,送貨服務(wù)送貨崭别,返回結(jié)果給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送結(jié)果給訂單服務(wù)恐锣,標記事務(wù)完成茅主。
基于命令的協(xié)調(diào)者方案的失敗流程如下:
- 庫存服務(wù)發(fā)現(xiàn)庫存不足,返回庫存不足結(jié)果給協(xié)調(diào)者土榴。
- 協(xié)調(diào)者發(fā)送返錢命令給支付服務(wù)诀姚,支付服務(wù)返錢給用戶。
- 協(xié)調(diào)者發(fā)送失敗結(jié)果給訂單服務(wù)鞭衩,標記事務(wù)失敗学搜。
因為Saga模型是一階段娃善,而TCC模型是兩階段论衍,和TCC模型相比瑞佩,Saga模型的性能要高一些,而且實現(xiàn)的時候要簡單一些坯台。但因為Saga模型沒有Try階段預(yù)留操作炬丸,在回滾的時候就會麻煩不少。比如發(fā)郵件服務(wù)蜒蕾,正向階段已經(jīng)發(fā)郵件給用戶稠炬,回滾的時候會對用戶不友好。
和TCC類似咪啡,Saga模型在實現(xiàn)時也需要注意冪等性首启,空操作,請求亂序等問題撤摸。另外Saga模型也可以滿足原子性毅桃,但無法滿足隔離性。不同事務(wù)并發(fā)執(zhí)行的時候准夷,隔離性只能滿足讀未提交級別(Read Uncommited)钥飞。
MQ模型
MQ模型使用消息隊列(Message Queue)來通知事務(wù)的各個參與者執(zhí)行操作。
MQ模型流程如下:
Sponsor MQ Participant
PREPARE
--------------->
ACK prepare*
<---------------
commit*
COMMIT
--------------->
commit*/abort*
COMMIT
--------------->
ACK commit*
end <---------------
An * next to the record type means that the record is forced to stable storage.
- 事務(wù)發(fā)起者發(fā)送Prepare消息到MQ衫嵌。
- MQ收到Prepare消息后读宙,保存到磁盤,不發(fā)送給事務(wù)參與者楔绞,返回ACK給事務(wù)發(fā)起者结闸。
- 事務(wù)發(fā)起者如果沒收到ACK,取消事務(wù)的執(zhí)行酒朵,給MQ發(fā)送Abort消息桦锄。如果收到ACK,執(zhí)行本地事務(wù)耻讽,給MQ發(fā)送Commit消息察纯。
- MQ收到消息后,如果是Abort针肥,刪除事務(wù)消息饼记。如果是Commit,MQ修改消息狀態(tài)為可發(fā)送慰枕,并發(fā)送該事務(wù)消息給事務(wù)參與者具则。
- 事務(wù)參與者收到消息后,執(zhí)行事務(wù)具帮,然后發(fā)送ACK給MQ博肋。
- MQ刪除事務(wù)消息低斋,標記事務(wù)完成。
流程中幾個需要注意的地方:
- 第4步中匪凡,如果MQ沒有收到事務(wù)發(fā)起者發(fā)送的Commit/Abort消息膊畴,MQ會向發(fā)起者查詢事務(wù)狀態(tài),根據(jù)狀態(tài)執(zhí)行后續(xù)操作病游。
- 第5步中唇跨,如果MQ長時間沒有收到事務(wù)參與者的ACK消息,MQ會按照間隔(比如1分鐘衬衬,5分鐘买猖,10分鐘,1小時滋尉,1天等)不斷重復(fù)發(fā)送Commit消息給事務(wù)參與者玉控,直至收到ACK。
從以上流程可以發(fā)現(xiàn)MQ事務(wù)模型依賴于MQ支持事務(wù)消息狮惜,目前只有RocketMQ支持事務(wù)消息高诺。如果不用RocketMQ,需要自己實現(xiàn)一個消息可靠性模塊讽挟,完成類似的功能懒叛。
MQ模型中,需要確保參與者一定能成功執(zhí)行事務(wù)耽梅,參與者不能說自己沒有條件執(zhí)行事務(wù)薛窥,比如支付服務(wù)作為參與者檢查發(fā)現(xiàn)用戶余額不足。所以MQ模型有很大的使用范圍限制眼姐。一般在邏輯上有可能失敗的操作(比如支付)需要由事務(wù)發(fā)起者完成诅迷,而事務(wù)參與者只執(zhí)行一定會成功的操作(比如充話費、發(fā)送游戲道具等)众旗。
和TCC罢杉,Saga模型一樣,MQ模型也需要注意事務(wù)參與者的冪等性贡歧。
各模型開源實現(xiàn)
支持Java項目滩租,支持TCC和Saga模型,支持Spring Cloud和Dubbo
支持Java項目利朵,支持TCC模型
支持Java項目律想,支持TCC、Saga和MQ模型
阿里開源的分布式事務(wù)方案绍弟,支持Java技即,支持AT(針對SQL數(shù)據(jù)庫)、TCC樟遣、Saga模型
華為開源的分布式事務(wù)方案而叼,支持Java身笤,支持Saga模型
參考:
MySQL技術(shù)內(nèi)幕:InnoDB存儲引擎
MySQL InnoDB Update和Crash Recovery流程
InnoDB Repeatable Read隔離級別之大不同
從0到1理解數(shù)據(jù)庫事務(wù)(下):隔離級別實現(xiàn)——MVCC與鎖
The basics of the InnoDB undo logging and history system
Saga Pattern How to Implement Business Transactions Using Microservices