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