一文解析MySQL的MVCC實現(xiàn)原理
1. 什么是MVCC
MVCC全稱是Multi-Version Concurrency Control(多版本并發(fā)控制),是一種并發(fā)控制的方法,通過維護(hù)一個數(shù)據(jù)的多個版本,減少讀寫操作的沖突。
如果沒有MVCC,想要實現(xiàn)同一條數(shù)據(jù)的并發(fā)讀寫,還要保證數(shù)據(jù)的安全性,就需要操作數(shù)據(jù)的時候加讀鎖和寫鎖,這樣就降低了數(shù)據(jù)庫的并發(fā)性能。
有了MVCC,就相當(dāng)于把同一份數(shù)據(jù)生成了多個版本,在操作的開始各生成一個快照,讀寫操作互不影響。無需加鎖,也實現(xiàn)數(shù)據(jù)的安全性和事務(wù)的隔離性。
事務(wù)的四大特性中隔離性就是基于MVCC實現(xiàn)的。
說MVCC的實現(xiàn)原理之前,先說一下事務(wù)的隔離級別。
2. 事務(wù)的隔離級別
說隔離級別之前,先說一下并發(fā)事務(wù)產(chǎn)生的問題:
臟讀: 一個事務(wù)讀到其他事務(wù)未提交的數(shù)據(jù)。
不可重復(fù)讀: 相同的查詢條件,多次查詢到的結(jié)果不一致,即讀到其他事務(wù)提交后的數(shù)據(jù)。
幻讀: 相同的查詢條件,多次查詢到的結(jié)果不一致,即讀到其他事務(wù)提交后的數(shù)據(jù)。
不可重復(fù)讀與幻讀的區(qū)別是: 不可重復(fù)讀是讀到了其他事務(wù)執(zhí)行update、delete后的數(shù)據(jù),而幻讀是讀到其他事務(wù)執(zhí)行insert后的數(shù)據(jù)。
再說一下事務(wù)的四大隔離級別:
Read UnCommitted(讀未提交): 讀到其他事務(wù)未提交的數(shù)據(jù),會出現(xiàn)臟讀、不可重復(fù)讀、幻讀。
Read Committed(讀已提交): 讀到其他事務(wù)已提交的數(shù)據(jù),解決了臟讀,會出現(xiàn)不可重復(fù)讀、幻讀。
Repeatable Read(可重復(fù)讀): 相同的條件,多次讀取到的結(jié)果一致。解決了臟讀、不可重復(fù)讀,會出現(xiàn)幻讀。
Serializable(串行化): 所有事務(wù)串行執(zhí)行,解決了臟讀、不可重復(fù)讀、幻讀。
隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
讀未提交 | 會 | 會 | 會 |
讀已提交 | 不會 | 會 | 會 |
可重復(fù)讀 | 不會 | 不會 | 會 |
串行化 | 不會 | 不會 | 不會 |
MVCC只在Read Committed和Repeatable Read兩個隔離級別下起作用,因為Read UnCommitted隔離級別下,讀寫都不加鎖,Serializable隔離級別下,讀寫都加鎖,也就不需要MVCC了。
再談一下Undo log日志。
3. Undo Log(回滾日志)
Undo Log記錄的是邏輯日志,也就是SQL語句。
比如:當(dāng)我們執(zhí)行一條insert語句時,Undo Log就記錄一條相反的delete語句。
作用:
- 回滾事務(wù)時,恢復(fù)到修改前的數(shù)據(jù)。
- 實現(xiàn) MVCC 。
事務(wù)四大特性中原子性也是基于Undo Log實現(xiàn)的。
下面開始談一下MVCC的實現(xiàn)原理。
4. MVCC的實現(xiàn)原理
4.1 當(dāng)前讀和快照讀
先普及一下什么是當(dāng)前讀和快照讀。
當(dāng)前讀: 讀取數(shù)據(jù)的最新版本,并對數(shù)據(jù)進(jìn)行加鎖。
例如:insert、update、delete、select for update、 select lock in share mode。
快照讀: 讀取數(shù)據(jù)的歷史版本,不對數(shù)據(jù)加鎖。
例如:select
MVCC是基于Undo Log、隱藏字段、Read View(讀視圖)實現(xiàn)的。
4.2 隱藏字段
先說一下MySQL的隱藏字段,當(dāng)我們創(chuàng)建一張表時,InnoDB引擎會增加2個隱藏字段。
DB_TRX_ID(最近一次提交事務(wù)的ID):修改表數(shù)據(jù)時,都會提交事務(wù),每個事務(wù)都有一個唯一的ID,這個字段就記錄了最近一次提交事務(wù)的ID。
DB_ROLL_PTR(上個版本的地址):修改表數(shù)據(jù)時,舊版本的數(shù)據(jù)都會被記錄到Undo Log日志中,每個版本的數(shù)據(jù)都有一個版本地址,這個字段記錄的就是上個版本的地址。
4.3 版本鏈
當(dāng)我們第一次往用戶表插入一條記錄時,表數(shù)據(jù)和隱藏字段的值是下面這樣的:
insert into user (name,age) values ('一燈',1);
事務(wù)ID(DB_TRX_ID)是1,上個版本地址(DB_ROLL_PTR)是null。
第二次提交事務(wù),把用戶年齡加1。
update user set age=age+1 where id=1;
事務(wù)ID變成2,上個版本地址指向Undo Log中的記錄。
第三次提交事務(wù),再把用戶年齡加1。
update user set age=age+1 where id=1;
事務(wù)ID變成3,上個版本地址指向Undo Log中事務(wù)ID為2的記錄。
這樣表記錄和Undo Log歷史數(shù)據(jù)就組成了一個版本鏈。
4.4 Read View(讀視圖)
在事務(wù)中,執(zhí)行SQL查詢,就會生成一個讀視圖,是用來保證數(shù)據(jù)的可見性,即讀到Undo Log中哪個版本的數(shù)據(jù)。
快照讀一般是讀取的歷史版本的讀視圖,當(dāng)前圖會生成一個最新版本的讀視圖。
讀視圖是基于下面幾個字段實現(xiàn)的:
m_ids :當(dāng)前系統(tǒng)中活躍的事務(wù)ID集合,即未提交的事務(wù)。
min_trx_id :m_ids中最小的ID
max_trx_id :下一個要分配的事務(wù)ID
creator_trx_id: 當(dāng)前事務(wù)ID
讀視圖決定當(dāng)前事務(wù)能讀到哪個版本的數(shù)據(jù),從表記錄到Undo Log歷史數(shù)據(jù)的版本鏈,依次匹配,滿足哪個版本的匹配規(guī)則,就能讀到哪個版本的數(shù)據(jù),一旦匹配成功就不再往下匹配。
數(shù)據(jù)可見性規(guī)則:
- DB_TRX_ID = creator_trx_id 如果這個版本數(shù)據(jù)的事務(wù)ID等于當(dāng)前事務(wù)ID,表示數(shù)據(jù)記錄的最后一次操作的事務(wù)就是當(dāng)前事務(wù),當(dāng)前讀視圖可以讀到這個版本的數(shù)據(jù)。
- DB_TRX_ID < min_trx_id 如果這個版本數(shù)據(jù)的事務(wù)ID小于所有活躍事務(wù)ID,表示這個版本的數(shù)據(jù)不再被事務(wù)使用,即事務(wù)已提交,當(dāng)前讀視圖可以讀到這個版本的數(shù)據(jù)。
- DB_TRX_ID >= max_trx_id 如果這個版本數(shù)據(jù)的事務(wù)ID大于等于下一個要分配的事務(wù)ID,表示有新事務(wù)更新了這個版本的數(shù)據(jù),這種情況下,當(dāng)前讀視圖不可以讀到這個版本的數(shù)據(jù)。
- min_trx_id <= DB_TRX_ID < max_trx_id 如果這個版本數(shù)據(jù)的事務(wù)ID在當(dāng)前系統(tǒng)中活躍的事務(wù)ID集合(m_ids)里面,表示這個版本的數(shù)據(jù)被其他事務(wù)更新過,當(dāng)前讀視圖不可以讀到這個版本的數(shù)據(jù)。 如果這個版本數(shù)據(jù)的事務(wù)ID不在當(dāng)前系統(tǒng)中活躍的事務(wù)ID集合(m_ids)里面,表示是在其他事務(wù)提交后創(chuàng)建的讀視圖,當(dāng)前讀視圖可以讀到這個版本的數(shù)據(jù)。
5. 不同隔離級別下可見性分析
在不同的事務(wù)隔離級別下,生成讀視圖的規(guī)則不同:
- READ COMMITTED(讀已提交) :在事務(wù)中每一次執(zhí)行快照讀時都生成一個讀視圖,每個讀視圖中四個字段的值都是不同的。
- REPEATABLE READ(可重復(fù)讀):僅在事務(wù)中第一次執(zhí)行快照讀時生成讀視圖,后續(xù)復(fù)用這個讀視圖。
5.1 READ COMMITTED(讀已提交)
設(shè)置MySQL隔離級別為讀已提交:
SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
執(zhí)行兩個事務(wù),驗證一下:
事務(wù)1第一次查詢時,會生成一個讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1,2 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數(shù)據(jù)是:
符號規(guī)則 DB_TRX_ID = creator_trx_id = 1
,可以看到當(dāng)前版本的數(shù)據(jù)。
事務(wù)1第二次查詢時,會生成一個新的讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數(shù)據(jù)是:
符號規(guī)則 min_trx_id <= DB_TRX_ID < max_trx_id(1<=2<3)
,并且當(dāng)前數(shù)據(jù)版本的事務(wù)ID不在當(dāng)前系統(tǒng)中活躍的事務(wù)ID集合,可以看到當(dāng)前版本的數(shù)據(jù)。
同一個事務(wù)內(nèi),相同的查詢條件,查詢到的數(shù)據(jù)不一致,查到了其他事務(wù)更新過的數(shù)據(jù),也就是出現(xiàn)了不可重復(fù)讀的情況。
再看一下,在可重復(fù)讀隔離級別下,是怎么解決這個問題的。
5.2 REPEATABLE READ(可重復(fù)讀)
設(shè)置MySQL隔離級別為可重復(fù)讀:
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
執(zhí)行兩個事務(wù),驗證一下:
事務(wù)1第一次查詢時,會生成一個讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1,2 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數(shù)據(jù)是:
符號規(guī)則 DB_TRX_ID = creator_trx_id = 1
,可以看到當(dāng)前版本的數(shù)據(jù)。
事務(wù)1第二次查詢時,會復(fù)用原有的讀視圖,讀視圖的各個屬性如下:
屬性 | 值 |
---|---|
m_ids | 1,2 |
min_limit_id | 1 |
max_limit_id | 3 |
creator_trx_id | 1 |
可見的版本鏈數(shù)據(jù)是:
符號規(guī)則 min_trx_id <= DB_TRX_ID < max_trx_id(1<=2<3)
,并且當(dāng)前數(shù)據(jù)版本的事務(wù)ID在當(dāng)前系統(tǒng)中活躍的事務(wù)ID集合,所以是不可以看到當(dāng)前版本的數(shù)據(jù)。
由此得知,可重復(fù)讀隔離級別下,相同的查詢條件,兩次查詢到的結(jié)果相同,也就是解決了可重復(fù)讀的問題,是通過復(fù)用原有的讀視圖的方式解決的。
到此這篇關(guān)于一問解析MySQL的MVCC實現(xiàn)原理的文章就介紹到這了,更多相關(guān)MySQL MVCC實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深度解析MySQL啟動時報“The server quit without up
這篇文章主要介紹了MySQL啟動時報“The server quit without updating PID file”錯誤的原因,需要的朋友可以參考下2017-05-05mysql 卡死 大部分線程長時間處于sending data的狀態(tài)
首先說明一下,這是個無頭的案子,雖然問題貌似解決了,不過到現(xiàn)在我也沒有答案,只是把這個問題拿出來晾晾2008-11-11MySQL底層數(shù)據(jù)結(jié)構(gòu)選用B+樹的原因
大家好,本篇文章主要講的是MySQL底層數(shù)據(jù)結(jié)構(gòu)選用B+樹的原因,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12