MySQL中的MVCC
MVCC的概念
MVCC: Multi-Version Concurrency Control,即多版本並發控制.
是樂觀鎖的一種實現方式.
並發事務存在的問題:
- 更新丟失(Lost Update):多個事務同時更新同一行時,最后的更新會覆蓋之前的更新。
- 臟讀(Dirty Reads):一個事務對記錄的未提交修改被其他事務讀取到。
- 不可重復讀(Non-Repeatable Reads):一個事務內多次查詢相同記錄結果不一致。
- 幻讀(Phantom Reads):一個事務重新查詢之前檢索過的數據,發現出現新的數據。
解決:
- 加讀寫鎖。
- 一致性快照讀(MVCC)。
特點
- 用來提高數據庫高並發場景下的吞吐性能。
- MySQL中InnoDB引擎支持MVCC。
- 比加行鎖效率高,開銷低。
- 在讀已提交(Read Committed)和可重復讀(Repeatable Read)隔離級別下起作用。
- 可以基於樂觀鎖和悲觀鎖實現。
- 使用行級鎖(
row_level_lock
),而非行鎖(innodb_row_lock
). - 同一個事務能夠看到數據一致的視圖.
- 事務開始的時間不同,看到相同表的數據可能不同.
基本原理
- 通過保留某個時間點的快照實現的.
基本特征
- 每行數據都存在一個版本,每次數據更新時都更新該版本.
- 修改數據時復制當前版本的數據進行修改,各個事務之間互不影響.
- 保存時比較版本號,成功(commit)則覆蓋原記錄,失敗則放棄(rollback).
InnoDB存儲引擎MVCC實現策略
細節:
- 每一行保存兩個隱藏列:當前行創建時版本號和刪除時版本號.
- 版本號是系統版本號,每開始一個新事務,系統版本號自增.而事務的版本號為事務開始時的系統版本號.
- 每個事務有自己的版本號.
MVCC下的InnoDB的增刪改查
插入數據
- 設記錄的版本號為當前事務的版本號。
- 向表中插入數據。
- 將
create version
設置為當前事務的版本號,delete version
為空。
更新操作
- 將舊的記錄標記為已刪除,
delete version
為當前事務版本號。 - 插入一行新的記錄,
create version
為當前事務版本號,delete version
為當前版本號。
刪除操作
- 將待刪除的行的
delete version
設置為當前事務版本號。
查詢操作
記錄需滿足兩個條件:
delete version
為空或者設置的版本號大於當前事務的版本號(即:刪除操作發生在當前事務之后)create version
小於等於當前事務版本號(即:記錄創建在當前事務之前)
注:
- MVCC只適用於MySQL中的讀已提交(Read Committed)和可重復讀(Repeatable Read)。
- Read uncommitted存在臟讀,即:讀到未提交事務的數據行。
- 串行化是對表加鎖。
InnoDB MVCC 實現原理
實現方式:
- 每一行記錄都有兩個隱藏列:
DATA_TRX_ID
和DATA_ROLL_PTR
。(若沒有主鍵,則還有一個隱藏主鍵) DATA_TRX_ID
:記錄最近更新這條記錄的事務ID(6字節)DATA_ROLL_PTR
:指向該行回滾段的指針,通過指針找到之前版本,通過鏈表形式組織(7字節)DB_ROW_ID
:行標識(隱藏單增ID),沒有主鍵時主動生成(6字節)
多事務並發操作數據
特征:
- 不同事務對同一行的更新操作產生多個版本。
- 通過回滾指針將這些版本鏈接成一條Undo Log鏈。
更新操作流程:
- 將待操作的行加排他鎖。
- 將該行原本的值拷貝到Undo Log中,
DB_TRX_ID
和DB_ROLL_PTR
保持不變。(形成歷史版本) - 修改該行的值,更新該行的
DATA_TRX_ID
為當前操作事務的事務ID,將DATA_ROLL_PTR
指向第二步拷貝到Undo Log鏈中的舊版本記錄。(通過DB_ROLL_PTR
可以找到歷史記錄) - 記錄Redo Log,包括Undo Log中的修改。
INSERT
操作:產生新的記錄,其DATA_TRX_ID
為當前插入記錄的事務ID。DELETE
操作:軟刪除,將DATA_TRX_ID
記錄下刪除該記錄的事務ID,真正刪除操作在事務提交時完成。
一致性讀的實現
- RU隔離級別下 ==> 直接讀取版本的最新記錄。
- SERIALIZABLE隔離級別 ==> 通過加鎖互斥訪問數據實現。
- RC和RR隔離級別 ==> 使用版本鏈(ReadView,可讀視圖)
RR下的ReadView生成
特點:
- 每個事務首次執行
SELECT
語句時,會將當前系統所有活躍事務拷貝到一個列表中生成ReadView。 - 每個事務后續的
SELECT
操作復用其之前生成的ReadView。 UPDATE
,DELETE
,INSERT
對一致性讀snapshot無影響。
示例:事務A,B同時操作同一行數據
- 若事務A的第一個
SELECT
在事務B提交之前進行,則即使事務B修改記錄后先於事務A進行提交,事務A后續的SELECT
操作也無法讀到事務B修改后的數據。 - 若事務A的第一個
SELECT
在事務B修改數據並提交事務之后,則事務A能讀到事務B的修改。
RC下的ReadView生成
特點:
- 每次
SELECT
執行,都會重新將當前系統中的所有活躍事務拷貝到一個列表中生成ReadView。 - ReadView的組成:(當前活躍事務ID列表,稱為
m_ids
)- 最小值為
up_limit_id
:最先開始的事務。 - 最大值為
low_limit_id
:最后開始的事務。
- 最小值為
- ID越小,事務開始的越早;ID越大,事務開始的越遲。
- 若被訪問版本的
trx_id
小於up_limit_id
== > 生成該版本的事務在ReadView生成前就已提交 == > 該版本可以被當前事務訪問。 - 若被訪問版本的
trx_id
大於low_limit_id
== > 生成該版本的事務在ReadView生成之后才提交 == > 該版本不可被當前事務訪問 == > 通過Undo Log找到之前的版本重新判斷。 - 若被訪問的版本在
up_limit_id
和low_limit_id
之間 == > 需要判斷trix_id
是否在m_ids
中存在 == > 若存在,則生成該版本的事務還在活躍,則該版本不可訪問,可由Undo Log找到之前的版本進行重新判斷;若不存在,則創建ReadView時該版本對應的事務已提交,可以訪問該版本。 - 找到記錄后,還要判斷
delete_flag
是否為true,若為true,則該記錄已被刪除,不返回;若為false,則記錄可以返回。
注:對於ID較大的事務較ID較小的事務先提交的情況,即事務發生晚但提交的早
- RC的本質:每一條
SELECT
都可以看到其他已經提交的事務對數據的修改,只要事務提交,其結果都可見,與事務開始的先后順序無關。 - RR的本質:第一條
SELECT
生成ReadView前,已經提交的事務的修改可見。
參考: