MySQL事務(wù)的隔離性是如何實(shí)現(xiàn)的
并發(fā)場景
最近做了一些分布式事務(wù)的項(xiàng)目,對(duì)事務(wù)的隔離性有了更深的認(rèn)識(shí),后續(xù)寫文章聊分布式事務(wù)。今天就復(fù)盤一下單機(jī)事務(wù)的隔離性是如何實(shí)現(xiàn)的?
隔離的本質(zhì)就是控制并發(fā),如果SQL語句就是串行執(zhí)行的。那么數(shù)據(jù)庫的四大特性中就不會(huì)有隔離性這個(gè)概念了,也就不會(huì)有臟讀,不可重復(fù)讀,幻讀等各種問題了
對(duì)數(shù)據(jù)庫的各種并發(fā)操作,只有如下四種,寫寫,讀讀,讀寫和寫讀
寫-寫
事務(wù)A更新一條記錄的時(shí)候,事務(wù)B能同時(shí)更新同一條記錄嗎?
答案肯定是不能的,不然就會(huì)造成臟寫問題,那如何避免臟寫呢?答案就是加鎖
讀-讀
MySQL讀操作默認(rèn)情況下不會(huì)加鎖,所以可以并行的讀
讀-寫 和 寫-讀
基于各種場景對(duì)并發(fā)操作容忍程度不同,MySQL就搞了個(gè)隔離性的概念。你自己根據(jù)業(yè)務(wù)場景選擇隔離級(jí)別。
√ 為會(huì)發(fā)生,×為不會(huì)發(fā)生
隔離級(jí)別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
read uncommitted(未提交讀) | √ | √ | √ |
read committed(提交讀) | × | √ | √ |
repeatable read(可重復(fù)讀) | × | × | √ |
serializable (可串行化) | × | × | × |
所以你看,MySQL是通過鎖和隔離級(jí)別對(duì)MySQL進(jìn)行并發(fā)控制的
MySQL中的鎖
行級(jí)鎖
InnoDB存儲(chǔ)引擎中有如下兩種類型的行級(jí)鎖
- 共享鎖(Shared Lock,簡稱S鎖),在事務(wù)需要讀取一條記錄時(shí),需要先獲取改記錄的S鎖
- 排他鎖(Exclusive Lock,簡稱X鎖),在事務(wù)要改動(dòng)一條記錄時(shí),需要先獲取該記錄的X鎖
如果事務(wù)T1獲取了一條記錄的S鎖之后,事務(wù)T2也要訪問這條記錄。如果事務(wù)T2想再獲取這個(gè)記錄的S鎖,可以成功,這種情況稱為鎖兼容,如果事務(wù)T2想再獲取這個(gè)記錄的X鎖,那么此操作會(huì)被阻塞,直到事務(wù)T1提交之后將S鎖釋放掉
如果事務(wù)T1獲取了一條記錄的X鎖之后,那么不管事務(wù)T2接著想獲取該記錄的S鎖還是X鎖都會(huì)被阻塞,直到事務(wù)1提交,這種情況稱為鎖不兼容。
多個(gè)事務(wù)可以同時(shí)讀取記錄,即共享鎖之間不互斥,但共享鎖會(huì)阻塞排他鎖。排他鎖之間互斥
S鎖和X鎖之間的兼容關(guān)系如下
兼容性 | X鎖 | S鎖 |
---|---|---|
X鎖 | 互斥 | 互斥 |
S鎖 | 互斥 | 兼容 |
update,delete,insert 都會(huì)自動(dòng)給涉及到的數(shù)據(jù)加上排他鎖,select 語句默認(rèn)不會(huì)加任何鎖
那什么情況下會(huì)對(duì)讀操作加鎖呢?
- select … lock in share mode,對(duì)讀取的記錄加S鎖
- select … for update ,對(duì)讀取的記錄加X鎖
- 在事務(wù)中讀取記錄,對(duì)讀取的記錄加S鎖
- 事務(wù)隔離級(jí)別在 SERIALIZABLE 下,對(duì)讀取的記錄加S鎖
InnoDB中有如下三種鎖
- Record Lock:對(duì)單個(gè)記錄加鎖
- Gap Lock:間隙鎖,鎖住記錄前面的間隙,不允許插入記錄
- Next-key Lock:同時(shí)鎖住數(shù)據(jù)和數(shù)據(jù)前面的間隙,即數(shù)據(jù)和數(shù)據(jù)前面的間隙都不允許插入記錄
寫個(gè)Demo演示一下
CREATE TABLE `girl` ( `id` int(11) NOT NULL, `name` varchar(255), `age` int(11), PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into girl values (1, '西施', 20), (5, '王昭君', 23), (8, '貂蟬', 25), (10, '楊玉環(huán)', 26), (12, '陳圓圓', 20);
Record Lock
對(duì)單個(gè)記錄加鎖
如把id值為8的數(shù)據(jù)加一個(gè)Record Lock,示意圖如下
Record Lock也是有S鎖和X鎖之分的,兼容性和之前描述的一樣。
SQL執(zhí)行加什么樣的鎖受很多條件的制約,比如事務(wù)的隔離級(jí)別,執(zhí)行時(shí)使用的索引(如,聚集索引,非聚集索引等),因此就不詳細(xì)分析了,舉幾個(gè)簡單的例子。
-- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ 利用主鍵進(jìn)行等值查詢 -- 對(duì)id=8的記錄加S型Record Lock select * from girl where id = 8 lock in share mode; -- READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ 利用主鍵進(jìn)行等值查詢 -- 對(duì)id=8的記錄加X型Record Lock select * from girl where id = 8 for update;
Gap Lock
鎖住記錄前面的間隙,不允許插入記錄
MySQL在可重復(fù)讀隔離級(jí)別下可以通過MVCC和加鎖來解決幻讀問題
當(dāng)前讀:加鎖
快照讀:MVCC
但是該如何加鎖呢?因?yàn)榈谝淮螆?zhí)行讀取操作的時(shí)候,這些幻影記錄并不存在,我們沒有辦法加Record Lock,此時(shí)可以通過加Gap Lock解決,即對(duì)間隙加鎖。
如一個(gè)事務(wù)對(duì)id=8的記錄加間隙鎖,則意味著不允許別的事務(wù)在id=8的記錄前面的間隙插入新記錄,即id值在(5, 8)這個(gè)區(qū)間內(nèi)的記錄是不允許立即插入的。直到加間隙鎖的事務(wù)提交后,id值在(5, 8)這個(gè)區(qū)間中的記錄才可以被提交
我們來看如下一個(gè)SQL的加鎖過程
-- REPEATABLE READ 利用主鍵進(jìn)行等值查詢 -- 但是主鍵值并不存在 -- 對(duì)id=8的聚集索引記錄加Gap Lock SELECT * FROM girl WHERE id = 7 LOCK IN SHARE MODE;
由于id=7的記錄不存在,為了禁止幻讀現(xiàn)象(避免在同一事務(wù)下執(zhí)行相同的語句得到的結(jié)果集中有id=7的記錄),所以在當(dāng)前事務(wù)提交前我們要預(yù)防別的事務(wù)插入id=7的記錄,此時(shí)在id=8的記錄上加一個(gè)Gap Lock即可,即不允許別的事務(wù)插入id值在(5, 8)這個(gè)區(qū)間的新記錄
給大家提一個(gè)問題,Gap Lock只能鎖定記錄前面的間隙,那么最后一條記錄后面的間隙該怎么鎖定?
其實(shí)mysql數(shù)據(jù)是存在頁中的,每個(gè)頁有2個(gè)偽記錄
- Infimum記錄,表示該頁面中最小的記錄
- upremum記錄,表示該頁面中最大的記錄
為了防止其它事務(wù)插入id值在(12, +∞)這個(gè)區(qū)間的記錄,我們可以給id=12記錄所在頁面的Supremum記錄加上一個(gè)gap鎖,此時(shí)就可以阻止其他事務(wù)插入id值在(12, +∞)這個(gè)區(qū)間的新記錄
Next-key Lock
同時(shí)鎖住數(shù)據(jù)和數(shù)據(jù)前面的間隙,即數(shù)據(jù)和數(shù)據(jù)前面的間隙都不允許插入記錄
所以你可以這樣理解Next-key Lock=Record Lock+Gap Lock
-- REPEATABLE READ 利用主鍵進(jìn)行范圍查詢 -- 對(duì)id=8的聚集索引記錄加S型Record Lock -- 對(duì)id>8的所有聚集索引記錄加S型Next-key Lock(包括Supremum偽記錄) SELECT * FROM girl WHERE id >= 8 LOCK IN SHARE MODE;
因?yàn)橐鉀Q幻讀的問題,所以需要禁別的事務(wù)插入id>=8的記錄,所以
- 對(duì)id=8的聚集索引記錄加S型Record Lock
- 對(duì)id>8的所有聚集索引記錄加S型Next-key Lock(包括Supremum偽記錄)
表級(jí)鎖
表鎖也有S鎖和X鎖之分
在對(duì)某個(gè)表執(zhí)行select,insert,update,delete語句時(shí),innodb存儲(chǔ)引擎是不會(huì)為這個(gè)表添加表級(jí)別的S鎖或者X鎖。
在對(duì)表執(zhí)行一些諸如ALTER TABLE,DROP TABLE這類的DDL語句時(shí),會(huì)對(duì)這個(gè)表加X鎖,因此其他事務(wù)對(duì)這個(gè)表執(zhí)行諸如SELECT INSERT UPDATE DELETE的語句會(huì)發(fā)生阻塞
在系統(tǒng)變量autocommit=0,innodb_table_locks = 1時(shí),手動(dòng)獲取InnoDB存儲(chǔ)引擎提供的表t的S鎖或者X鎖,可以這么寫
對(duì)表t加表級(jí)別的S鎖
lock tables t read
對(duì)表t加表級(jí)別的X鎖
lock tables t write
如果一個(gè)事務(wù)給表加了S鎖,那么
- 別的事務(wù)可以繼續(xù)獲得該表的S鎖
- 別的事務(wù)可以繼續(xù)獲得表中某些記錄的S鎖
- 別的事務(wù)不可以繼續(xù)獲得該表的X鎖
- 別的事務(wù)不可以繼續(xù)獲得表中某些記錄的X鎖
如果一個(gè)事務(wù)給表加了X鎖,那么
- 別的事務(wù)不可以繼續(xù)獲得該表的S鎖
- 別的事務(wù)不可以繼續(xù)獲得表中某些記錄的S鎖
- 別的事務(wù)不可以繼續(xù)獲得該表的X鎖
- 別的事務(wù)不可以繼續(xù)獲得表中某些記錄的X鎖
所以修改線上的表時(shí)一定要小心,因?yàn)闀?huì)使大量事務(wù)阻塞,目前有很多成熟的修改線上表的方法,不再贅述
隔離級(jí)別
讀未提交:每次讀取最新的記錄,沒有做特殊處理
串行化:事務(wù)串行執(zhí)行,不會(huì)產(chǎn)生并發(fā)
所以我們重點(diǎn)關(guān)注讀已提交和可重復(fù)讀的隔離實(shí)現(xiàn)!
這兩種隔離級(jí)別是通過MVCC(多版本并發(fā)控制)來實(shí)現(xiàn)的,本質(zhì)就是MySQL通過undolog存儲(chǔ)了多個(gè)版本的歷史數(shù)據(jù),根據(jù)規(guī)則讀取某一歷史版本的數(shù)據(jù),這樣就可以在無鎖的情況下實(shí)現(xiàn)讀寫并行,提高數(shù)據(jù)庫性能
那么undolog是如何存儲(chǔ)修改前的記錄?
對(duì)于使用InnoDB存儲(chǔ)引擎的表來說,聚集索引記錄中都包含下面2個(gè)必要的隱藏列
trx_id:一個(gè)事務(wù)每次對(duì)某條聚集索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù)id賦值給trx_id隱藏列
roll_pointer:每次對(duì)某條聚集索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫入undo日志中。這個(gè)隱藏列就相當(dāng)于一個(gè)指針,通過他找到該記錄修改前的信息
如果一個(gè)記錄的name從貂蟬被依次改為王昭君,西施,會(huì)有如下的記錄,多個(gè)記錄構(gòu)成了一個(gè)版本鏈
為了判斷版本鏈中哪個(gè)版本對(duì)當(dāng)前事務(wù)是可見的,MySQL設(shè)計(jì)出了ReadView的概念。4個(gè)重要的內(nèi)容如下
- m_ids:在生成ReadView時(shí),當(dāng)前系統(tǒng)中活躍的事務(wù)id列表
- min_trx_id:在生成ReadView時(shí),當(dāng)前系統(tǒng)中活躍的最小的事務(wù)id,也就是m_ids中的最小值
- max_trx_id:在生成ReadView時(shí),系統(tǒng)應(yīng)該分配給下一個(gè)事務(wù)的事務(wù)id值
- creator_trx_id:生成該ReadView的事務(wù)的事務(wù)id
當(dāng)對(duì)表中的記錄進(jìn)行改動(dòng)時(shí),執(zhí)行insert,delete,update這些語句時(shí),才會(huì)為事務(wù)分配唯一的事務(wù)id,否則一個(gè)事務(wù)的事務(wù)id值默認(rèn)為0。
max_trx_id并不是m_ids中的最大值,事務(wù)id是遞增分配的。比如現(xiàn)在有事務(wù)id為1,2,3這三個(gè)事務(wù),之后事務(wù)id為3的事務(wù)提交了,當(dāng)有一個(gè)新的事務(wù)生成ReadView時(shí),m_ids的值就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4
執(zhí)行過程如下:
- 如果被訪問版本的trx_id=creator_id,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問
- 如果被訪問版本的trx_id<min_trx_id,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問
- 被訪問版本的trx_id>=max_trx_id,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟,該版本不可以被當(dāng)前事務(wù)訪問
- 被訪問版本的trx_id是否在m_ids列表中
- 4.1 是,創(chuàng)建ReadView時(shí),該版本還是活躍的,該版本不可以被訪問。順著版本鏈找下一個(gè)版本的數(shù)據(jù),繼續(xù)執(zhí)行上面的步驟判斷可見性,如果最后一個(gè)版本還不可見,意味著記錄對(duì)當(dāng)前事務(wù)完全不可見
- 4.2 否,創(chuàng)建ReadView時(shí),生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問
好了,我們知道了版本可見性的獲取規(guī)則,那么是怎么實(shí)現(xiàn)讀已提交和可重復(fù)讀的呢?
其實(shí)很簡單,就是生成ReadView的時(shí)機(jī)不同
舉個(gè)例子,先建立如下表
CREATE TABLE `girl` ( `id` int(11) NOT NULL, `name` varchar(255), `age` int(11), PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Read Committed
Read Committed(讀已提交),每次讀取數(shù)據(jù)前都生成一個(gè)ReadView
下面是3個(gè)事務(wù)執(zhí)行的過程,一行代表一個(gè)時(shí)間點(diǎn)
先分析一下5這個(gè)時(shí)間點(diǎn)select的執(zhí)行過程
- 系統(tǒng)中有兩個(gè)事務(wù)id分別為100,200的事務(wù)正在執(zhí)行
- 執(zhí)行select語句時(shí)生成一個(gè)ReadView,mids=[100,200],min_trx_id=100,max_trx_id=201,creator_trx_id=0(select這個(gè)事務(wù)沒有執(zhí)行更改操作,事務(wù)id默認(rèn)為0)
- 最新版本的name列為西施,該版本trx_id值為100,在mids列表中,不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本
- 下一個(gè)版本的name列王昭君,該版本的trx_id值為100,也在mids列表內(nèi),因此也不符合要求,繼續(xù)跳到下一個(gè)版本
- 下一個(gè)版本的name列為貂蟬,該版本的trx_id值為10,小于min_trx_id,因此最后返回的name值為貂蟬
再分析一下8這個(gè)時(shí)間點(diǎn)select的執(zhí)行過程
- 系統(tǒng)中有一個(gè)事務(wù)id為200的事務(wù)正在執(zhí)行(事務(wù)id為100的事務(wù)已經(jīng)提交)
- 執(zhí)行select語句時(shí)生成一個(gè)ReadView,mids=[200],min_trx_id=200,max_trx_id=201,creator_trx_id=0
- 最新版本的name列為楊玉環(huán),該版本trx_id值為200,在mids列表中,不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本
- 下一個(gè)版本的name列為西施,該版本的trx_id值為100,小于min_trx_id,因此最后返回的name值為西施
當(dāng)事務(wù)id為200的事務(wù)提交時(shí),查詢得到的name列為楊玉環(huán)。
Repeatable Read
Repeatable Read(可重復(fù)讀),在第一次讀取數(shù)據(jù)時(shí)生成一個(gè)ReadView
可重復(fù)讀因?yàn)橹辉诘谝淮巫x取數(shù)據(jù)的時(shí)候生成ReadView,所以每次讀到的是相同的版本,即name值一直為貂蟬,具體的過程上面已經(jīng)演示了兩遍了,我這里就不重復(fù)演示了,相信你一定會(huì)自己分析了。
參考博客
[1]https://souche.yuque.com/bggh1p/8961260/gyzlaf
[2]https://zhuanlan.zhihu.com/p/35477890
到此這篇關(guān)于MySQL事務(wù)的隔離性是如何實(shí)現(xiàn)的的文章就介紹到這了,更多相關(guān)MySQL事務(wù)的隔離性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MySQL臟讀幻讀不可重復(fù)讀及事務(wù)的隔離級(jí)別和MVCC、LBCC實(shí)現(xiàn)
- 解決MySql8.0 查看事務(wù)隔離級(jí)別報(bào)錯(cuò)的問題
- 深入理解Mysql事務(wù)隔離級(jí)別與鎖機(jī)制問題
- MySQL系列之十 MySQL事務(wù)隔離實(shí)現(xiàn)并發(fā)控制
- 詳解MySQL中事務(wù)隔離級(jí)別的實(shí)現(xiàn)原理
- Mysql數(shù)據(jù)庫事務(wù)的臟讀幻讀及不可重復(fù)讀詳解
- 一文搞懂MySQL臟讀,幻讀和不可重復(fù)讀
- Mysql事務(wù)并發(fā)臟讀+不可重復(fù)讀+幻讀詳解
- Mysql事務(wù)的隔離級(jí)別(臟讀+幻讀+可重復(fù)讀)
相關(guān)文章
MySQL數(shù)據(jù)庫優(yōu)化技術(shù)之配置技巧總結(jié)
這篇文章主要介紹了MySQL數(shù)據(jù)庫優(yōu)化技術(shù)之配置技巧,較為詳細(xì)的總結(jié)分析了MySQL進(jìn)行硬件級(jí)軟件優(yōu)化的相關(guān)方法與注意事項(xiàng),需要的朋友可以參考下2016-07-07mysql mysqldump只導(dǎo)出表結(jié)構(gòu)或只導(dǎo)出數(shù)據(jù)的實(shí)現(xiàn)方法
mysql mysqldump只導(dǎo)出表結(jié)構(gòu)或只導(dǎo)出數(shù)據(jù)的實(shí)現(xiàn)方法,需要的朋友可以參考下。2011-11-11深入理解MySQL中MVCC與BufferPool緩存機(jī)制
這篇文章主要介紹了深入理解MySQL中MVCC與BufferPool緩存機(jī)制,MySQL默認(rèn)RR隔離級(jí)別就是通過該機(jī)制來保證的MVCC,更多主題相關(guān)內(nèi)容,需要的可以參考下面文章內(nèi)容介紹2022-05-05MySQL使用TEXT/BLOB類型的知識(shí)點(diǎn)詳解
在本篇文章里小編給大家整理的是關(guān)于MySQL使用TEXT/BLOB類型的幾點(diǎn)注意內(nèi)容,有興趣的朋友們學(xué)習(xí)下。2020-03-03MySQL特定表全量、增量數(shù)據(jù)同步到消息隊(duì)列-解決方案
mysql要同步原始全量數(shù)據(jù),也要實(shí)時(shí)同步MySQL特定庫的特定表增量數(shù)據(jù),同時(shí)對(duì)應(yīng)的修改、刪除也要對(duì)應(yīng),下面就為大家分享一下2021-11-11mysql日期和時(shí)間的間隔計(jì)算實(shí)例分析
這篇文章主要介紹了mysql日期和時(shí)間的間隔計(jì)算,結(jié)合實(shí)例形式分析了mysql日期和時(shí)間間隔計(jì)算的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2019-12-12MySQL報(bào)錯(cuò) table “xxx” doesn‘t exit的解決
本文主要介紹了MySQL報(bào)錯(cuò) table “xxx” doesn‘t exit的解決,主要原因是英文字母大小寫敏感導(dǎo)致,下面就來介紹一下解決方法,感興趣的可以了解一下2023-10-10