
最近在研究分布式數(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ù)庫的讀/寫操作。包含有以下兩個目的:
為數(shù)據(jù)庫操作序列提供了一個從失敗中恢復(fù)到正常狀態(tài)的方法,同時提供了數(shù)據(jù)庫即使在異常狀態(tài)下仍能保持一致性的方法。
當(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,格式如下:

每個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é),是一個遞增的值,表示當(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ì)展開。

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日志組成了一個鏈表:

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

- 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ù)庫有四種隔離級別,由低到高如下:
- 未提交讀(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é)合起來,就是嚴(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ù)是否可見:
- 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID和creator_trx_id相同,說明是當(dāng)前事務(wù)修改的記錄,此時該版本數(shù)據(jù)可見。
- 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID小于min_trx_id,說明是已經(jīng)提交的事務(wù)作的修改,該版本數(shù)據(jù)可見。
- 如果當(dāng)前版本數(shù)據(jù)的事務(wù)ID大于等于max_trx_id,說明是該版本的事務(wù)是在read view創(chuàng)建之后生成的,該版本數(shù)據(jù)不可見
- 如果當(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)備階段:
- 分配事務(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ī)則等待合適的時機(jī)寫入磁盤
恢復(fù)或者回滾階段:
- 根據(jù)redo日志中的LSN和事務(wù)ID,數(shù)據(jù)文件中的LSN和binlog中的事務(wù)ID決定需要做恢復(fù)還是回滾
- 如果事務(wù)已提交,但數(shù)據(jù)頁還未寫入磁盤,需要恢復(fù),根據(jù)redo日志中的字段對數(shù)據(jù)頁進(jìn)行恢復(fù)
- 如果需要回滾,根據(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
協(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ù)器宕機(jī)和網(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)者宕機(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ù)。
協(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日志,重啟后需要額外處理。
- 協(xié)調(diào)者發(fā)出Prepare請求前宕機(jī)。此時事務(wù)還未開始,不會有影響。
- 參與者在Prepare階段宕機(jī)。此時協(xié)調(diào)者超時,處理方式和上文網(wǎng)絡(luò)超時第一條相同。參與者重啟后需要向協(xié)調(diào)者查詢事務(wù)狀態(tài)。
- 在協(xié)調(diào)者寫Commit/Abort日志前,協(xié)調(diào)者宕機(jī)。協(xié)調(diào)者重啟后,進(jìn)入Prepare超時處理流程,處理方式和上文網(wǎng)絡(luò)超時第一條相同。
- 在協(xié)調(diào)者寫Commit/Abort日志后,協(xié)調(diào)者宕機(jī)。協(xié)調(diào)者重啟后,需要給參與者發(fā)送日志中決定的Commit/Abort請求。
- 在協(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
- 協(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),隔離性可以通過鎖或者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)組成。
基于事件的分布式方案的成功流程如下:

- 訂單服務(wù)創(chuàng)建訂單,發(fā)出訂單創(chuàng)建事件ORDER_CREATED_EVENT。
- 支付服務(wù)訂閱ORDER_CREATED_EVENT,執(zhí)行支付操作,發(fā)出支付完成事件BILLED_ORDER_EVENT。
- 庫存服務(wù)訂閱BILLED_ORDER_EVENT,扣除庫存,發(fā)出貨物準(zhǔn)備完成事件ORDER_PREPARED_EVENT。
- 送貨服務(wù)訂閱ORDER_PREPARED_EVENT,送貨,發(fā)出貨物交付事件ORDER_DELIVERED_EVENT。
- 訂單服務(wù)訂閱ORDER_DELIVERED_EVENT,標(biāo)記事務(wù)完成。
基于事件的分布式方案的失敗流程如下:

庫存服務(wù)發(fā)現(xiàn)庫存不足,發(fā)出庫存不足事件PRODUCT_OUT_OF_STOCK_EVENT。
支付服務(wù)訂閱PRODUCT_OUT_OF_STOCK_EVENT,給用戶返回錢。
訂單服務(wù)訂閱PRODUCT_OUT_OF_STOCK_EVENT,標(biāo)記訂單失敗。
基于命令的協(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ā)送準(zhǔn)備訂單命令給庫存服務(wù),庫存服務(wù)扣除庫存,返回結(jié)果給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送送貨命令給送貨服務(wù),送貨服務(wù)送貨,返回結(jié)果給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送結(jié)果給訂單服務(wù),標(biāo)記事務(wù)完成。
基于命令的協(xié)調(diào)者方案的失敗流程如下:

- 庫存服務(wù)發(fā)現(xiàn)庫存不足,返回庫存不足結(jié)果給協(xié)調(diào)者。
- 協(xié)調(diào)者發(fā)送返錢命令給支付服務(wù),支付服務(wù)返錢給用戶。
- 協(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.
- 事務(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ù)消息,標(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)
支持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