雖然平時已經很少使用MySQL了,但是數據庫作為基本技能仍然不能忘,最近在學習數據庫隔離級別,在此寫下個人理解以備復習。
大家都知道數據庫事務ACID(原子性、一致性、隔離性和持久性)的四個特征,也知道數據庫存在三種並發問題(臟讀、不可重復讀、幻讀),以及針對性的四種隔離級別(讀未提交、讀已提交、可重復讀、序列化)。
解決與否 | 臟讀 | 不可重復讀 | 幻讀 |
讀未提交 | Yes | Yes | Yes |
讀已提交 | No | Yes | Yes |
可重復讀 | No | No | Yes |
串行化 | No | No | No |
特別提醒一句,隔離級別作用在連接(或會話)級別。客戶端每次連接數據庫的時候,都要根據自己對一致性的需求程度合理設置自己的事務隔離級別。
那么問題來了,MySQL底層(下文均指InnoDB引擎)是采用何種技術來實現這四種隔離級別的呢?
讀未提交
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的事務對當前行執行了刪除。
掃描歷史版本串成的鏈表的過濾條件是:
- 如果當前記錄的DATA_TRX_ID小於Tmin(之前存在的數據),那么由DELETE_BIT決定是否可見;否則,轉2。
- 如果當前記錄的DATA_TRX_ID小於Tmax,且不在活躍的事務ID集合中,那么由DELETE_BIT決定是否可見視為可見;否則,轉3。
- 否則視為不可見,順着DATA_ROLL_PTR進入上一個歷史版本,或因為到頭而結束回溯。
因為插入的數據版本號要么在活躍事務ID集合內、要么小於當前事務ID,所以MVCC機制同時解決了幻讀問題。
需要指出的是,以上三個隔離級別中的讀均為普通的SELECT。如果用的是SELECT ... LOCK IN SHARE MODE或SELCT ... FOR UPDATE,均屬於當前讀。即加讀鎖或寫鎖,讀葉子節點最新值。如果更早的事務改了行值,依然會存在不可重復讀的情況;如果前后兩次讀均為當前讀,則不會如此(因為第一次讀加鎖了)。
在RR級別下,如果WHERE的條件列上有唯一索引,那么MySQL只加行級鎖;如果是普通索引,會加間隙鎖來防止幻讀;如果沒有索引,就會首先鎖表的所有記錄、再釋放不符合條件的行的鎖,因此會大大降低並發寫的能力。
串行化
讀寫均加表級的讀寫鎖即可,直接讀主鍵索引B+樹的葉子節點的最新數據。該級別下,數據一致性很強,但是並發寫的能力非常差。