CMU15-445 數(shù)據(jù)庫系統(tǒng)播客:深入解析數(shù)據(jù)庫并發(fā)控制 - 兩階段鎖(2PL)
在構(gòu)建任何多用戶應(yīng)用程序時(shí),數(shù)據(jù)庫的并發(fā)控制都是一個(gè)無法回避的核心問題。我們?nèi)绾卧试S多個(gè)用戶同時(shí)讀寫數(shù)據(jù),同時(shí)又能保證數(shù)據(jù)的完整性和一致性?這正是數(shù)據(jù)庫事務(wù)隔離性(Isolation)所要解決的難題。
本文將深入探討實(shí)現(xiàn)隔離性的經(jīng)典協(xié)議—— 兩階段鎖(Two-Phase Locking, 2PL) 。我們將從最基本的問題出發(fā),層層遞進(jìn),揭示2PL的工作原理、它面臨的挑戰(zhàn)以及在現(xiàn)代數(shù)據(jù)庫系統(tǒng)(如 MySQL 和 PostgreSQL)中的實(shí)際應(yīng)用。
問題的起點(diǎn):為什么不能“隨用隨放”鎖?
一個(gè)很自然的想法是:當(dāng)一個(gè)事務(wù)需要訪問某個(gè)數(shù)據(jù)時(shí),就給它加上鎖;用完之后,立刻釋放鎖,讓其他事務(wù)可以繼續(xù)使用。這種策略看似高效,但卻隱藏著巨大的風(fēng)險(xiǎn)。
讓我們看一個(gè)經(jīng)典的例子—— 不可重復(fù)讀(Unrepeatable Read) :
- 事務(wù) T1 讀取賬戶 A 的余額為 100 元,并對(duì)其加上鎖。
- T1 釋放了對(duì) A 的鎖。
- 此時(shí),事務(wù) T2 介入,它也讀取賬戶 A 的余額,并將其修改為 200 元后提交。
- 隨后,T1 因?yàn)闃I(yè)務(wù)需要, 再次讀取 賬戶 A 的余額,卻發(fā)現(xiàn)余額變成了 200 元。
對(duì)于 T1 而言,在同一個(gè)事務(wù)中兩次讀取同一數(shù)據(jù),得到的結(jié)果卻不一致。這破壞了事務(wù)的隔離性,可能導(dǎo)致嚴(yán)重的業(yè)務(wù)邏輯錯(cuò)誤。根本原因在于,我們 無法預(yù)知一個(gè)事務(wù)未來的所有操作 。T1 在釋放鎖時(shí),并不知道自己未來是否還會(huì)再次訪問該數(shù)據(jù)。
為了在這種“未來未知”的動(dòng)態(tài)場景下保證隔離性,我們需要一個(gè)更嚴(yán)謹(jǐn)?shù)牟l(fā)控制協(xié)議。兩階段鎖(2PL)應(yīng)運(yùn)而生,它是一種經(jīng)典的 悲觀并發(fā)控制 方法,其核心思想是“先鎖定,再操作”。
兩階段鎖(2PL)的核心思想
2PL 協(xié)議非常直觀,它規(guī)定了事務(wù)在獲取和釋放鎖時(shí)必須遵守的一個(gè)簡單規(guī)則。在了解規(guī)則之前,我們先明確鎖的類型和對(duì)象。
鎖的類型
2PL 主要使用兩種基本鎖:
- 共享鎖(Shared Lock, S-LOCK) :用于 讀 操作。多個(gè)事務(wù)可以同時(shí)持有同一個(gè)數(shù)據(jù)對(duì)象的共享鎖。可以把它理解為“大家可以一起讀”。
- 排他鎖(Exclusive Lock, X-LOCK) :用于 寫 操作。一旦某個(gè)事務(wù)持有了數(shù)據(jù)對(duì)象的排他鎖,其他任何事務(wù)都不能再對(duì)該對(duì)象施加任何鎖(無論是 S 鎖還是 X 鎖)。可以理解為“我正在寫,誰都別動(dòng)”。
它們的兼容關(guān)系如下矩陣所示:
S-LOCK | X-LOCK | |
S-LOCK | 兼容 | 不兼容 |
X-LOCK | 不兼容 | 不兼容 |
兩個(gè)階段(The Two Phases)
2PL 的精髓在于,它將一個(gè)事務(wù)的生命周期嚴(yán)格劃分為兩個(gè)階段:
增長階段(Growing Phase)
- 在此階段,事務(wù)可以 不斷地請求和獲取 它所需要的鎖。
- 事務(wù)向數(shù)據(jù)庫的 鎖管理器(Lock Manager) 發(fā)送鎖請求。鎖管理器會(huì)根據(jù)鎖的兼容性矩陣來決定是立即授予鎖,還是讓事務(wù)阻塞等待。
- 在此階段,事務(wù)絕對(duì)不能釋放任何鎖。
收縮階段(Shrinking Phase)
- 當(dāng)事務(wù) 釋放了它的第一個(gè)鎖 時(shí),它就立即進(jìn)入收縮階段。
- 一旦進(jìn)入此階段,事務(wù)就只能釋放已經(jīng)持有的鎖,而不能再請求任何新的鎖。
正是這個(gè)“先增長、后收縮”的規(guī)定,保證了由 2PL 管理的事務(wù)調(diào)度是 沖突可串行化(Conflict Serializable) 的。這意味著,盡管事務(wù)在并發(fā)執(zhí)行,其最終效果等同于以某種順序串行執(zhí)行,從而保證了數(shù)據(jù)的一致性。
兩階段鎖例子
為了更具體地理解這個(gè)過程,讓我們通過一個(gè)銀行轉(zhuǎn)賬的例子來逐步分解這兩個(gè)階段,并回答 “鎖何時(shí)釋放?” 以及 “階段何時(shí)轉(zhuǎn)換?” 這兩個(gè)核心問題。
場景設(shè)定:
事務(wù) T1 需要從賬戶 A 轉(zhuǎn)賬 50 元到賬戶 B。
整個(gè)過程如下:
第一階段:增長階段(Growing Phase)
事務(wù) T1 開始執(zhí)行,它的目標(biāo)是不斷獲取它完成任務(wù)所需的所有鎖。
T1: BEGIN TRANSACTION;
- 事務(wù)開始。
T1: LOCK-X(A); // 請求對(duì) A 的排他鎖
- T1 的第一個(gè)操作是修改賬戶 A。它向鎖管理器請求對(duì) A 的排他鎖(X-Lock)。
- 鎖管理器授予該鎖。
- 此時(shí),T1 正式進(jìn)入增長階段。 它只持有 LOCK-X(A)。
T1: READ(A); WRITE(A);
- T1 讀取 A 的余額,減去 50 元,然后將新余額寫回。在整個(gè)操作期間,它都持有 A 的鎖。
T1: LOCK-X(B); // 請求對(duì) B 的排他鎖
- 接下來,T1 需要修改賬戶 B。它向鎖管理器請求對(duì) B 的排他鎖。
- 鎖管理器授予該鎖。
- T1 仍然處于增長階段,因?yàn)樗栽讷@取新的鎖,并且沒有釋放任何已有的鎖。此刻,T1 同時(shí)持有 LOCK-X(A) 和 LOCK-X(B)。
T1: READ(B); WRITE(B);
- T1 讀取 B 的余額,加上 50 元,然后將新余額寫回。
轉(zhuǎn)折點(diǎn):從增長階段到收縮階段
現(xiàn)在,T1 已經(jīng)完成了對(duì) A 和 B 的所有修改。它可以開始釋放鎖了。
T1: UNLOCK(A); // 釋放對(duì) A 的鎖
- 這是整個(gè)過程最關(guān)鍵的轉(zhuǎn)折點(diǎn)!
- 當(dāng) T1 執(zhí)行 UNLOCK(A),釋放它持有的第一個(gè)鎖時(shí):
- T1 的增長階段(Growing Phase)立即結(jié)束。
- T1 的收縮階段(Shrinking Phase)立即開始。
第二階段:收縮階段(Shrinking Phase)
一旦進(jìn)入此階段,T1 的行為將受到嚴(yán)格限制。
- 核心規(guī)則生效 :從現(xiàn)在起,T1 絕對(duì)不能再請求任何新的鎖 。如果此時(shí)業(yè)務(wù)邏輯突然要求 T1 去檢查另一個(gè)賬戶 C 的狀態(tài)(需要獲取 LOCK-S(C)),根據(jù) 2PL 協(xié)議,該請求將被拒絕,T1 必須中止。這就是 2PL 保證可串行性的核心機(jī)制。
T1: UNLOCK(B); // 釋放對(duì) B 的鎖
- T1 繼續(xù)處于收縮階段,它釋放了持有的最后一個(gè)鎖。
T1: COMMIT;
- 事務(wù)提交,所有修改永久生效。
通過這個(gè)例子,我們可以清晰地回答之前的問題:
增長階段何時(shí)結(jié)束?
- 在事務(wù)釋放其持有的第一個(gè)鎖的瞬間結(jié)束。在我們的例子中,是執(zhí)行 UNLOCK(A) 的那一刻。
收縮階段何時(shí)開始?
- 與增長階段結(jié)束是同一時(shí)刻,即釋放第一個(gè)鎖時(shí)立刻開始。
鎖何時(shí)被釋放?
- 在標(biāo)準(zhǔn)的 2PL 中,鎖可以在 收縮階段 的任何時(shí)候被釋放, 不一定需要等到事務(wù)提交 。然而,正如我們前文所討論的,這種提前釋放會(huì)導(dǎo)致“臟讀”和“級(jí)聯(lián)終止”等問題。因此,在實(shí)際系統(tǒng)中更常用的是 強(qiáng)嚴(yán)格兩階段鎖(SS2PL) ,它規(guī)定所有鎖必須持有到事務(wù)最終提交(Commit)或中止(Abort)時(shí)才能一次性釋放,這相當(dāng)于將整個(gè)收縮階段壓縮到了事務(wù)結(jié)束的最后一個(gè)點(diǎn)。
2PL 的挑戰(zhàn)與演進(jìn):從理論到實(shí)踐
雖然標(biāo)準(zhǔn)的 2PL 解決了可串行化的問題,但在實(shí)際應(yīng)用中,它仍然有兩個(gè)致命的缺陷: 級(jí)聯(lián)終止(Cascading Aborts) 和 死鎖(Deadlocks) 。
挑戰(zhàn)一:級(jí)聯(lián)終止
想象以下場景:
- 事務(wù) T1 修改了數(shù)據(jù) A,但 尚未提交 。
- 根據(jù)標(biāo)準(zhǔn) 2PL,T1 可以在不提交的情況下進(jìn)入收縮階段,釋放了對(duì) A 的 X 鎖。
- 事務(wù) T2 立即獲取了 A 的鎖,并讀取了 T1 修改后的(但未提交的)值。我們稱之為 臟讀(Dirty Read) 。
- 此時(shí),T1 由于某種原因執(zhí)行失敗,必須 中止(Abort) 并回滾其所有修改。
- 現(xiàn)在問題來了:T2 讀取了一個(gè)根本不存在的“臟”數(shù)據(jù)。為了維護(hù)數(shù)據(jù)一致性, 數(shù)據(jù)庫系統(tǒng)必須強(qiáng)制中止 T2 。如果還有 T3 讀取了 T2 的(同樣未提交的)修改,那么 T3 也必須被中止。
這種一個(gè)事務(wù)的失敗導(dǎo)致一連串相關(guān)事務(wù)被動(dòng)中止的現(xiàn)象,就是 級(jí)聯(lián)終止 。它不僅會(huì)造成大量計(jì)算資源的浪費(fèi),也讓系統(tǒng)恢復(fù)的邏輯變得異常復(fù)雜。
解決方案:嚴(yán)格與強(qiáng)嚴(yán)格 2PL
為了解決臟讀和級(jí)聯(lián)終止問題,現(xiàn)實(shí)世界的數(shù)據(jù)庫系統(tǒng)普遍采用了 2PL 的兩個(gè)增強(qiáng)版本:
- 嚴(yán)格兩階段鎖(Strict 2PL) :該協(xié)議要求事務(wù) 在提交或中止之前,不得釋放任何它持有的排他鎖(X-LOCK) 。這意味著,任何事務(wù)所做的修改在它提交之前,對(duì)其他事務(wù)都是不可見的。這就徹底杜絕了臟讀。
- 強(qiáng)嚴(yán)格兩階段鎖(Strong Strict 2PL, SS2PL) :也稱為 Rigorous 2PL,這是最常用的版本。它比 Strict 2PL 更進(jìn)一步,要求事務(wù) 在提交或中止之前,不得釋放任何它持有的鎖(包括 S-LOCK 和 X-LOCK) 。
SS2PL 實(shí)際上消除了獨(dú)立的“收縮階段” 。事務(wù)的所有鎖都在其生命周期結(jié)束時(shí)(commit 或 abort)一次性全部釋放。
SS2PL 的優(yōu)勢是巨大的:
- 完全避免級(jí)聯(lián)終止 :由于所有鎖都持有到最后,其他事務(wù)不可能讀到未提交的數(shù)據(jù)。
- 簡化恢復(fù)邏輯 :當(dāng)一個(gè)事務(wù)需要中止時(shí),DBMS 只需恢復(fù)它自己修改的原始值即可,無需擔(dān)心對(duì)其他并發(fā)事務(wù)的影響。
當(dāng)然,SS2PL 的代價(jià)是犧牲了一部分并發(fā)度(因?yàn)殒i的持有時(shí)間變長了)。但在實(shí)踐中,它帶來的安全性和簡明性遠(yuǎn)比這點(diǎn)并發(fā)度損失更重要。
挑戰(zhàn)二:死鎖(Deadlock)
死鎖是所有基于鎖的并發(fā)控制協(xié)議都可能面臨的問題。當(dāng)兩個(gè)或多個(gè)事務(wù)循環(huán)等待對(duì)方持有的鎖時(shí),死鎖就發(fā)生了。
經(jīng)典例子
- T1 持有數(shù)據(jù) A 的鎖,并請求數(shù)據(jù) B 的鎖。
- T2 持有數(shù)據(jù) B 的鎖,并請求數(shù)據(jù) A 的鎖。
此時(shí),T1 和 T2 都將無限期地等待下去,系統(tǒng)陷入停滯。
處理死鎖通常有兩種策略: 死鎖檢測 和 死鎖預(yù)防 。
策略一:死鎖檢測(Deadlock Detection)
這是一種“事后處理”的策略。
- 構(gòu)建等待圖(Waits-For Graph) :DBMS 在后臺(tái)維護(hù)一個(gè)有向圖。圖中的每個(gè)節(jié)點(diǎn)代表一個(gè)事務(wù),如果 T1 正在等待 T2 釋放鎖,就有一條從 T1 指向 T2 的邊。
- 周期性檢測 :系統(tǒng)會(huì)定期檢查這個(gè)等待圖中是否存在 循環(huán) 。一旦發(fā)現(xiàn)循環(huán),就證明發(fā)生了死鎖。
- 選擇受害者并中止 :檢測到死鎖后,DBMS 必須選擇一個(gè)“受害者”事務(wù)并將其中止,以打破循環(huán)。受害者釋放其所有鎖,讓其他事務(wù)得以繼續(xù)。
如何選擇受害者是一個(gè)復(fù)雜的問題,通常會(huì)基于一些啟發(fā)式規(guī)則,例如:
- 選擇最年輕的事務(wù)(時(shí)間戳最大)。
- 選擇已執(zhí)行工作最少的事務(wù)。
- 選擇持有鎖最少的事務(wù)。
- 為防止某個(gè)事務(wù)總是被選為受害者( 饑餓 Starvation ),也會(huì)考慮其歷史重啟次數(shù)。
策略二:死鎖預(yù)防(Deadlock Prevention)
這是一種“防患于未然”的策略,它通過制定規(guī)則來確保死鎖從一開始就不會(huì)發(fā)生,從而避免了構(gòu)建和檢測等待圖的開銷。
一種常見的方法是 基于事務(wù)時(shí)間戳分配優(yōu)先級(jí) (通常時(shí)間戳越小,即越老的事務(wù),優(yōu)先級(jí)越高)。當(dāng)發(fā)生鎖沖突時(shí),按以下策略之一處理:
Wait-Die(“老”等“新”)
- 如果請求鎖的事務(wù) T_req優(yōu)先級(jí)更高 (更老),則 T_req等待 持有鎖的 T_hold 。
- 如果請求鎖的事務(wù) T_req優(yōu)先級(jí)更低 (更年輕),則 T_req中止 (Die)并稍后重啟。
Wound-Wait(“老”搶“新”)
- 如果請求鎖的事務(wù) T_req優(yōu)先級(jí)更高 (更老),則它會(huì) 強(qiáng)制 持有鎖的 T_hold中止 (Wound),搶占鎖。
- 如果請求鎖的事務(wù) T_req優(yōu)先級(jí)更低 (更年輕),則 T_req等待T_hold 。
一個(gè)關(guān)鍵細(xì)節(jié)是:當(dāng)一個(gè)事務(wù)因死鎖預(yù)防而中止并重啟時(shí), 它會(huì)保留其原始的時(shí)間戳 。這保證了它最終會(huì)成為“最老”的事務(wù),從而獲得最高優(yōu)先級(jí),避免了饑餓問題。
2PL 的實(shí)際應(yīng)用:多粒度與意向鎖
如果一個(gè)事務(wù)需要更新一個(gè)包含數(shù)百萬行數(shù)據(jù)的表,難道要為每一行都獲取一個(gè)排他鎖嗎?這顯然是低效的。鎖管理器的開銷會(huì)變得巨大。
為了解決這個(gè)問題,數(shù)據(jù)庫引入了 多粒度鎖定(Multi-Granularity Locking) 的概念。鎖可以施加在不同的層級(jí)上:
Database → Table → Page → Tuple(Row)
事務(wù)可以在這個(gè)層次結(jié)構(gòu)的任何一層上請求鎖。當(dāng)一個(gè)事務(wù)在某個(gè)高層級(jí)節(jié)點(diǎn)(如 Table)上獲取鎖時(shí),它就 隱式地鎖定了其下的所有子節(jié)點(diǎn) (如該表的所有 Page 和 Tuple)。
但這又帶來了新問題:如果事務(wù) T1 鎖定了整個(gè)表,事務(wù) T2 如何知道它不能去修改表中的某一行呢?反之,如果 T2 鎖定了某一行,T1 又如何知道它不能直接鎖定整個(gè)表呢?
答案就是 意向鎖(Intention Locks) 。
意向鎖是施加在 高層級(jí)節(jié)點(diǎn) 的“標(biāo)記”或“提示”,它表明了事務(wù) 打算 在更低的層級(jí)上施加何種鎖。
- 意向共享鎖(Intention-Shared, IS) :表明事務(wù)打算在下層節(jié)點(diǎn)獲取 S 鎖。例如,在對(duì)某個(gè)元組加 S 鎖之前,先在表上加 IS 鎖。
- 意向排他鎖(Intention-Exclusive, IX) :表明事務(wù)打算在下層節(jié)點(diǎn)獲取 X 鎖。例如,在對(duì)某個(gè)元組加 X 鎖之前,先在表上加 IX 鎖。
- 共享意向排他鎖(Shared+Intention-Exclusive, SIX) :一種組合鎖,表示對(duì)當(dāng)前節(jié)點(diǎn)(如表)加 S 鎖,同時(shí)打算在下層節(jié)點(diǎn)(如元組)加 X 鎖。一個(gè)典型的場景是:SELECT * FROM table; UPDATE table SET ... WHERE id=...
通過意向鎖,鎖管理器可以非常高效地判斷鎖請求是否沖突。例如,當(dāng)一個(gè)事務(wù)想對(duì)整個(gè)表加 X 鎖時(shí),它只需檢查表上是否有任何其他鎖(包括 IS, IX 等)。如果發(fā)現(xiàn)表上已有一個(gè) IX 鎖,它就知道有其他事務(wù)正在修改表內(nèi)的某些行,因此必須等待。
結(jié)論:2PL 在現(xiàn)代數(shù)據(jù)庫中的地位
時(shí)至今日,兩階段鎖協(xié)議及其變體(尤其是 強(qiáng)嚴(yán)格兩階段鎖 SS2PL )仍然是絕大多數(shù)關(guān)系型數(shù)據(jù)庫(如 MySQL InnoDB, PostgreSQL, SQL Server )并發(fā)控制的核心基石。
當(dāng)你在 MySQL 中將事務(wù)隔離級(jí)別設(shè)置為 SERIALIZABLE 時(shí),其底層正是通過 SS2PL 來防止所有并發(fā)異常,確保事務(wù)的可串行化。
作為開發(fā)者,你通常不需要在 SQL 中手動(dòng)聲明 LOCK TABLE。數(shù)據(jù)庫管理系統(tǒng)會(huì)根據(jù)你的查詢(SELECT, INSERT, UPDATE, DELETE)自動(dòng)為你獲取和管理所需的鎖。但理解 2PL 的工作原理,可以幫助你:
- 分析和解決應(yīng)用中遇到的死鎖問題。
- 理解不同隔離級(jí)別之間的性能和一致性權(quán)衡。
- 更有效地使用 SELECT ... FOR UPDATE 這樣的語句來提前鎖定資源,優(yōu)化業(yè)務(wù)邏輯。
總而言之,兩階段鎖協(xié)議是數(shù)據(jù)庫理論與工程實(shí)踐完美結(jié)合的典范。它通過一個(gè)簡單而優(yōu)雅的規(guī)則,為復(fù)雜的數(shù)據(jù)世界帶來了秩序和穩(wěn)定。





































