分布式系統(tǒng)中的分布式事務(wù)、原子性、兩階段鎖與兩階段提交
ACID 確保事務(wù)的正確性
事務(wù)(Transaction)是并發(fā)控制和原子提交的抽象,它將一系列操作(可能在數(shù)據(jù)庫中的不同記錄上)視為一個單一的單元,并且不受故障或來自其他活動觀察的影響。事務(wù)處理系統(tǒng)要求程序員標(biāo)記操作序列的開始和結(jié)束。
數(shù)據(jù)庫通常采用 ACID 原則來確保事務(wù)的正確性:
- 原子性(Atomicity - A)
要么全部完成,要么全部不完成 。即使發(fā)生故障,也不會出現(xiàn)部分更新完成而部分更新未完成的情況,它是“全有或全無”(all-or-nothing)的。原子性旨在掩蓋程序執(zhí)行過程中發(fā)生的故障。
- 一致性(Consistency - C)
一致性通常指數(shù)據(jù)庫將強(qiáng)制執(zhí)行由應(yīng)用程序聲明的特定不變性(invariants)。
如果我們在銀行進(jìn)行轉(zhuǎn)賬,那么一個重要的不變性是 銀行的總金額不應(yīng)改變 。也就是說,即使錢從一個賬戶轉(zhuǎn)移到另一個賬戶,銀行所有賬戶的總和也應(yīng)該保持不變。
“一致性”屬性確保在事務(wù)開始和結(jié)束時,數(shù)據(jù)庫的狀態(tài)都符合預(yù)定義的規(guī)則或約束。例如,如果一個轉(zhuǎn)賬事務(wù)成功完成,那么在扣除和增加金額之后,總資金量應(yīng)仍然保持一致,否則事務(wù)將被視為無效并回滾。
- 隔離性(Isolation - I)
隔離性是指并發(fā)執(zhí)行的事務(wù)是否能夠看到彼此的中間更改。目標(biāo)是“不能”。從技術(shù)上講,隔離性意味著事務(wù)的執(zhí)行是 可串行化(serializable)的 。
可串行化(Serializable)的含義: 如果一組事務(wù)并發(fā)執(zhí)行并產(chǎn)生結(jié)果(包括新的數(shù)據(jù)庫記錄和任何輸出),那么這些結(jié)果是可串行化的, 當(dāng)且僅當(dāng)存在這些相同事務(wù)的某種串行執(zhí)行順序(即一次執(zhí)行一個,不并行)能夠產(chǎn)生與實際執(zhí)行相同的結(jié)果。 這意味著,即使事務(wù)是并發(fā)執(zhí)行的,它們最終的效果也如同按某種順序逐個執(zhí)行一樣。
- 兩階段鎖定(Two-Phase Locking - 2PL) 是一種常用的并發(fā)控制機(jī)制,通過要求事務(wù)在操作數(shù)據(jù)前獲取鎖,并在事務(wù)提交或中止前一直持有鎖,來強(qiáng)制實現(xiàn)隔離性或可串行化。兩階段鎖定的“兩個階段”是指事務(wù)在 獲取鎖 和 釋放鎖 行為上的兩個獨立階段(后文會詳細(xì)探討)。
- 持久性(Durability - D)
持久性意味著在事務(wù)提交后(即客戶端收到數(shù)據(jù)庫已執(zhí)行事務(wù)的回復(fù)后), 事務(wù)對數(shù)據(jù)庫的修改將是持久的,不會因任何形式的故障而被擦除 。
這通常意味著數(shù)據(jù)必須寫入 非易失性存儲 ,例如磁盤。
- 日志(Logging) 是實現(xiàn)持久性的一種重要技術(shù)。系統(tǒng)會在將更改寫入實際數(shù)據(jù)庫之前,先將更改記錄到一個 日志 中。即使系統(tǒng)在將更改“安裝”(install)到主存儲之前崩潰,恢復(fù)程序也可以使用日志來重做這些更改,確保數(shù)據(jù)最終的持久性。
一切或一無所有,這就是原子性
想象一下你在進(jìn)行一次銀行轉(zhuǎn)賬:從你的賬戶 A 轉(zhuǎn) 100 元到朋友的賬戶 B。這個操作至少包含兩個步驟:
- 從賬戶 A 扣除 100 元。
- 給賬戶 B 增加 100 元。
現(xiàn)在,設(shè)想一個最糟糕的情況:系統(tǒng)在完成第一步后突然崩潰了。你的錢被扣了,但你朋友沒收到。這顯然是不可接受的。
為了解決這類問題,我們需要一個保證: 這一系列操作要么全部成功,要么就像從未發(fā)生過一樣 。這就是 原子性 (atomicity) 的核心思想,它也是我們今天要討論的 事務(wù) (transaction) 最重要的特性之一。一個事務(wù)就是一組操作,它被設(shè)計為在面對并發(fā)和故障時,表現(xiàn)得像一個單一的、不可分割的單元。
并發(fā)控制與可串行化:承諾「先到后到」
在現(xiàn)實世界中,系統(tǒng)很少一次只處理一個請求。銀行的系統(tǒng)可能同時在處理成千上萬筆轉(zhuǎn)賬和查詢。當(dāng)多個事務(wù)并發(fā)執(zhí)行,并且它們試圖讀寫相同的數(shù)據(jù)時,新的問題就出現(xiàn)了。
比如,在你的轉(zhuǎn)賬事務(wù) (T1) 正在進(jìn)行的同時,另一個事務(wù) (T2) 正在做全行審計,計算所有賬戶的總金額。如果 T2 在 T1 從賬戶 A 扣款后、向賬戶 B 加款前讀取了 A 和 B 的余額,那么它會發(fā)現(xiàn)總金額少了 100 元,從而引發(fā)錯誤的警報。
為了防止這種混亂,我們需要另一種保證,稱為 先后原子性 (before-or-after atomicity) 。它的意思是,并發(fā)事務(wù)的執(zhí)行結(jié)果,必須和它們按照 某個 串行順序(一個接一個)執(zhí)行的結(jié)果完全一樣。這個屬性也叫做 可串行化 (serializability) 。
至于究竟是哪個串行順序,我們通常不關(guān)心。只要最終結(jié)果是 T1; T2 或者 T2; T1 兩種串行順序之一的結(jié)果即可。這種保證讓我們可以在享受并發(fā)帶來的高性能的同時,不必?fù)?dān)心事務(wù)之間互相干擾,產(chǎn)生意想不到的錯誤結(jié)果。
兩階段鎖定
如何實現(xiàn)可串行化呢?一種常見的策略是 悲觀并發(fā)控制 (pessimistic concurrency control) 。它的核心思想是“先申請再使用”,它假設(shè)沖突很可能會發(fā)生,因此通過鎖定機(jī)制來阻止?jié)撛诘臎_突。
最著名的悲觀鎖協(xié)議就是 兩階段鎖定 (two-phase locking, 簡稱 2PL) 。注意,它的名字和我們稍后要講的“兩階段提交”很像,但它們是完全不同的兩個概念。
2PL 的規(guī)則很簡單:
- 擴(kuò)展階段 (Phase 1) :事務(wù)可以根據(jù)需要獲取鎖,但不能釋放任何鎖。
- 收縮階段 (Phase 2) :一旦事務(wù)釋放了第一個鎖,它就進(jìn)入收縮階段,此后只能釋放鎖,不能再獲取任何新的鎖。
在實踐中,一種更嚴(yán)格也更常見的變體叫“強(qiáng)嚴(yán)格兩階段鎖定”,它要求事務(wù)必須持有所有鎖,直到事務(wù)結(jié)束(提交或中止)后才能一次性釋放。
為什么鎖必須持有到事務(wù)結(jié)束?
這是一個核心問題。想象一下,如果事務(wù) T1 修改了數(shù)據(jù) x,然后立即釋放了對 x 的鎖。此時,事務(wù) T2 讀取了 x 的新值并提交。但隨后,T1 因為某種原因決定 中止 (abort) ,它會撤銷自己對 x 的修改。這時,T2 的計算結(jié)果就建立在一個“從未存在過”的數(shù)據(jù)之上,破壞了系統(tǒng)的一致性。持有鎖直到事務(wù)最終狀態(tài)(提交或中止)確定,就是為了防止這種“臟讀”問題。
兩階段鎖定會產(chǎn)生死鎖嗎?
會的 。這是一個經(jīng)典的場景:事務(wù) T1 鎖定了資源 A,然后嘗試獲取資源 B 的鎖;同時,事務(wù) T2 鎖定了資源 B,并嘗試獲取資源 A 的鎖。兩者將永遠(yuǎn)地等待對方,形成 死鎖 (deadlock) 。數(shù)據(jù)庫系統(tǒng)通常有專門的機(jī)制來處理這種情況,比如通過超時或者檢測等待圖中的循環(huán)來發(fā)現(xiàn)死鎖,然后強(qiáng)制中止其中一個事務(wù)來打破僵局。
鎖是排他的,還是允許多個讀者?
為了簡化討論,我們常假設(shè)鎖是 排他鎖 (exclusive locks) 。但在實際系統(tǒng)中,為了提高性能,通常會區(qū)分 共享鎖 (shared locks) 和排他鎖。多個事務(wù)可以同時持有同一個數(shù)據(jù)的共享鎖(用于讀取),但只要有一個事務(wù)想寫入,它就必須獲取排他鎖,并且此時不能有任何其他事務(wù)持有該數(shù)據(jù)的任何鎖(無論是共享還是排他)。
樂觀與悲觀之爭
與悲觀鎖“先問后走”的策略相反,還有一種 樂觀并發(fā)控制 (optimistic concurrency control) 。它的哲學(xué)是“先走再說,不行再道歉”。
在這種模型下,事務(wù)執(zhí)行時不會加鎖,它們自由地讀取數(shù)據(jù),并將修改寫入一個私有工作區(qū)。直到事務(wù)準(zhǔn)備提交時,系統(tǒng)才會進(jìn)行沖突檢查,看看在它執(zhí)行期間,它讀取的數(shù)據(jù)是否被其他已提交的事務(wù)修改過。如果沒有沖突,就提交;如果發(fā)現(xiàn)沖突,那么這個事務(wù)就必須中止并重試。
如何在悲觀和樂觀并發(fā)控制之間選擇?
這取決于你的應(yīng)用場景中沖突發(fā)生的頻率。
- 如果事務(wù)之間沖突非常頻繁(比如很多用戶搶購?fù)患唐罚?nbsp;悲觀鎖 更合適。雖然它可能會因為等待鎖而降低并發(fā)度,但它避免了大量事務(wù)因沖突而中止重試所帶來的無效工作。
- 如果事務(wù)之間沖突很少(比如用戶大多在修改自己的個人資料), 樂觀鎖 更優(yōu)。它省去了加鎖和解鎖的開銷,允許更高的并發(fā),只有在極少數(shù)發(fā)生沖突時才付出中止重日志回滾的代價。
樂觀鎖與悲觀鎖在 MySQL 中的實現(xiàn)
在 MySQL 中,這兩種鎖更多的是一種設(shè)計思想的體現(xiàn),而不是兩種有明確開關(guān)的獨立功能。它們通過不同的 SQL 命令和表結(jié)構(gòu)設(shè)計來實現(xiàn)。
悲觀鎖 (Pessimistic Locking)
悲觀鎖的實現(xiàn),完全依賴于數(shù)據(jù)庫提供的原生鎖機(jī)制。在 MySQL (主要指 InnoDB 存儲引擎) 中,當(dāng)你執(zhí)行特定的 SELECT 語句時,就可以顯式地為數(shù)據(jù)行加上悲觀鎖。
主要有兩種方式:
- 共享鎖 (Shared Lock)
SELECT ... LOCK IN SHARE MODE;
這條語句會為你查詢的行加上一個共享鎖。其他事務(wù)可以讀取這些行(也可以加共享鎖),但不能修改它們,直到你的事務(wù)提交或回滾。這允許多個“讀者”同時存在,但會阻塞“寫者”。
- 排他鎖 (Exclusive Lock)
SELECT ... FOR UPDATE;
這是更強(qiáng)的鎖。它會為你查詢的行加上一個排他鎖。其他任何事務(wù)都不能再為這些行加任何鎖(無論是共享還是排他),也不能修改它們,直到你的事務(wù)結(jié)束。它同時阻塞了“讀者”和“寫者”。
當(dāng)你執(zhí)行一個普通的 UPDATE 或 DELETE 語句時,InnoDB 實際上也會自動地為涉及的行加上排他鎖,這本身就是一種悲觀鎖的體現(xiàn)。
總而言之,悲觀鎖在 MySQL 中是通過 LOCK IN SHARE MODE 和 FOR UPDATE 以及隱式的 UPDATE/DELETE 鎖來實現(xiàn)的,它利用數(shù)據(jù)庫的鎖機(jī)制來強(qiáng)制同步,保證在修改數(shù)據(jù)期間的獨占訪問。
樂觀鎖 (Optimistic Locking)
樂觀鎖則完全相反,它不依賴于數(shù)據(jù)庫的鎖機(jī)制,而是在 應(yīng)用層面 實現(xiàn)的一種并發(fā)控制策略。實現(xiàn)它的前提是,你需要在你的數(shù)據(jù)表中增加一個額外的列,通常是 version (版本號) 或者 timestamp (時間戳)。
實現(xiàn)步驟如下:
- 增加版本列
在你的表中增加一個 version 列,通常是整型,默認(rèn)值為 0 或 1。
ALTER TABLE products ADD COLUMN version INT NOT NULL DEFAULT 1;
- 讀取數(shù)據(jù)時包含版本號
當(dāng)你的應(yīng)用程序需要修改一條數(shù)據(jù)時,你首先將這條數(shù)據(jù)連同它的 version 值一起讀出來。
SELECT id, name, stock, version FROM products WHERE id = 101;
假設(shè)讀出的 stock 是 50,version 是 2。
- 更新數(shù)據(jù)時校驗并更新版本號
當(dāng)你準(zhǔn)備將修改寫回數(shù)據(jù)庫時,你的 UPDATE 語句必須同時滿足兩個條件:id 匹配,并且 version 也要匹配你當(dāng)初讀出來的值。如果更新成功,則同時將 version 加一。
UPDATE products
SET stock = 49, version = version + 1
WHERE id = 101 AND version = 2;
工作原理
- 如果這條 UPDATE 語句成功執(zhí)行,并且影響的行數(shù)為 1,說明在你讀取數(shù)據(jù)到寫入數(shù)據(jù)的這段時間內(nèi),沒有其他事務(wù)修改過這條數(shù)據(jù)。更新成功!
- 如果影響的行數(shù)為 0,則說明在你操作的這段時間里,有另一個事務(wù)已經(jīng)修改了這條數(shù)據(jù),并增加了 version 的值。你手里的 version = 2 已經(jīng)過時了。此時,更新失敗。你的應(yīng)用程序需要捕獲這個“失敗”,然后通常會重新讀取最新的數(shù)據(jù),再嘗試一遍修改流程,或者提示用戶操作沖突。
總結(jié)來說,樂觀鎖在 MySQL 中是通過在表中增加版本字段,并在 UPDATE 時利用 WHERE 子句進(jìn)行版本校驗來實現(xiàn)的。它將并發(fā)控制的責(zé)任從數(shù)據(jù)庫轉(zhuǎn)移到了應(yīng)用程序。
分布式兩階段提交
當(dāng)一個事務(wù)需要修改分布在不同服務(wù)器上的數(shù)據(jù)時,問題變得更加復(fù)雜。比如,一個跨行轉(zhuǎn)賬事務(wù),既要操作 A 銀行的數(shù)據(jù)庫,也要操作 B 銀行的數(shù)據(jù)庫。我們?nèi)绾伪WC這個分布式事務(wù)的原子性?
這就是 兩階段提交 (two-phase commit, 簡稱 2PC) 大顯身手的地方。2PC 引入了兩個角色:一個 協(xié)調(diào)者 (coordinator) 和多個 參與者 (participants) (或稱為 workers)。
會有多個事務(wù)同時活躍嗎?參與者如何知道消息屬于哪個事務(wù)?
是的,系統(tǒng)中可以同時有許多活躍的分布式事務(wù)。為了區(qū)分它們,協(xié)調(diào)者發(fā)起的每個事務(wù)都會帶有一個全局唯一的 事務(wù) ID (transaction ID) 。所有在參與者之間傳遞的消息都會包含這個 ID,這樣每個參與者就知道自己是在為哪個事務(wù)工作。
2PC 的流程如下:
階段一:投票階段 (Voting Phase)
- 準(zhǔn)備 (Prepare) :協(xié)調(diào)者向所有參與者發(fā)送一個 PREPARE 消息,詢問“你們是否準(zhǔn)備好提交?”。
- 投票 (Vote) :
- 每個參與者收到 PREPARE 消息后,會檢查自己是否能完成任務(wù)。如果可以,它會將所有需要的數(shù)據(jù)和操作記錄到持久化的 日志 (log) 中,確保即使現(xiàn)在崩潰,重啟后也能完成提交。然后,它向協(xié)調(diào)者回復(fù) PREPARED 消息。一旦發(fā)送了 PREPARED,參與者就進(jìn)入了“準(zhǔn)備就緒”狀態(tài),它放棄了單方面中止的權(quán)利,只能等待協(xié)調(diào)者的最終指令。
- 如果參與者因為任何原因無法完成任務(wù),它會直接回復(fù) ABORT 消息。
參與者為何會發(fā)送 ABORT 而不是 PREPARED?
有多種可能的原因:
- 本地約束沖突 :例如,事務(wù)試圖插入一個重復(fù)的主鍵,而表定義了主鍵唯一性約束。
- 死鎖 :參與者可能卷入了一個本地的鎖死鎖,為了打破死鎖,它必須中止當(dāng)前事務(wù)。
- 崩潰恢復(fù) :參與者可能在收到 PREPARE 之前就已經(jīng)崩潰并重啟,導(dǎo)致它丟失了為該事務(wù)所做的臨時修改和持有的鎖,因此無法保證能完成提交。
階段二:決定階段 (Decision Phase)
- 做決定:協(xié)調(diào)者收集所有參與者的投票。
- 如果 所有 參與者都回復(fù)了 PREPARED,協(xié)調(diào)者就決定 提交 (commit) 整個事務(wù)。
- 如果 任何一個 參與者回復(fù)了 ABORT,或者在超時時間內(nèi)沒有響應(yīng),協(xié)調(diào)者就決定 中止 (abort) 整個事務(wù)。
- 通知結(jié)果 :協(xié)調(diào)者將最終決定(COMMIT 或 ABORT)廣播給所有參與者。參與者收到后,執(zhí)行相應(yīng)的操作(正式提交或回滾),然后釋放資源。
2PC 系統(tǒng)如何撤銷修改?
關(guān)鍵在于日志。在準(zhǔn)備階段,參與者不僅僅記錄了要“做什么” (redo 信息),也記錄了如何“撤銷” (undo 信息)。如果最終決定是中止,參與者就會根據(jù)日志中的 undo 記錄,執(zhí)行反向操作,將數(shù)據(jù)恢復(fù)到事務(wù)開始前的狀態(tài)。
2PC 的挑戰(zhàn)與替代方案
2PC 雖然經(jīng)典,但有一個致命弱點: 阻塞問題 。
如果協(xié)調(diào)者崩潰了,參與者該怎么辦?
這是 2PC 最大的問題。如果一個參與者已經(jīng)發(fā)送了 PREPARED 消息,然后協(xié)調(diào)者崩潰了,這個參與者就完全不知道該提交還是中止。它不能自己做決定,因為其他參與者可能投了反對票。因此,它只能 無限期地等待 ,并持有事務(wù)期間獲得的鎖,這會阻塞其他需要這些資源的事務(wù),直到協(xié)調(diào)者恢復(fù)。
為什么不用三階段提交 (3PC)?
3PC 確實是為了解決 2PC 的阻塞問題而設(shè)計的,它在準(zhǔn)備和提交之間增加了一個“預(yù)提交”階段。理論上,這允許在協(xié)調(diào)者崩潰后,存活的參與者們可以互相通信并達(dá)成一個一致的決定。然而,3PC 協(xié)議更復(fù)雜,通信開銷更大,并且在面對網(wǎng)絡(luò)分區(qū)(一部分參與者無法與另一部分通信)時仍然可能阻塞。在工程實踐中,許多系統(tǒng)認(rèn)為這種復(fù)雜性帶來的收益有限,因此 2PC 仍然是更主流的選擇。
可以用 Raft 替代 2PC 嗎?
不行,它們解決的是不同的問題。
- 2PC 是用來協(xié)調(diào)多個節(jié)點執(zhí)行 不同但相關(guān) 的操作,并保證這些操作的原子性(要么都做,要么都不做)。它通常要求所有參與者都存活才能做出進(jìn)展。
- Raft 是一種共識算法,用于讓一組節(jié)點(副本)就 同一個值或同一個操作序列 達(dá)成一致,從而實現(xiàn)一個高可用的狀態(tài)機(jī)。Raft 只需要大多數(shù)節(jié)點存活即可工作。
實戰(zhàn)中的兩階段提交 —— 以 MySQL 為例
你可能覺得分布式事務(wù)離我們很遙遠(yuǎn),但實際上,像 MySQL 這樣的常用數(shù)據(jù)庫內(nèi)部就在使用 2PC 的思想來解決一致性問題。
背景:MySQL 的主從復(fù)制
在生產(chǎn)環(huán)境中,MySQL 常常采用主從(Primary-Replica)或主備(Primary-Secondary)架構(gòu)。所有寫操作在主庫上進(jìn)行,然后通過一種叫做 二進(jìn)制日志 (binary log, binlog) 的文件記錄下來。從庫會讀取主庫的 binlog,并在自己身上重放這些操作,從而與主庫保持?jǐn)?shù)據(jù)同步。
同時,MySQL 的 InnoDB 存儲引擎自身也有一套用于崩潰恢復(fù)的日志系統(tǒng),叫做 重做日志 (redo log) 。
問題:redo log 和 binlog 的一致性
現(xiàn)在問題來了:一次事務(wù)提交,既要寫 redo log(為了保證 InnoDB 自身崩潰后能恢復(fù)),也要寫 binlog(為了讓從庫能同步)。這兩次寫操作必須是原子的。
想象一下,如果先寫了 redo log,事務(wù)在主庫上生效了,但還沒來得及寫 binlog,主庫就崩潰了。主庫重啟后,通過 redo log 恢復(fù)了數(shù)據(jù),但 binlog 里沒有這次的修改記錄,導(dǎo)致所有從庫都丟失了這次更新,數(shù)據(jù)就不一致了。
反之,如果先寫了 binlog,但還沒寫 redo log 就崩潰了。主庫重啟后,通過 redo log 回滾了未完成的事務(wù),數(shù)據(jù)被撤銷了。但 binlog 里卻有這次的記錄,從庫會執(zhí)行這次更新,數(shù)據(jù)同樣不一致。
解決方案:內(nèi)部的兩階段提交
為了解決這個問題,MySQL 巧妙地在數(shù)據(jù)庫服務(wù)器內(nèi)部實現(xiàn)了一個兩階段提交。在這里:
- 協(xié)調(diào)者 :是 MySQL 服務(wù)器本身。
- 參與者 :主要是 InnoDB 存儲引擎。
當(dāng)客戶端執(zhí)行 COMMIT 時,流程如下:
- 階段一:準(zhǔn)備 (Prepare)
- MySQL 服務(wù)器通知 InnoDB:“準(zhǔn)備提交事務(wù)”。
- InnoDB 寫入 redo log,并將這個事務(wù)標(biāo)記為 prepared 狀態(tài)。注意,此時事務(wù)并未真正提交,只是處于可以被提交的狀態(tài)。
- 階段二:提交 (Commit)
- 如果 InnoDB prepare 成功,MySQL 服務(wù)器就會將該事務(wù)寫入 binlog 。
- 寫完 binlog 后,MySQL 服務(wù)器再通知 InnoDB:“正式提交事務(wù)”。
- InnoDB 收到指令后,將 redo log 中該事務(wù)的狀態(tài)從 prepared 修改為 committed 。提交完成。
XID 的作用與崩潰恢復(fù)
在這個過程中,一個關(guān)鍵的東西是 事務(wù) ID (XID) 。它會被同時寫入 redo log 和 binlog,作為兩者關(guān)聯(lián)的憑證。
如果系統(tǒng)在寫完 binlog 后、InnoDB 最終提交前崩潰,重啟時 MySQL 會這樣做:
- 它會掃描最后的 binlog 文件,找出其中已經(jīng)包含的事務(wù) XID。
- 然后去檢查 InnoDB redo log 中處于 prepared 狀態(tài)的事務(wù)。
- 如果一個 prepared 狀態(tài)的事務(wù),其 XID 存在于 binlog 中,說明協(xié)調(diào)者(MySQL Server)在崩潰前已經(jīng)做出了“提交”的決定(因為 binlog 已經(jīng)寫入),那么就命令 InnoDB 提交這個事務(wù)。
- 如果一個 prepared 狀態(tài)的事務(wù),其 XID 不 存在于 binlog 中,說明協(xié)調(diào)者在崩潰前還沒來得及做決定,那么就命令 InnoDB 回滾這個事務(wù)。
通過這種方式,MySQL 保證了 redo log 和 binlog 之間的數(shù)據(jù)一致性,從而確保了整個主從復(fù)制架構(gòu)的可靠性。
先寫 redo log 還是 binlog?
答案是: 先寫 redo log (prepare 階段),再寫 binlog 。
這個順序至關(guān)重要,是保證數(shù)據(jù)一致性的核心。讓我們再回顧一下不這么做的后果:
- 如果先寫 binlog,后寫 redo log
- binlog 寫入成功。(此時從庫已經(jīng)可以看到這個修改,并準(zhǔn)備同步)
- 數(shù)據(jù)庫 崩潰 。
- redo log 還沒來得及寫。
- 重啟后 MySQL 通過 redo log 進(jìn)行崩潰恢復(fù),發(fā)現(xiàn)這個事務(wù)沒有完成 prepare 和 commit,于是 回滾 了它。
- 結(jié)果主庫數(shù)據(jù)被回滾,但從庫執(zhí)行了 binlog 中的操作。 主從數(shù)據(jù)不一致 。
- 正確的順序:先寫 redo log (prepare),后寫 binlog
- redo log 寫入成功,狀態(tài)為 prepared。
- 數(shù)據(jù)庫 崩潰 。
- binlog 還沒來得及寫。
- 重啟后 :MySQL 進(jìn)行恢復(fù)。它發(fā)現(xiàn) redo log 中有一個 prepared 狀態(tài)的事務(wù),然后它會去 binlog 中查找對應(yīng)的事務(wù) ID (XID)。
- 決策 :因為它在 binlog 中 找不到 這個事務(wù)的記錄,MySQL 就知道這個事務(wù)在崩潰前并沒有被分發(fā)給從庫,于是決定 回滾 它。
- 結(jié)果 :主庫回滾了事務(wù),binlog 里也沒有這個事務(wù),從庫自然也不會執(zhí)行。 主從數(shù)據(jù)保持一致 。
只有當(dāng) binlog 也成功寫入后,整個事務(wù)才被認(rèn)為是“可以安全提交的”。這時即使在最終 commit redo log 之前崩潰,恢復(fù)時也會因為在 binlog 中能找到記錄而決定提交事務(wù),最終依然能保證主從一致性。