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