1. 程式人生 > >MySQL隔離級別的實現

MySQL隔離級別的實現

機制 表鎖 情況 mod 平時 就會 table height 日誌

雖然平時已經很少使用MySQL了,但是數據庫作為基本技能仍然不能忘,最近在學習數據庫隔離級別,在此寫下個人理解以備復習。

大家都知道數據庫事務ACID(原子性、一致性、隔離性和持久性)的四個特征,也知道數據庫存在三種並發問題(臟讀、不可重復讀、幻讀),以及針對性的四種隔離級別(讀未提交、讀已提交、可重復讀、序列化)。

解決與否 臟讀 不可重復讀 幻讀
讀未提交 Yes Yes Yes
讀已提交 No Yes Yes
可重復讀 No No Yes
串行化 No No No

特別提醒一句,隔離級別作用在連接(或會話)級別。客戶端每次連接數據庫的時候,都要根據自己對一致性的需求程度合理設置自己的事務隔離級別。

那麽問題來了,MySQL底層是采用何種技術來實現這四種隔離級別的呢?

讀未提交

MySQL全表的數據存儲在以主鍵為排序值的B+樹索引中,葉子節點存儲了相應主鍵的整行記錄。需要指出的是,葉子節點的數據都是最新的數據,可能是事務提交後的一致狀態,也可能是事務執行中的中間值(可能被回滾)。

當隔離級別設置為RU時:

  • 所有的讀不加鎖,讀到的都是葉子節點上最新的值,性能最好。
  • 所有的寫(更新、插入、刪除)加行級排斥鎖,不存在臟寫的問題,寫完就釋放鎖。

讀已提交

當隔離級別是RC和RR時,就要談到大名鼎鼎的MVCC(多版本控制) 技術。通過在每行加入若幹隱藏的字段,它實現了不加鎖的讀操作,性能較好。

  • 先說RC級別的寫操作,MySQL依然加行級排斥鎖。事務開始時會往UNDO日誌中寫入當前的有效記錄值,B+樹葉子節點的隱藏列DATA_ROLL_PTR會存儲指向該UNDO記錄的指針。順著行的DATA_ROLL_PTR的指針形成一個鏈表,記錄該行數據的有效的歷史記錄。
  • 再說不加鎖的讀操作,如果葉子節點正被其他事務鎖定,那麽MySQL順著葉子節點的DATA_ROLL_PTR指針找到上一個有效的歷史記錄即可。

可重復讀

在事務開始的時候,除了正常往UNDO日誌中寫回滾的數據外,會創建一個ReadView,記錄了當前活躍的其他事務的ID,其中最小值為Tmin,最大值為Tmax。

當執行SELECT操作時,MySQL順著行記錄的DATA_ROLL_PTR指針查找符合條件的歷史版本。這裏就用到了另一個隱藏列DATA_TRX_ID,其中存儲的是更新該記錄的事務ID(事務ID是全局遞增且唯一的)。如果DELETE_BIT為1,則代表ID為DATA_TRX_ID的事務對當前行執行了刪除。

掃描歷史版本串成的鏈表的過濾條件是:

  1. 如果當前記錄的DATA_TRX_ID小於Tmin(之前存在的數據),那麽由DELETE_BIT決定是否可見;否則,轉2。
  2. 如果當前記錄的DATA_TRX_ID小於Tmax,且不在活躍的事務ID集合中,那麽由DELETE_BIT決定是否可見視為可見;否則,轉3。
  3. 否則視為不可見,順著DATA_ROLL_PTR進入上一個歷史版本,或因為到頭而結束回溯。

因為插入的數據版本號要麽在活躍事務ID集合內、要麽大於當前事務ID,所以MVCC機制同時解決了幻讀問題。

需要指出的是,以上三個隔離級別中的讀均為普通的SELECT。如果用的是SELECT ... LOCK IN SHARE MODE或SELCT ... FOR UPDATE,均屬於當前讀。即加寫鎖,讀葉子節點最新值,如果更早的事務改了行值,依然會存在不可重復讀的情況。

在RR級別下,如果WHERE的條件列上有唯一索引,那麽MySQL只加行級鎖;如果是普通索引,會加間隙鎖來防止幻讀;如果沒有索引,就會采用表鎖,大大降低並發寫的能力。

串行化

讀寫均加表級的讀寫鎖即可,直接讀主鍵索引B+樹的葉子節點的最新數據。該級別下,數據一致性很強,但是並發寫的能力非常差。

MySQL隔離級別的實現