MVCC全稱是Multi-Version Concurrency Control(多版本併發控制),是一種併發控制的方法,通過維護一個數據的多個版本,減少讀寫操作的衝突。 如果沒有MVCC,想要實現同一條數據的併發讀寫,還要保證數據的安全性,就需要操作數據的時候加讀鎖和寫鎖,這樣就降低了資料庫的併發... ...
1. 什麼是MVCC
MVCC全稱是Multi-Version Concurrency Control(多版本併發控制),是一種併發控制的方法,通過維護一個數據的多個版本,減少讀寫操作的衝突。
如果沒有MVCC,想要實現同一條數據的併發讀寫,還要保證數據的安全性,就需要操作數據的時候加讀鎖和寫鎖,這樣就降低了資料庫的併發性能。
有了MVCC,就相當於把同一份數據生成了多個版本,在操作的開始各生成一個快照,讀寫操作互不影響。無需加鎖,也實現數據的安全性和事務的隔離性。
事務的四大特性中隔離性就是基於MVCC實現的。
說MVCC的實現原理之前,先說一下事務的隔離級別。
2. 事務的隔離級別
說隔離級別之前,先說一下併發事務產生的問題:
臟讀: 一個事務讀到其他事務未提交的數據。
不可重覆讀: 相同的查詢條件,多次查詢到的結果不一致,即讀到其他事務提交後的數據。
幻讀: 相同的查詢條件,多次查詢到的結果不一致,即讀到其他事務提交後的數據。
不可重覆讀與幻讀的區別是: 不可重覆讀是讀到了其他事務執行update、delete後的數據,而幻讀是讀到其他事務執行insert後的數據。
再說一下事務的四大隔離級別:
Read UnCommitted(讀未提交): 讀到其他事務未提交的數據,會出現臟讀、不可重覆讀、幻讀。
Read Committed(讀已提交): 讀到其他事務已提交的數據,解決了臟讀,會出現不可重覆讀、幻讀。
Repeatable Read(可重覆讀): 相同的條件,多次讀取到的結果一致。解決了臟讀、不可重覆讀,會出現幻讀。
Serializable(串列化): 所有事務串列執行,解決了臟讀、不可重覆讀、幻讀。
隔離級別 | 臟讀 | 不可重覆讀 | 幻讀 |
---|---|---|---|
讀未提交 | 會 | 會 | 會 |
讀已提交 | 不會 | 會 | 會 |
可重覆讀 | 不會 | 不會 | 會 |
串列化 | 不會 | 不會 | 不會 |
MVCC只在Read Committed和Repeatable Read兩個隔離級別下起作用,因為Read UnCommitted隔離級別下,讀寫都不加鎖,Serializable隔離級別下,讀寫都加鎖,也就不需要MVCC了。
再談一下Undo log日誌。
3. Undo Log(回滾日誌)
Undo Log記錄的是邏輯日誌,也就是SQL語句。
比如:當我們執行一條insert語句時,Undo Log就記錄一條相反的delete語句。
作用:
-
回滾事務時,恢復到修改前的數據。
-
實現 MVCC 。
事務四大特性中原子性也是基於Undo Log實現的。
下麵開始談一下MVCC的實現原理。
4. MVCC的實現原理
4.1 當前讀和快照讀
先普及一下什麼是當前讀和快照讀。
當前讀: 讀取數據的最新版本,並對數據進行加鎖。
例如:insert、update、delete、select for update、 select lock in share mode。
快照讀: 讀取數據的歷史版本,不對數據加鎖。
例如:select
MVCC是基於Undo Log、隱藏欄位、Read View(讀視圖)實現的。
4.2 隱藏欄位
先說一下MySQL的隱藏欄位,當我們創建一張表時,InnoDB引擎會增加2個隱藏欄位。
DB_TRX_ID(最近一次提交事務的ID):修改表數據時,都會提交事務,每個事務都有一個唯一的ID,這個欄位就記錄了最近一次提交事務的ID。
DB_ROLL_PTR(上個版本的地址):修改表數據時,舊版本的數據都會被記錄到Undo Log日誌中,每個版本的數據都有一個版本地址,這個欄位記錄的就是上個版本的地址。
4.3 版本鏈
當我們第一次往用戶表插入一條記錄時,表數據和隱藏欄位的值是下麵這樣的:
insert into user (name,age) values ('一燈',1);
事務ID(DB_TRX_ID)是1,上個版本地址(DB_ROLL_PTR)是null。
第二次提交事務,把用戶年齡加1。
update user set age=age+1 where id=1;
事務ID變成2,上個版本地址指向Undo Log中的記錄。
第三次提交事務,再把用戶年齡加1。
update user set age=age+1 where id=1;
事務ID變成3,上個版本地址指向Undo Log中事務ID為2的記錄。
這樣表記錄和Undo Log歷史數據就組成了一個版本鏈。
4.4 Read View(讀視圖)
在事務中,執行SQL查詢,就會生成一個讀視圖,是用來保證數據的可見性,即讀到Undo Log中哪個版本的數據。
快照讀一般是讀取的歷史版本的讀視圖,當前圖會生成一個最新版本的讀視圖。
讀視圖是基於下麵幾個欄位實現的:
m_ids :當前系統中活躍的事務ID集合,即未提交的事務。
min_trx_id :m_ids中最小的ID
max_trx_id :下一個要分配的事務ID
creator_trx_id: 當前事務ID
讀視圖決定當前事務能讀到哪個版本的數據,從表記錄到Undo Log歷史數據的版本鏈,依次匹配,滿足哪個版本的匹配規則,就能讀到哪個版本的數據,一旦匹配成功就不再往下匹配。
數據可見性規則:
- DB_TRX_ID = creator_trx_id
如果這個版本數據的事務ID等於當前事務ID,表示數據記錄的最後一次操作的事務就是當前事務,當前讀視圖可以讀到這個版本的數據。 - DB_TRX_ID < min_trx_id
如果這個版本數據的事務ID小於所有活躍事務ID,表示這個版本的數據不再被事務使用,即事務已提交,當前讀視圖可以讀到這個版本的數據。 - DB_TRX_ID >= max_trx_id
如果這個版本數據的事務ID大於等於下一個要分配的事務ID,表示有新事務更新了這個版本的數據,這種情況下,當前讀視圖不可以讀到這個版本的數據。 - min_trx_id <= DB_TRX_ID < max_trx_id
如果這個版本數據的事務ID在當前系統中活躍的事務ID集合(m_ids)裡面,表示這個版本的數據被其他事務更新過,當前讀視圖不可以讀到這個版本的數據。
如果這個版本數據的事務ID不在當前系統中活躍的事務ID集合(m_ids)裡面,表示是在其他事務提交後創建的讀視圖,當前讀視圖可以讀到這個版本的數據。
5. 不同隔離級別下可見性分析
在不同的事務隔離級別下,生成讀視圖的規則不同:
- READ COMMITTED(讀已提交) :在事務中每一次執行快照讀時都生成一個讀視圖,每個讀視圖中四個欄位的值都是不同的。
- REPEATABLE READ(可重覆讀):僅在事務中第一次執行快照讀時生成讀視圖,後續復用這個讀視圖。
5.1 READ COMMITTED(讀已提交)
設置MySQL隔離級別為讀已提交:
SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
執行兩個事務,驗證一下:
事務1第一次查詢時,會生成一個讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1,2 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數據是:
符號規則 DB_TRX_ID = creator_trx_id = 1
,可以看到當前版本的數據。
事務1第二次查詢時,會生成一個新的讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數據是:
符號規則 min_trx_id <= DB_TRX_ID < max_trx_id(1<=2<3)
,並且當前數據版本的事務ID不在當前系統中活躍的事務ID集合,可以看到當前版本的數據。
同一個事務內,相同的查詢條件,查詢到的數據不一致,查到了其他事務更新過的數據,也就是出現了不可重覆讀的情況。
再看一下,在可重覆讀隔離級別下,是怎麼解決這個問題的。
5.2 REPEATABLE READ(可重覆讀)
設置MySQL隔離級別為可重覆讀:
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
執行兩個事務,驗證一下:
事務1第一次查詢時,會生成一個讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1,2 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數據是:
符號規則 DB_TRX_ID = creator_trx_id = 1
,可以看到當前版本的數據。
事務1第二次查詢時,會復用原有的讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1,2 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數據是:
符號規則 min_trx_id <= DB_TRX_ID < max_trx_id(1<=2<3)
,並且當前數據版本的事務ID在當前系統中活躍的事務ID集合,所以是不可以看到當前版本的數據。
由此得知,可重覆讀隔離級別下,相同的查詢條件,兩次查詢到的結果相同,也就是解決了可重覆讀的問題,是通過復用原有的讀視圖的方式解決的。