cmu15545筆記-併發控制總結(Concurrency Control Summary)

咪啪魔女發表於2024-12-03

目錄
  • 總覽
    • ACID
    • 序列化與衝突操作
    • 隔離級別
    • 概念層級
  • 二階段鎖
    • 原理
    • 級聯回滾
    • 強二階段鎖
    • 死鎖檢測和避免
    • 鎖層級
    • 實踐應用
    • 實現的隔離級別
  • OOC
    • 原理
    • 三個階段
    • 實現的隔離級別
  • 處理幻讀
  • MVC
    • 原理
    • 寫偏差異常(Write Skew Anomaly)
    • 版本儲存(Version-storage)
      • Append Only
      • Time Travel Storage
      • Delta Storage
    • 垃圾回收
      • Tuple Level
      • Transition Level
      • 對比
    • 索引管理
    • 刪除
    • 各個資料庫的實現方式
  • 總結

總覽

該筆記包含了原課程中關於併發控制的四節課的內容:

  • 併發控制理論(Concurrency Control Theory)
  • 二階段鎖併發控制(Two-Phase Locking Concurrency Control)
  • 樂觀併發控制/基於時間戳(Optimistic Concurrency Control)
  • 多版本併發控制(Multiple-Version Concurrency Control)

ACID

併發控制與資料庫恢復一體兩面,在資料庫系統中設計到如下部分:

cmu15545筆記-併發控制總結(Concurrency Control Summary)

資料庫為達成一個目的的語句塊稱為一個事務,特殊地,一個語句本身就可以視為一個事務。

併發控制與資料庫恢復的目標是實現事務的ACID

  • Atomicity:全或無。
  • Consistency:資料整體一致;分散式場景下,強一致或最終一致。
  • Isolation:事務並行,但是互不干涉。
  • Durability:事務提交,就保證修改已經持久化。

image-20241128122657667

序列化與衝突操作

如何確定事務併發的執行結果是滿足隔離性(Insolation)的?
答:事務是可序列化的,即並行排程結果和序列排程一致。

序列排程(Serial Schedule):資料庫每次只執行一個事務,不併行。

可序列化排程(Serializable Schedule):允許事務並行,但是並行執行的結果可以等效為序列排程。

為什麼有的事務是可序列化的,而有的無法序列化?
答:取決於是否包含(讀寫)衝突操作,存在衝突操作時,往往就是不可序列化的。

衝突操作帶來併發問題:

  • 讀寫衝突(R-W):不可重複讀,幻讀[嚴格來說是 Scan / Insert ]
  • 寫讀衝突(W-R):髒頁讀
  • 寫寫衝突(W-W):更新丟失

image-20241129143220423

如何判斷一組事務是可序列化的?

答:依賴關係圖(Dependency Graph)不成環。

(髒讀一般是回滾時才考慮)

image-20241120165135138

隔離級別

資料不一致問題與隔離級別對照圖。

Dirty Read Unrepeatable Read Lost Update Phantom
Serializable No No No No
Repeatable Read No No No Maybe
Read Committed No Maybe Maybe Maybe
Read Uncommitted Maybe Maybe Maybe Maybe

不同資料庫支援的隔離級別。

其中比較特殊的是Oracle最高支援的是Snapshot Isolation而不是Serializable

image-20241129122622520

完整的隔離級別層級圖。

image-20241129122743274

事務的序列化排程和序列化隔離級別的關係:

  • 序列化排程(Serializable Schedule):保證事務一致性,指兩個事務併發時,效果等價於序列執行,即依賴圖不成環。

  • 序列化隔離級別(Serializable Level):指不出現上文的四個併發問題。

  • 一般認為,滿足 序列化隔離級別 時,事務間就可以實現序列化排程。

概念層級

併發控制實現的目標是事務的ACID,但由於存在衝突操作,導致出現一系列併發問題,造成資料不一致,為了權衡效能與併發問題的錯誤程度,定義了不同的隔離級別,為了實現不同的隔離級別,有各樣的併發控制機制,如二階段鎖,OOC,索引鎖等等。

概念由抽象到具體,從頂層到底層,結構圖如下:

cmu15545筆記-併發控制總結(Concurrency Control Summary)

下面主要介紹不同的併發控制機制的原理與解決的問題。

二階段鎖

原理

有兩類鎖,共享鎖和互斥鎖。

S-Lock X-Lock
S-Lock
X-Lock

二階段鎖如何解決併發問題?

  • 最粗的粒度:事務開始就加鎖,事務提交時釋放鎖,此時是嚴格的序列化,但是效率不高。
  • 最細的粒度:操作時加鎖,操作結束就釋放鎖,沒有解決依賴圖成環的問題,依然是非序列化的。
image image
  • 二階段鎖:分為兩個階段,鎖獲取階段和鎖釋放階段,只有獲取完所有鎖以後才能釋放鎖。

image-20241128144630780

由於獲取完所有鎖才會釋放,所以依賴圖不會成環(見“序列化與衝突操作”一節),但是由於我們無法對Abort操作加鎖,並且插入或刪除元素時也無法加鎖,因此二階段鎖可以解決不可重複讀和更新丟失,無法解決髒讀和幻讀。

級聯回滾

髒讀會帶來級聯回滾(Cascading Abort)的問題,見下圖。

\(T_2\)讀取了\(T_1\)未提交的資料,導致\(T_1\)回滾時,\(T_2\)也需要回滾。

image-20241128151540707

強二階段鎖

強二階段鎖(String Strict 2PL):不是操作結束後立刻解鎖,而是在事務提交時統一解鎖。

解決了髒讀問題。因為由於事務提交前不釋放鎖,所以另一個事務無法讀到剛修改的資料。

image-20241128152347036

死鎖檢測和避免

一個死鎖的例子:鎖交錯。

image-20241128153128615

死鎖檢測:檢測是否成環

時序圖上體現為交叉,在依賴圖上體現為環。

image-20241128153513785

處理方式:選擇一個事務進行回滾。

如何選擇:依照年齡,執行進度,鎖數量等。

如何回滾:全部回滾;部分回滾:設定savepoint,回滾到savepoint。

死鎖避免:設定優先順序,保證鎖單向傳遞,不產生交錯

  • 事務的開始時間越早,優先順序越高
  • 分類:非搶佔式(Wait-Die),搶佔式(Wound-Wait
高->低 低->高
非搶佔式 高優先順序等待【愛幼】 低優先順序放棄
搶佔式 高優先順序搶佔 低優先順序等待【尊老】

沒有雙向等待,也就不會產生交錯,也就不會有死鎖。

image-20241128155123174

鎖層級

資料庫需要維護不同層級(Hierarchical)的鎖來保證併發度。

image-20241128160942998

意向鎖:在給子層級加鎖時,給父層級加意向鎖,相容矩陣如下。

image-20241128162450411

實踐應用

Select ... For Update

BEGIN;
SELECT balance FROM Accounts WHERE account_id = 1 FOR UPDATE;
UPDATE Accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;

扣減餘額時,先select再update:

  • select時,account=1的tuple上的是S鎖,父結點上的是IS鎖;
  • update時,accnount=1的tuple需要上X鎖,父結點升級為ISX鎖;
  • 所以可以透過Select ... For Update,一開始就給tuple上X,給父結點上ISX鎖

Select ... Skip Locked

可以跳過鎖,避免阻塞。

例如:

SELECT task_id, task_data
FROM task_queue
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1;
  • 一個任務表儲存了待處理任務,每個任務由不同的執行緒負責處理:
  • 每個執行緒獲取一個未鎖定的任務進行處理;
  • 已被其他執行緒鎖定的任務會被跳過,從而提高併發處理效率

實現的隔離級別

  1. SERIALIZABLE:強二階段鎖+幻讀預防措施(如索引鎖,見後文)。
  2. REPEATABLE READS:強二階段鎖。

OOC

樂觀的併發控制(Optimistic Concurrency Control)。

原理

假設大多數時候沒有衝突,先執行操作,操作結束後再進行一次驗證。

  • 如果確實沒有衝突,提交事務,寫入結果
  • 如果有衝突,回滾,重新進行

三個階段

  1. 讀取(Read Phase):每個事務有一個私有的儲存空間,當訪問元組時,將訪問結果讀取到該空間中,後續的操作都在該空間進行。
  2. 驗證(Validation Phase):賦予事務一個時間戳,並校驗是否有衝突,即是否滿足下面的條件。
    • \(WriteSet(T1) ∩ ReadSet(T2) = Ø\)
    • 如果此時事務2還處於讀取階段,那麼還需要滿足:\(WriteSet(T1) ∩ WriteSet(T2) = Ø\)
  3. 寫入(Write Phase):寫入結果,此時修改其他事務可見。

image-20241129172951290

image-20241129173524368

實現的隔離級別

如何解決併發問題:併發問題的圖示見上文“序列化與衝突操作“一節

當處於驗證階段時,如果\(T_1<T_2\)

  • 由於讀的是副本:不會出現髒讀不可重複讀問題。

  • 由於讀的是副本:如果\(WriteSet(T1) ∩ ReadSet(T2) ≠ Ø\)

    \(T_2\)理應看到\(T_1\)的更新值,但是由於\(T_1\)還沒有把結果寫入磁碟,所以\(T_2\)讀的是副本,而不是\(T_1\)的更新值。

    所以此時存在資料不一致問題,因此要保證\(WriteSet(T1) ∩ ReadSet(T2) = Ø\)

  • \(WriteSet(T1) ∩ WriteSet(T2) = Ø\):解決更新丟失問題。

  • 沒有解決幻讀問題。

反例1:\(WriteSet(T1) ∩ ReadSet(T2) ≠ Ø\),此時\(T_2\)沒有讀到\(T_1\)的更新。

image-20241129173806337

反例2:\(WriteSet(T1) ∩ WriteSet(T2) ≠ Ø\),可能出現更新丟失問題。

image-20241129180539646

注:在驗證階段,我麼都是與未提交的事務進行校驗,稱為前向校驗(Forward Validation)。

image-20241129181019340

當然也可以與已提交的事務進行校驗,稱為後向校驗(Backward Validation)。

image-20241129181059663

總結:

  1. SERIALIZABLE:OOC+幻讀預防措施(如索引鎖,見後文)。
  2. REPEATABLE READS:OOC。

處理幻讀

主要採用索引鎖(Index Lock)的方式。

在插入資料時,鎖住索引見的間隙(Gap),從而阻止插入或刪除。

image-20241129182341555

image-20241129182324843

更進一步:給索引鎖也加上意向鎖這個層級。

image-20241129182426013

MVC

原理

基本思想:事務透過元組(Tuple)的版本,判斷可見性。

  • 版本:解決我能看到誰

    • 三元組 [begin-Txn, end-Txn, value]
    • 讀操作:置begin-Txn
    • 寫操作:置新值begin-Txn,舊值end-Txn
  • 事務活動表:解決我能不能訪問我看到的,必要時藉助鎖

image image
image image
image image
image image

從上述例子中,可以看出MVCC解決了

  • 髒讀,不可重複讀

  • 更新丟失

  • 幻讀依然沒有解決,需要結合索引鎖等機制

寫偏差異常(Write Skew Anomaly)

只檢測直接的寫衝突,無法捕獲事務之間的隱式邏輯依賴,導致會違背全域性約束。

假設有一個醫院的醫生值班系統,要求 任何時刻至少有一名醫生值班。表 Shifts 記錄了當前醫生是否值班:

DoctorID OnDuty
1 Yes
2 Yes
  • T1: 醫生 1 決定取消自己的值班,讀取當前值班情況,發現醫生 2 仍在值班,於是提交一個事務將自己從值班中移除。

  • T2: 醫生 2 決定取消自己的值班,讀取當前值班情況,發現醫生 1 仍在值班,於是提交一個事務將自己從值班中移除。

過程

  1. T1T2 基於同一個快照讀取 Shifts 表,發現當前有另一名醫生在值班(醫生 2 和醫生 1)。
  2. T1T2 分別更新自己的記錄,將 OnDuty 設定為 No。
  3. 兩個事務沒有直接修改相同的記錄,因此快照隔離認為沒有寫衝突,允許它們同時提交。
  4. 提交後,Shifts 表中所有醫生的 OnDuty 均為 No,違反了至少 2 名醫生值班的約束。

版本儲存(Version-storage)

元組的版本資訊如何儲存:

  • Append only:新舊版本在同一張表空間
  • Time travel storage:新舊版本分開
  • Delta Storage:不儲存實際值,而是儲存增量delta

Append Only

每個邏輯元組的所有物理版本都儲存在一個相同的表空間中。

不同邏輯元組的物理版本之間用連結串列串聯。

當更新時,新增一個新的物理版本到的表空間中,如下圖所示(省略了begin_Txnend_Txn)。

image-20241202130033051

連結串列的串聯順序可以是由舊到新 Oldest-to-Newest (O2N),也可以是由新到舊 Newest-to-Oldest (N2O)

Time Travel Storage

有一張主表和一張歷史版本表,當更新的時候,把舊版本寫入歷史版本表中,然後新版本寫到主表上。

比如寫入\(A_3\)時,先寫把\(A_2\)寫到歷史版本表,維護相應指標,然後把\(A_3\)寫入主表。

image-20241202131600382

Delta Storage

依然是有豬逼哦啊和歷史版本表,但是歷史版本表儲存增量而不是實際值。

\(A_1=111 \rightarrow A_2 = 222 \rightarrow A_3 = 333\)的版本記錄記錄如下。

image-20241202132120421

垃圾回收

事務的所有歷史版本記錄那都是存放在表空間中,久而久之就會不斷堆積,所以對於沒有用的版本記錄,需要及時回收。

可回收的版本記錄:

  1. 活躍事務都不可見的版本。
  2. 終止(abort)的事務的版本。

垃圾回收的目標:找到上述兩類過期版本,並將它們安全地刪除。

垃圾回收的兩個層級:

  1. 元組層級(Tuple Level
  2. 事務層級(Transaction Level)

Tuple Level

Background Vacuuming

後臺清理執行緒集中化清理,適用於所有版本儲存方式。

清理現場掃描歷史版本表,將每個歷史版本的begin_Txnend_Txn與當前所有活躍的事務的id進行比較,判斷是否可以清理。

image-20241203102054656

如上圖,清除了\(A_{100}\)\(B_{100}\)

改進:新增髒頁點陣圖,快速跳轉到代清理的版本。

image-20241203102124716

Cooperative Cleaning

分散式清理,清理任務分攤到每個工作執行緒,適用於O2N。

全域性維護一個事務id(Txn),表示當前活躍事務的最小id,當每個工作執行緒在自己的歷史版本表中尋找自己的可見版本時,順帶清理掉那些全域性不可見的版本。

image-20241203102306935

Transition Level

集中化清理,但是舊版本收集分攤到工作執行緒,適用於所有版本儲存方式。

當事務建立了一個新版本後,將舊版本提交給中心清理執行緒,中心執行緒統一清理舊版本。

image-20241203103154830

image-20241203103219349

image-20241203103234459

對比

屬性 Background Vacuuming Cooperative Cleaning Transition Level Vacuuming
觸發方式 後臺任務自動觸發 事務或查詢過程中觸發 根據事務需求動態觸發
舊版本收集 清理執行緒 工作執行緒 工作執行緒
舊版本清理 清理執行緒 工作執行緒 清理執行緒
系統資源開銷 佔用額外資源 分散到使用者操作中 智慧排程,可能額外增加開銷
適用場景 資料量大,需釋放磁碟空間 實時查詢環境 隔離級別需求高的環境
優缺點平衡 減少使用者影響但有清理滯後風險 實時性好但增加使用者操作開銷 智慧性高但複雜度和判斷開銷增加

索引管理

二級索引維護方式

  1. 邏輯指標(Logical Pointers):二級索引使用每個元組的固定識別符號(例如主鍵)來指向資料。間接訪問,需要回表。
  2. 物理指標(Physical Pointers):直接使用實體地址指向版本鏈的頭部。直接訪問,無需回表,但是維護困難。
image image

索引重複值問題(Duplicate Key Problem):

MVCC中不同時間的事務會看到元組的不同版本,所以一個元組會有不同的索引,指向不同的物理版本。

如下圖,begin_Txn < 30的事務看到的是A已刪除,而begin_Txn >= 30的事務看到的是A=30。

image-20241203111257907

刪除

如何表示一個版本被刪除。

  1. 版本上新增一個標識位,標識已刪除
  2. 新建一個空版本標識已刪除,

image-20241203112652523

各個資料庫的實現方式

image-20241203112828671

Index Management

  • Secondary Indexes
    • Logical Pointers
    • Physical Pointers
    • Multiple key Problem(GC)

總結

2PL,OOC,MVCC都是實現事務ACID的方式。

2PL運用鎖來控制併發,比較底層;OOC先執行,後利用時間戳檢測,適合衝突少的情況;MVCC利用版本控制,除了可以控制併發正確性,還能進行版本回溯,是當前的主流方式。

強二階段鎖,OOC和MVCC都能避免髒讀,不可重複讀和更新丟失,但是無法避免幻讀,需要額外利用其他機制,如索引鎖。

MVCC會有寫偏差異常(Write Skew Anomaly),無法實現完全到序列化的隔離級別,往往和其他併發控制機制如2PL,OOC結合使用。

image-20241203121010635

相關文章