1. 程式人生 > >CMU15-455 Lab2 - task4 Concurrency Index -併發B+樹索引演算法的實現

CMU15-455 Lab2 - task4 Concurrency Index -併發B+樹索引演算法的實現

最近在做 CMU-15-445 Database System,lab2 是需要完成一個支援併發操作的B+樹,最後一部分的 Task4 是完成併發的索引這裡對這部分加鎖的思路和完成做一個總結,關於 B+ 樹本身的操作(插入、刪除)之後再整理。 [TOC] # 一些基礎知識 ## 索引的併發控制 併發控制協議是DBMS用來確保對共享物件進行併發操作的“正確”結果的方法。 協議的正確性標準可能會有所不同: * 邏輯正確性:這意味著執行緒能夠讀取應允許其讀取的值。 * 物理正確性:這意味著資料結構中沒有指標,這將導致執行緒讀取無效的記憶體位置。 ## Lock 和 Latch ### Lock Lock 是一種較高級別的邏輯原語,可保護資料庫的內容(例如,元組,表,資料庫)免受其他事務的侵害。事務將在整個持續時間內保持鎖定狀態。資料庫系統可以將查詢執行時所持有的鎖暴露給使用者。鎖需要能夠回滾更改。 ### Latch latch 是低階保護原語,用於保護來自其他執行緒的DBMS**內部資料結構**(例如,資料結構,記憶體區域)的關鍵部分。 latch 僅在執行操作期間保持。 latch 不需要能夠回滾更改。 latch 有兩種模式: * READ:允許多個執行緒同時讀取同一專案。一個執行緒可以獲取讀 latch ,即使另一個執行緒也已獲取它。 * WRITE:僅允許一個執行緒訪問該專案。如果另一個執行緒以任何模式保持該 latch ,則該執行緒將無法獲取寫 latch 。持有寫 latch 的執行緒還可以防止其他執行緒獲取讀 latch > 這部分 提供的 RWLatch 的實現真的寫得好,放到末尾來參考 這裡對他們的不同做一個比較: ![](https://img2020.cnblogs.com/blog/1930022/202103/1930022-20210316161524934-8516338.png) ## Latch 的實現 用於實現 latch 的基礎原語是通過現代CPU提供的原子比較和交換(CAS)指令實現的。這樣,執行緒可以檢查記憶體位置的內容以檢視其是否具有特定值。如果是這樣,則CPU將舊值交換為新值。否則,記憶體位置的值將保持不變。 有幾種方法可以在DBMS中實現 latch 。每種方法在工程複雜性和執行時效能方面都有不同的權衡。這些測試和設定步驟是自動執行的(即,沒有其他執行緒可以更新測試和設定步驟之間的值。 ### Blocking OS Mutex latch(鎖存器) 的一種可能的實現方式是OS內建的互斥鎖基礎結構。 Linux提供了mutex(fast user-space mutex ),它由(1) user space 中的自旋 latch 和(2)OS級別的 mutex 組成。 如果DBMS可以獲取 user space latch ,則設定 latch 。即使它包含兩個內部 latch ,它也顯示為DBMS的單個 latch 。如果DBMS無法獲取 user space latch ,則它將進入核心並嘗試獲取更昂貴的互斥鎖。如果DBMS無法獲取第二個互斥鎖,則執行緒會通知OS鎖已被阻塞,然後對其進行排程。 作業系統互斥鎖通常是DBMS內部的一個不好的選擇,因為它是由OS管理的,並且開銷很大。 ### Test-and-Set Spin Latch (TAS) 自旋 latch 是OS互斥鎖的更有效替代方法,因為它是由DBMS控制的。自旋 latch 本質上是執行緒在記憶體中嘗試更新的位置(例如,將布林值設定為true)。執行緒執行CAS以嘗試更新記憶體位置。如果無法獲取 latch ,則DBMS可以控制會發生什麼。它可以選擇重試(例如,使用while迴圈)或允許作業系統取消排程。因此,與OS互斥鎖相比,此方法為DBMS提供了更多的控制權,而OS互斥鎖無法獲取 latch 而使OS得到了控制權。 ### Reader-Writer Latches 互斥鎖和自旋 latch 不區分讀/寫(即,它們不支援不同的模式)。 DBMS需要一種允許併發讀取的方法,因此,如果應用程式進行大量讀取,它將具有更好的效能,因為讀取器可以共享資源,而不必等待。 讀寫器 latch 允許將 latch 保持在讀或寫模式下。它跟蹤在每種模式下有多少個執行緒保持 latch 並正在等待獲取 latch 。讀取器-寫入器 latch 使用前兩個 latch 實現中的一種作為原語,並具有其他邏輯來處理讀取器-寫入器佇列,該佇列是每種模式下對 latch 的佇列請求。不同的DBMS對於如何處理佇列可以具有不同的策略。 # B+樹加鎖演算法 為了能儘可能安全和更多的人使用B+樹,需要使用一定的鎖演算法來講 B+ 樹的某部分鎖住來進行操作。這裡將使用 `Lock crabbing / coupling` 協議來允許多個執行緒同時訪問/修改B+樹,基本的思想如下: 1. 獲取 parent 的 latch 2. 獲取 child 的 lacth 3. 如果 child 是 “安全的”,那麼就可以釋放 parent 的鎖。“安全”指的是某個節點在子節點更新的時候不會 分裂 split 或者 合併 merge(在插入的時候不滿,在刪除的時候大於最小的大小) > 當然,這裡需要區分讀鎖和寫鎖,read latch 和 write latch > > 鎖是在針對於每個 Page 頁面上的,我們將對每個 內部頁面或者根頁面進行鎖 推薦看 cmu-15-445 lecture 9 的例子,講解的非常清楚,這裡舉幾個例子: ## 例子 ### 查詢 ![](https://img2020.cnblogs.com/blog/1930022/202103/1930022-20210316161623320-297466953.png) ... 一步步向下加鎖,如果獲得了子頁面,就將父親的讀鎖直接釋放 ![](https://img2020.cnblogs.com/blog/1930022/202103/1930022-20210316161632500-320263551.png) ### 刪除和插入 刪除和插入其實都是寫鎖,區別不大,只是在具體的判斷某個節點是否安全的地方進行不同的判斷即可。這裡舉一個不安全插入的例子: ![](https://img2020.cnblogs.com/blog/1930022/202103/1930022-20210316161645584-949192063.png) ![](https://img2020.cnblogs.com/blog/1930022/202103/1930022-20210316161656230-146039546.png) ![](https://img2020.cnblogs.com/blog/1930022/202103/1930022-20210316161704651-798108244.png) ## 優化 我們上面聊到的其實是悲觀鎖的一種實現,也就是說如果處於不安全狀態,我們就一定加鎖(注意,不安全狀態不一定),所以可能效率可能會稍微打一點折扣,這裡介紹一下樂觀鎖的思路: 假定預設是查詢多,大多數操作不會進行分裂 split 或者 合併 merge,不會修改到父親頁面,一直樂觀地在樹中採用讀鎖來遍歷,直到真的發現會修改父親頁面之後,再次以悲觀鎖的方式執行一次寫操作即可。 ## Leaf Scan 剛才提到的的執行緒以“自上而下”的方式獲取 latch 。這意味著執行緒只能從其當前節點下方的節點獲取 latch 。如果所需的 latch 不可用,則執行緒必須等待直到可用。鑑於此,永遠不會出現死鎖。 但是葉節點掃描很容易發生死鎖,因為現在我們有執行緒試圖同時在兩個不同方向(即從左到右和從右到左)獲取鎖。索引 latch 不支援死鎖檢測或避免死鎖。 所以解決這個問題的唯一方法是通過編碼規則。葉子節點同級 latch 獲取協議必須支援“無等待”模式。也就是說,B +樹程式碼必須處理失敗的 latch 獲取。這意味著,如果執行緒嘗試獲取葉節點上的 latch ,但該 latch 不可用,則它將立即中止其操作(釋放其持有的所有 latch ),然後重新開始操作。 我的想法是:可以來採用單方向的鎖,或者是兩個方向的葉子鎖,假定一個方向的優先順序問題,優先順序高的可以搶佔優先順序較低方向的鎖。 # 具體實現思路 ## transaction 每個執行緒針對資料庫的操作都會新建一個事務,每個事務內都會執行不同的操作,在每次執行事務的時候對當前事務進行一個標記,用一個列舉型別表明本次事務是增刪改查的哪一種: ```c++ /** * 操作的類別 */ enum class OpType { READ = 0, INSERT, DELETE, UPDATE }; ``` ## 自頂向下遞迴查詢子頁面 這是整個悲觀鎖演算法的最基礎的地方,也就是上方的例子中的內容,當我們遞迴去查詢 Leaf Page 的時候就可以對“不安全的”頁面加上鎖,此後,就不需要再次加鎖了,同時將所有鎖住的 Page 利用 transaction 來儲存,這樣在最終修改結束之後統一釋放不安全頁面的讀鎖或者寫鎖即可。 這裡對兩個核心的函式進行說明: ### 遞迴查詢子頁面 遞迴查詢子頁面過程中需要加鎖,將這個邏輯抽離出去,根據不同事務的操作來決定是否釋放鎖 ```c++ /* * Find leaf page containing particular key, if leftMost flag == true, find * the left most leaf page */ INDEX_TEMPLATE_ARGUMENTS Page *BPLUSTREE_TYPE::FindLeafPage(const KeyType &key, bool leftMost, Transaction *transaction) { { std::lo