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

transaction.jpg

最近在研究分布式數(shù)據(jù)庫相關(guān)的技術(shù),對于數(shù)據(jù)庫來說,不管是單機(jī)數(shù)據(jù)庫還是分布式數(shù)據(jù)庫,事務(wù)都是一個繞不去的坎。不光是數(shù)據(jù)庫,對于微服務(wù)架構(gòu),不同服務(wù)之間也會涉及到分布式事務(wù)的處理。本文先介紹事務(wù)的基本概念和原理,然后介紹單機(jī)事務(wù)的實現(xiàn)方案,最后介紹分布式事務(wù)的實現(xiàn)方案。

學(xué)習(xí)事務(wù)的過程中參考了不少文章,這些文章比本文更有價值,結(jié)尾有這些文章的地址。

什么是事務(wù)

描述原理和實現(xiàn)之前,我們首先需要了解究竟什么是事務(wù)。就像開會,先劃定議題,介紹背景知識,才能更高效的討論。維基百科中對事務(wù)的描述如下:

數(shù)據(jù)庫事務(wù)通常包含了一個序列的對數(shù)據(jù)庫的讀/寫操作。包含有以下兩個目的:

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

  2. 當(dāng)多個應(yīng)用程序在并發(fā)訪問數(shù)據(jù)庫時,可以在這些應(yīng)用程序之間提供一個隔離方法,以防止彼此的操作互相干擾。

當(dā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)也能保持?jǐn)?shù)據(jù)的一致,三是隔離性,一個事務(wù)的執(zhí)行不會影響其他事務(wù)。要實現(xiàn)這幾個特性,是數(shù)據(jù)庫事務(wù)的主要難點。其實準(zhǔn)確地說,是有四個特性,也就是常說的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ù)進(jìn)行讀寫和修改的能力,隔離性可以防止多個事務(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ù)前的總和是一樣的。

單機(jī)事務(wù)

傳統(tǒng)單機(jī)情況下,數(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宕機(jī)了,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ù)來控制寫盤時機(jī),有三種取值:

  • 0,事務(wù)提交時不寫盤,由線程每秒寫盤一次。
  • 1,事務(wù)提交時調(diào)用fsync強制寫盤。
  • 2,事務(wù)提交時寫入文件系統(tǒng)緩存,由操作系統(tǒng)決定何時將緩存寫入磁盤。

如果設(shè)置為0,MySQL服務(wù)器進(jìn)程宕機(jī)時有可能丟失數(shù)據(jù);如果設(shè)置為2,操作系統(tǒng)宕機(jī)時有可能丟失數(shù)據(jù)。

redo日志并不一定是提交才會寫盤,如果innodb_flush_log_at_trx_commit設(shè)置為0,即使還沒提交,也可能寫盤。

如果每次修改數(shù)據(jù)都需要寫redo日志到磁盤,那為什么不把Buffer Pool中的數(shù)據(jù)直接寫磁盤呢?原因主要有兩個:

  • 直接刷新數(shù)據(jù)是一個隨機(jī)IO,每次修改的數(shù)據(jù)在不同的數(shù)據(jù)頁中,而redo日志是連續(xù)的,寫盤是順序IO。
  • 直接刷新數(shù)據(jù)是以數(shù)據(jù)頁為單位,MySQL默認(rèn)是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é),是一個遞增的值,表示當(dāng)前redo日志總共有多少個字節(jié)。LSN保存在redo日志文件和每個數(shù)據(jù)頁的頭部。寫redo文件時,MySQL會把當(dāng)前的LSN一起寫入文件,然后修改內(nèi)存當(dāng)前數(shù)據(jù)頁的LSN。等到數(shù)據(jù)頁寫盤時,LSN也會一起保存在該數(shù)據(jù)頁對應(yīng)的磁盤中,而且當(dāng)前LSN值會寫入數(shù)據(jù)文件ibdata的第一個page中作為整個數(shù)據(jù)文件的checkpoint。

MySQL啟動時,首先檢查當(dāng)前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,需要對這一頁進(jìn)行數(shù)據(jù)恢復(fù)。理論上redo日志中所有在checkpoint之后的事務(wù)都需要恢復(fù),為什么這里還要比較每一頁的LSN?這是因為MySQL刷臟頁時,是先把所有臟頁寫入磁盤,最后再寫入checkpoint LSN。有可能臟頁已經(jīng)寫入磁盤,但是在寫入checkpoint LSN前宕機(jī),這就需要在恢復(fù)事務(wù)時判斷數(shù)據(jù)頁中的LSN,避免重復(fù)恢復(fù)。這里只介紹了redo日志在數(shù)據(jù)恢復(fù)時的使用,實際上還要結(jié)合binlog一起使用,這就更復(fù)雜了,不詳細(xì)展開。

post-redo-recovery.png

undo日志

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

post-undo-history.png

圖中undo列表由三個SQL操作組成,左上角為當(dāng)前記錄的內(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: 一些標(biāo)記位
  • DATA_TRX_ID: 這次修改對應(yīng)的事務(wù)ID
  • DATA_ROLL_PTR: 回滾指針,記錄當(dāng)前數(shù)據(jù)上一個版本在回滾段中的位置
  • update vector: 表示修改的數(shù)據(jù)
  • start: 表示上一條undo日志位置

除了上面介紹的字段,undo日志還有一個undo header頭部信息,其中一個字段是TRX_UNDO_STATE,表示undo日志的狀態(tài)。取值有下面幾個:

  • TRX_UNDO_ACTIVE: 初使?fàn)顟B(tài)
  • TRX_UNDO_CACHED:
  • TRX_UNDO_TO_FREE: 可以釋放
  • TRX_UNDO_TO_PURGE: 可以清理
  • TRX_UNDO_PREPARED: 準(zhǔn)備狀態(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ù)作為當(dāng)前數(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é)合起來,就是嚴(yán)格可串行化(Strict Serializable),既滿足可串行化,也滿足線性一致性,是最高的一致性模型。

不同的隔離級別可以解決不同級別的讀問題,如下:

隔離級別 臟讀 不可重復(fù)讀 幻讀
未提交讀 可能發(fā)生 可能發(fā)生 可能發(fā)生
提交讀 - 可能發(fā)生 可能發(fā)生
可重復(fù)讀 - - 可能發(fā)生
可序列化 - - -

臟讀、不可重復(fù)讀、幻讀的解釋如下:

臟讀

當(dāng)一個事務(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ù)中,當(dāng)一行數(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í)行過程中,當(dāng)兩個完全相同的查詢語句執(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日志是如何支持不同的隔離級別,解決讀問題的呢?

對于未提交讀,只要讀取記錄當(dāng)前版本的值就行了。

對于已提交讀,在執(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. 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID和creator_trx_id相同,說明是當(dāng)前事務(wù)修改的記錄,此時該版本數(shù)據(jù)可見。
  2. 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID小于min_trx_id,說明是已經(jīng)提交的事務(wù)作的修改,該版本數(shù)據(jù)可見。
  3. 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID大于等于max_trx_id,說明是該版本的事務(wù)是在read view創(chuàng)建之后生成的,該版本數(shù)據(jù)不可見
  4. 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID在min_trx_id和max_trx_id之間,并且在m_ids內(nèi),該版本數(shù)據(jù)不可見,如果不在m_ids內(nèi),該版本數(shù)據(jù)可見。

如果當(dāng)前版本數(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可以處理。但是另一種幻讀則處理不了,這涉及到快照讀和當(dāng)前讀的概念。快照讀就是前面介紹的使用undo日志來選擇一個合適的版本來讀取,select操作使用這種方式。而insert、update和delete則使用當(dāng)前讀,對于這幾個修改操作,必須使用最新的數(shù)據(jù)進(jìn)行修改。舉個例子:

事務(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í)行更新操作,使用當(dāng)前讀獲取的最新數(shù)據(jù),將id為3的記錄名字改成了Tom,然后事務(wù)1執(zhí)行第二次查詢,這時是可以讀到id為3的數(shù)據(jù)的,兩次select讀到的數(shù)據(jù)不一樣。因為事務(wù)1進(jìn)行update操作,數(shù)據(jù)的版本號是事務(wù)1自己的事務(wù)ID,所以第二次select能讀到id為3的記錄。是不是很復(fù)雜?

事務(wù)流程

介紹完了redo日志,undo日志后,我們完整描述一下事務(wù)流程。

準(zhǔn)備階段:

  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ī)則等待合適的時機(jī)寫入磁盤

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

  1. 根據(jù)redo日志中的LSN和事務(wù)ID,數(shù)據(jù)文件中的LSN和binlog中的事務(wù)ID決定需要做恢復(fù)還是回滾
  2. 如果事務(wù)已提交,但數(shù)據(jù)頁還未寫入磁盤,需要恢復(fù),根據(jù)redo日志中的字段對數(shù)據(jù)頁進(jìn)行恢復(fù)
  3. 如果需要回滾,根據(jù)undo日志中的上一個版本數(shù)據(jù)進(jìn)行回滾

分布式事務(wù)協(xié)議

分布式事務(wù)有兩種形式,一種是分布式數(shù)據(jù)庫事務(wù),另一種分布式微服務(wù)事務(wù)。

第一種分布式數(shù)據(jù)庫事務(wù)由傳統(tǒng)單機(jī)事務(wù)進(jìn)化而來。當(dāng)單機(jī)數(shù)據(jù)庫無法支撐所有負(fù)載時,必然要將數(shù)據(jù)拆分到多臺數(shù)據(jù)庫。如果一次操作涉及到多臺數(shù)據(jù)庫,那就需要一種方案來維護(hù)整個數(shù)據(jù)庫集群的事務(wù)特性,也就是ACID特性。

第二種分布式微服務(wù)事務(wù)由分布在不同機(jī)器上的服務(wù)組成。最近微服務(wù)架構(gòu)興起,不同的服務(wù)被拆分到不同的服務(wù)器進(jìn)程中。比如一個網(wǎng)絡(luò)購物服務(wù),由訂單服務(wù)器,支付服務(wù)器,倉儲服務(wù)器和物流服務(wù)器組成。每個服務(wù)器使用的數(shù)據(jù)存儲方案可能不同,有的用MySQL,有的用Redis。如何讓網(wǎng)絡(luò)購物服務(wù)完整執(zhí)行,而不會導(dǎo)致支付了但倉庫中沒有剩余庫存,這是分布式服務(wù)事務(wù)需要處理的問題之一。

分布式事務(wù)和單機(jī)事務(wù)相比,處理的問題更為困難。

第一個問題是因為有多個事務(wù)參與者。傳統(tǒng)單機(jī)事務(wù),如果宕機(jī),整個事務(wù)的執(zhí)行都會失敗,原子性比較好處理。但是分布式事務(wù),參與者分布在不同的機(jī)器上,可能第一個參與者成功鎖住資源,而第二個參與者對資源加鎖失敗,也可能第一個參與者提交事務(wù)成功,但是第二個參與者提交時機(jī)器宕機(jī),處理起來困難。

第二個問題是網(wǎng)絡(luò)超時導(dǎo)致的問題。服務(wù)器A告訴服務(wù)器B提交事務(wù),然后超時了,沒有收到服務(wù)器B的響應(yīng)。這時有可能服務(wù)器B沒收到請求,也可能服務(wù)器B收到請求,成功處理,發(fā)送給A的響應(yīng)丟失。如何區(qū)分這兩種情況然后進(jìn)行處理也是困難的地方。

第三個問題是距離導(dǎo)致的延遲問題。傳統(tǒng)單機(jī)事務(wù)不存在網(wǎng)絡(luò)延遲,所有操作都在一臺機(jī)器上處理。但是分布式事務(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ù)器宕機(jī)和網(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)者宕機(jī)或者協(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,也就是需要支持冪等,當(dāng)然這個問題還比較好處理。
          • 即使所有參與者都回復(fù)的是Yes,協(xié)調(diào)者如果在接收回復(fù)階段超時了,然后寫Abort日志,之后宕機(jī)了。此時參與者執(zhí)行Commit操作會破壞原子性。那怎么辦呢?有三種辦法:
            • 第一種辦法是什么也不做,告警,等待人工處理。
            • 第二種辦法是等待網(wǎng)絡(luò)恢復(fù)或者協(xié)調(diào)者宕機(jī)重啟。
            • 第三種辦法是保證協(xié)調(diào)者的可用性(Availablity),主協(xié)調(diào)者宕機(jī)后有其他的協(xié)調(diào)者能繼續(xù)服務(wù)。
  3. 協(xié)調(diào)者等待參與者Commit/Abort之后的ACK超時。根據(jù)上面的討論,參與者一定會執(zhí)行Commit/Abort操作,此時協(xié)調(diào)者可以認(rèn)為事務(wù)已經(jīng)完成了,返回結(jié)果給客戶端。事實上,協(xié)調(diào)者寫完Commit/Abort日志,發(fā)送Commit/Abort請求給參與者后,就可以直接返回結(jié)果給客戶端,不必等待最后的ACK。

宕機(jī)問題

宕機(jī)問題都有可能轉(zhuǎn)化為超時問題,宕機(jī)前如果寫了redo/undo日志,重啟后需要額外處理。

  1. 協(xié)調(diào)者發(fā)出Prepare請求前宕機(jī)。此時事務(wù)還未開始,不會有影響。
  2. 參與者在Prepare階段宕機(jī)。此時協(xié)調(diào)者超時,處理方式和上文網(wǎng)絡(luò)超時第一條相同。參與者重啟后需要向協(xié)調(diào)者查詢事務(wù)狀態(tài)。
  3. 在協(xié)調(diào)者寫Commit/Abort日志前,協(xié)調(diào)者宕機(jī)。協(xié)調(diào)者重啟后,進(jìn)入Prepare超時處理流程,處理方式和上文網(wǎng)絡(luò)超時第一條相同。
  4. 在協(xié)調(diào)者寫Commit/Abort日志后,協(xié)調(diào)者宕機(jī)。協(xié)調(diào)者重啟后,需要給參與者發(fā)送日志中決定的Commit/Abort請求。
  5. 在協(xié)調(diào)者寫Commit/Abort日志后,參與者宕機(jī)。此時參與者在重啟后需要向協(xié)調(diào)者查詢事務(wù)狀態(tài),執(zhí)行對應(yīng)的操作。

通過上面的討論,我們可以看到由于事務(wù)狀態(tài)分布在協(xié)調(diào)者和各個參與者之間,要保證兩階段提交的一致性是非常困難的。如果能夠保證協(xié)調(diào)者的可用性(Availablity),比如采用主備或者Paxos來實現(xiàn),同時還需要保證各個參與者宕機(jī)后能夠重啟恢復(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)者或者參與者宕機(jī),必須等待超時或者服務(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),隔離性可以通過鎖或者M(jìn)VCC實現(xiàn),但是這里的鎖和MVCC不是單機(jī)下的,而是分布式鎖和分布式MVCC,實現(xiàn)起來并不容易。

XA模型嚴(yán)格保障事務(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ù)最終會達(dá)到一個一致的狀態(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負(fù)責(zé)給A減100塊錢,PB負(fù)責(zé)給B加100塊錢。TCC模型流程如下:

Try階段

  • PA檢查A賬戶是否有足夠的余額,凍結(jié)A賬戶的100塊,寫日志到磁盤。這個階段不需要鎖住A賬戶,其他事務(wù)可以對A賬戶操作,可以看到這100塊,但是不能對這100塊錢操作。
  • PB檢查B賬戶的合法性。

如果PA和PB在Try階段都返回成功,進(jìn)入Confirm階段

  • PA減A賬戶的100塊,寫日志到磁盤。
  • PB加100塊到B賬戶,寫日志到磁盤。

如果PA或者PB在Try階段返回失敗,進(jìn)入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ù)器宕機(jī)等問題。

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ā)出貨物準(zhǔn)備完成事件ORDER_PREPARED_EVENT。
  4. 送貨服務(wù)訂閱ORDER_PREPARED_EVENT,送貨,發(fā)出貨物交付事件ORDER_DELIVERED_EVENT。
  5. 訂單服務(wù)訂閱ORDER_DELIVERED_EVENT,標(biāo)記事務(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,標(biāo)記訂單失敗。

基于命令的協(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ā)送準(zhǔn)備訂單命令給庫存服務(wù),庫存服務(wù)扣除庫存,返回結(jié)果給協(xié)調(diào)者。
  4. 協(xié)調(diào)者發(fā)送送貨命令給送貨服務(wù),送貨服務(wù)送貨,返回結(jié)果給協(xié)調(diào)者。
  5. 協(xié)調(diào)者發(fā)送結(jié)果給訂單服務(wù),標(biāo)記事務(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ù),標(biāo)記事務(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ù)消息,標(biāo)記事務(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容