從單機事務(wù)到分布式事務(wù)

transaction.jpg

最近在研究分布式數(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ù)庫的讀/寫操作。包含有以下兩個目的:

  1. 為數(shù)據(jù)庫操作序列提供了一個從失敗中恢復(fù)到正常狀態(tài)的方法以舒,同時提供了數(shù)據(jù)庫即使在異常狀態(tài)下仍能保持一致性的方法蔓钟。

  2. 當多個應(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,格式如下:

post-redo-block.png

每個block由12字節(jié)頭部log block header跷乐,492字節(jié)日志內(nèi)容log block和8字節(jié)尾部log block tailer組成肥败。

log block日志內(nèi)容中保存是具體redo日志,格式如下:

post-redo-log-body.png
  • 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ù)雜了缘挑,不詳細展開。

post-redo-recovery.png

undo日志

undo日志用于事務(wù)的回滾和MVCC桶略,分別對應(yīng)原子性和隔離性语淘。MySQL中修改數(shù)據(jù)時,并不是簡單地在當前數(shù)據(jù)上修改际歼,而是先把修改前的數(shù)據(jù)保存在undo log中惶翻,然后修改當前數(shù)據(jù)并且在當前數(shù)據(jù)中增加一個指針指向修改前的數(shù)據(jù)。如下圖所示鹅心,undo日志組成了一個鏈表:

post-undo-history.png

圖中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日志格式。

post-undo-log-format.png
  • 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ù)庫有四種隔離級別锄开,由低到高如下:

  1. 未提交讀(READ UNCOMMITED):允許讀取其他事務(wù)未提交的修改
  2. 已提交讀(READ COMMITED):只能讀取其他事務(wù)已提交的修改
  3. 可重復(fù)讀(REPEATABLE READ):同一個事務(wù)內(nèi)素标,多次讀取操作得到的每個數(shù)據(jù)行的內(nèi)容是一樣的
  4. 可串行化(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ù)是否可見:

  1. 如果當前版本數(shù)據(jù)的事務(wù)ID和creator_trx_id相同金度,說明是當前事務(wù)修改的記錄应媚,此時該版本數(shù)據(jù)可見。
  2. 如果當前版本數(shù)據(jù)的事務(wù)ID小于min_trx_id猜极,說明是已經(jīng)提交的事務(wù)作的修改中姜,該版本數(shù)據(jù)可見。
  3. 如果當前版本數(shù)據(jù)的事務(wù)ID大于等于max_trx_id跟伏,說明是該版本的事務(wù)是在read view創(chuàng)建之后生成的丢胚,該版本數(shù)據(jù)不可見
  4. 如果當前版本數(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ù)流程。

準備階段:

  1. 分配事務(wù)ID
  2. 如果隔離級別是REPEATABLE READ冈爹,創(chuàng)建read view
  3. 分配undo日志涌攻,把修改之前的數(shù)據(jù)寫入undo日志
  4. 為undo日志的修改創(chuàng)建redo日志,寫入redo log buffer
  5. 在buffer pool中修改數(shù)據(jù)頁频伤,回滾指針指向undo日志
  6. 為數(shù)據(jù)頁的修改創(chuàng)建redo日志恳谎,寫入redo log buffer

提交階段:

  1. 寫binlog到磁盤
  2. 修改undo日志狀態(tài)為"purge"
  3. 根據(jù)innodb_flush_log_at_trx_commit判斷是否需要立即把redo log buffer寫入磁盤
  4. buffer pool中的數(shù)據(jù)頁根據(jù)規(guī)則等待合適的時機寫入磁盤

恢復(fù)或者回滾階段:

  1. 根據(jù)redo日志中的LSN和事務(wù)ID,數(shù)據(jù)文件中的LSN和binlog中的事務(wù)ID決定需要做恢復(fù)還是回滾
  2. 如果事務(wù)已提交憋肖,但數(shù)據(jù)頁還未寫入磁盤因痛,需要恢復(fù),根據(jù)redo日志中的字段對數(shù)據(jù)頁進行恢復(fù)
  3. 如果需要回滾岸更,根據(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

  1. 協(xié)調(diào)者分配事務(wù)ID,寫到磁盤兜辞,然后詢問所有參與者是否可以執(zhí)行事務(wù)

  2. 參與者執(zhí)行事務(wù)迎瞧,對資源加鎖,寫redo/undo日志到磁盤

  3. 如果參與者執(zhí)行事務(wù)成功逸吵,回復(fù)Yes凶硅,如果執(zhí)行失敗,回復(fù)No

第二階段Commit/Abort

分兩種情況

所有參與者回復(fù)Yes扫皱,執(zhí)行Commit

  1. 協(xié)調(diào)者寫Commit日志到磁盤足绅,然后向所有參與者發(fā)送Commit請求

  2. 參與者執(zhí)行Commit操作,寫Commit日志到磁盤韩脑,釋放資源

  3. 參與者回復(fù)協(xié)調(diào)者ACK完成消息

  4. 協(xié)調(diào)者收到所有參與者完成消息后氢妈,完成事務(wù)

至少有一個參與者回復(fù)No,執(zhí)行Abort

  1. 協(xié)調(diào)者寫Abort日志到磁盤段多,然后向所有參與者發(fā)送Abort請求

  2. 參與者使用Undo日志回滾事務(wù)首量,寫Abort日志到磁盤,釋放資源

  3. 參與者回復(fù)協(xié)調(diào)者回滾完成消息

  4. 協(xié)調(diào)者收到所有參與者回滾完成消息后进苍,取消事務(wù)

整個兩階段流程加缘,看著挺簡單的,但是麻煩的地方在于如何處理服務(wù)器宕機和網(wǎng)絡(luò)超時問題觉啊,我們來分析一下整個過程如何處理這兩個問題拣宏。首先有個原則是如果協(xié)調(diào)者已經(jīng)寫Commit日志到磁盤,各個參與者就應(yīng)該提交事務(wù)柄延,不允許回滾蚀浆。

網(wǎng)絡(luò)超時問題

  1. 協(xié)調(diào)者發(fā)送完P(guān)repare請求后,在規(guī)定時間內(nèi)沒有收到所有參與者的回復(fù)搜吧。此時簡單的做法是協(xié)調(diào)者發(fā)送Abort請求給所有參與者市俊,參與者執(zhí)行回滾操作。因為可能所有的參與者都Prepare成功滤奈,只是協(xié)調(diào)者沒有收到回復(fù)消息摆昧,這種做法選擇了正確性,犧牲了性能蜒程。

  2. 參與者等待協(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ù)饥伊。
  3. 協(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日志,重啟后需要額外處理暮的。

  1. 協(xié)調(diào)者發(fā)出Prepare請求前宕機笙以。此時事務(wù)還未開始,不會有影響冻辩。
  2. 參與者在Prepare階段宕機猖腕。此時協(xié)調(diào)者超時,處理方式和上文網(wǎng)絡(luò)超時第一條相同恨闪。參與者重啟后需要向協(xié)調(diào)者查詢事務(wù)狀態(tài)倘感。
  3. 在協(xié)調(diào)者寫Commit/Abort日志前,協(xié)調(diào)者宕機咙咽。協(xié)調(diào)者重啟后老玛,進入Prepare超時處理流程,處理方式和上文網(wǎng)絡(luò)超時第一條相同钧敞。
  4. 在協(xié)調(diào)者寫Commit/Abort日志后蜡豹,協(xié)調(diào)者宕機。協(xié)調(diào)者重啟后溉苛,需要給參與者發(fā)送日志中決定的Commit/Abort請求镜廉。
  5. 在協(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

  1. 協(xié)調(diào)者詢問所有參與者是否可以執(zhí)行事務(wù)
  2. 參與者檢查是否可以執(zhí)行事務(wù)秉沼,如果可以執(zhí)行事務(wù)成功桶雀,回復(fù)Yes,如果不能唬复,回復(fù)No

第二階段PreCommit

分兩種情況

CanCommit階段所有參與者回復(fù)

  1. 協(xié)調(diào)者向所有參與者發(fā)送PreCommit請求
  2. 參與者執(zhí)行事務(wù)矗积,對資源加鎖,寫redo/undo日志到磁盤
  3. 如果參與者執(zhí)行事務(wù)成功敞咧,回復(fù)Yes棘捣,如果執(zhí)行失敗,回復(fù)No

CanCommit至少有一個參與者回復(fù)No

  1. 協(xié)調(diào)者向所有參與者發(fā)送Abort請求
  2. 參與者取消事務(wù)

第三階段DoCommit

分兩種情況

PreCommit階段所有參與者回復(fù)Yes

  1. 協(xié)調(diào)者向所有參與者發(fā)送Commit請求
  2. 參與者執(zhí)行Commit操作休建,釋放資源
  3. 參與者回復(fù)協(xié)調(diào)者ACK完成消息
  4. 協(xié)調(diào)者收到所有參與者完成消息后乍恐,完成事務(wù)

PreCommit至少有一個參與者回復(fù)No

  1. 協(xié)調(diào)者向所有參與者發(fā)送Rollback請求
  2. 參與者使用Undo日志回滾事務(wù)评疗,釋放資源
  3. 參與者回復(fù)協(xié)調(diào)者回滾完成消息
  4. 協(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ī)范和接口。

post-xa.png

XA模型中有三種角色:

  • AP(Application Program): 客戶端程序男图,定義事務(wù)的內(nèi)容
  • TM(Transaction Manager): 事務(wù)的管理者示姿,也即兩階段提交的協(xié)調(diào)者
  • RM(Resource Manager): 資源管理者,也即兩階段提交的參與者

XA接口規(guī)范如下:

post-xa-api.png

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)組成。

基于事件的分布式方案的成功流程如下:

post-saga-choreography-commit.png
  1. 訂單服務(wù)創(chuàng)建訂單晓殊,發(fā)出訂單創(chuàng)建事件ORDER_CREATED_EVENT断凶。
  2. 支付服務(wù)訂閱ORDER_CREATED_EVENT,執(zhí)行支付操作巫俺,發(fā)出支付完成事件BILLED_ORDER_EVENT认烁。
  3. 庫存服務(wù)訂閱BILLED_ORDER_EVENT,扣除庫存介汹,發(fā)出貨物準備完成事件ORDER_PREPARED_EVENT却嗡。
  4. 送貨服務(wù)訂閱ORDER_PREPARED_EVENT,送貨痴昧,發(fā)出貨物交付事件ORDER_DELIVERED_EVENT稽穆。
  5. 訂單服務(wù)訂閱ORDER_DELIVERED_EVENT,標記事務(wù)完成赶撰。

基于事件的分布式方案的失敗流程如下:

post-saga-choreography-rollback.png
  1. 庫存服務(wù)發(fā)現(xiàn)庫存不足舌镶,發(fā)出庫存不足事件PRODUCT_OUT_OF_STOCK_EVENT。

  2. 支付服務(wù)訂閱PRODUCT_OUT_OF_STOCK_EVENT豪娜,給用戶返回錢餐胀。

  3. 訂單服務(wù)訂閱PRODUCT_OUT_OF_STOCK_EVENT,標記訂單失敗瘤载。

基于命令的協(xié)調(diào)者方案的成功流程如下:

post-saga-orchestrator-commit.png
  1. 訂單服務(wù)創(chuàng)建訂單否灾,發(fā)送訂單事務(wù)給協(xié)調(diào)者。
  2. 協(xié)調(diào)者發(fā)送支付命令給支付服務(wù)鸣奔,支付服務(wù)執(zhí)行支付墨技,返回結(jié)果給協(xié)調(diào)者。
  3. 協(xié)調(diào)者發(fā)送準備訂單命令給庫存服務(wù)挎狸,庫存服務(wù)扣除庫存扣汪,返回結(jié)果給協(xié)調(diào)者。
  4. 協(xié)調(diào)者發(fā)送送貨命令給送貨服務(wù)锨匆,送貨服務(wù)送貨崭别,返回結(jié)果給協(xié)調(diào)者。
  5. 協(xié)調(diào)者發(fā)送結(jié)果給訂單服務(wù)恐锣,標記事務(wù)完成茅主。

基于命令的協(xié)調(diào)者方案的失敗流程如下:

post-saga-orchestrator-rollback.png
  1. 庫存服務(wù)發(fā)現(xiàn)庫存不足,返回庫存不足結(jié)果給協(xié)調(diào)者土榴。
  2. 協(xié)調(diào)者發(fā)送返錢命令給支付服務(wù)诀姚,支付服務(wù)返錢給用戶。
  3. 協(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.
  1. 事務(wù)發(fā)起者發(fā)送Prepare消息到MQ衫嵌。
  2. MQ收到Prepare消息后读宙,保存到磁盤,不發(fā)送給事務(wù)參與者楔绞,返回ACK給事務(wù)發(fā)起者结闸。
  3. 事務(wù)發(fā)起者如果沒收到ACK,取消事務(wù)的執(zhí)行酒朵,給MQ發(fā)送Abort消息桦锄。如果收到ACK,執(zhí)行本地事務(wù)耻讽,給MQ發(fā)送Commit消息察纯。
  4. MQ收到消息后,如果是Abort针肥,刪除事務(wù)消息饼记。如果是Commit,MQ修改消息狀態(tài)為可發(fā)送慰枕,并發(fā)送該事務(wù)消息給事務(wù)參與者具则。
  5. 事務(wù)參與者收到消息后,執(zhí)行事務(wù)具帮,然后發(fā)送ACK給MQ博肋。
  6. 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)

ByteTCC

支持Java項目滩租,支持TCC和Saga模型,支持Spring Cloud和Dubbo

tcc-transaction

支持Java項目利朵,支持TCC模型

EasyTransaction

支持Java項目律想,支持TCC、Saga和MQ模型

Seata

阿里開源的分布式事務(wù)方案绍弟,支持Java技即,支持AT(針對SQL數(shù)據(jù)庫)、TCC樟遣、Saga模型

Apache ServiceComb

華為開源的分布式事務(wù)方案而叼,支持Java身笤,支持Saga模型

參考:

數(shù)據(jù)庫事務(wù)

ACID

事務(wù)隔離

Innodb中的事務(wù)隔離級別和鎖的關(guān)系

MySQL技術(shù)內(nèi)幕:InnoDB存儲引擎

MySQL InnoDB Update和Crash Recovery流程

InnoDB undo log 漫游

5.7 Innodb事務(wù)系統(tǒng)

InnoDB Repeatable Read隔離級別之大不同

從0到1理解數(shù)據(jù)庫事務(wù)(下):隔離級別實現(xiàn)——MVCC與鎖

The basics of the InnoDB undo logging and history system

兩階段提交

Two-Phase Commit

分布式事務(wù) - 兩階段提交與三階段提交

Saga Pattern How to Implement Business Transactions Using Microservices

如何選擇分布式事務(wù)解決方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葵陵,隨后出現(xiàn)的幾起案子液荸,更是在濱河造成了極大的恐慌,老刑警劉巖埃难,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莹弊,死亡現(xiàn)場離奇詭異涤久,居然都是意外死亡涡尘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門响迂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來考抄,“玉大人,你說我怎么就攤上這事蔗彤〈罚” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵然遏,是天一觀的道長贫途。 經(jīng)常有香客問我,道長待侵,這世上最難降的妖魔是什么丢早? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮秧倾,結(jié)果婚禮上怨酝,老公的妹妹穿的比我還像新娘。我一直安慰自己那先,他們只是感情好农猬,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著售淡,像睡著了一般斤葱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上揖闸,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天揍堕,我揣著相機與錄音,去河邊找鬼楔壤。 笑死鹤啡,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的蹲嚣。 我是一名探鬼主播递瑰,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼祟牲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抖部?” 一聲冷哼從身側(cè)響起说贝,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慎颗,沒想到半個月后乡恕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡俯萎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年傲宜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夫啊。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡函卒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出撇眯,到底是詐尸還是另有隱情报嵌,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布熊榛,位于F島的核電站锚国,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏玄坦。R本人自食惡果不足惜血筑,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望营搅。 院中可真熱鬧云挟,春花似錦、人聲如沸转质。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽休蟹。三九已至沸枯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赂弓,已是汗流浹背绑榴。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盈魁,地道東北人翔怎。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親赤套。 傳聞我的和親對象是個殘疾皇子飘痛,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344