溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

分析MySQL數(shù)據(jù)庫Innodb中的事務(wù)隔離級別和鎖的關(guān)系

發(fā)布時間:2021-11-08 10:46:05 來源:億速云 閱讀:137 作者:iii 欄目:MySQL數(shù)據(jù)庫

本篇內(nèi)容主要講解“分析MySQL數(shù)據(jù)庫Innodb中的事務(wù)隔離級別和鎖的關(guān)系”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“分析MySQL數(shù)據(jù)庫Innodb中的事務(wù)隔離級別和鎖的關(guān)系”吧!




update class_teacher set class_name='初三三班' where id=1;


commit;

select id,class_name,teacher_id from class_teacher where teacher_id=1;

idclass_nameteacher_id1初三三班12初三一班1

讀到了事務(wù)B修改的數(shù)據(jù),和第一次查詢的結(jié)果不一樣,是不可重讀的。


commit;


事務(wù)B修改id=1的數(shù)據(jù)提交之后,事務(wù)A同樣的查詢,后一次和前一次的結(jié)果不一樣,這就是不可重讀(重新讀取產(chǎn)生的結(jié)果不一樣)。這就很可能帶來一些問題,那么我們來看看在RR級別中MySQL的表現(xiàn):

事務(wù)A事務(wù)B事務(wù)C
begin;

begin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=1;

idclass_nameteacher_id1初三二班12初三一班1



update class_teacher set class_name='初三三班' where id=1;

commit;

 




insert into class_teacher values (null,'初三三班',1);

 

commit;

select id,class_name,teacher_id from class_teacher where teacher_id=1;

idclass_nameteacher_id1初三二班12初三一班1

沒有讀到事務(wù)B修改的數(shù)據(jù),和第一次sql讀取的一樣,是可重復(fù)讀的。

沒有讀到事務(wù)C新添加的數(shù)據(jù)。



commit;

我們注意到,當(dāng)teacher_id=1時,事務(wù)A先做了一次讀取,事務(wù)B中間修改了id=1的數(shù)據(jù),并commit之后,事務(wù)A第二次讀到的數(shù)據(jù)和第一次完全相同。所以說它是可重讀的。那么MySQL是怎么做到的呢?這里姑且賣個關(guān)子,我們往下看。

####不可重復(fù)讀和幻讀的區(qū)別####
很多人容易搞混不可重復(fù)讀和幻讀,確實(shí)這兩者有些相似。但不可重復(fù)讀重點(diǎn)在于update和delete,而幻讀的重點(diǎn)在于insert。

如果使用鎖機(jī)制來實(shí)現(xiàn)這兩種隔離級別,在可重復(fù)讀中,該sql第一次讀取到數(shù)據(jù)后,就將這些數(shù)據(jù)加鎖,其它事務(wù)無法修改這些數(shù)據(jù),就可以實(shí)現(xiàn)可重復(fù)讀了。但這種方法卻無法鎖住insert的數(shù)據(jù),所以當(dāng)事務(wù)A先前讀取了數(shù)據(jù),或者修改了全部數(shù)據(jù),事務(wù)B還是可以insert數(shù)據(jù)提交,這時事務(wù)A就會發(fā)現(xiàn)莫名其妙多了一條之前沒有的數(shù)據(jù),這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這么做可以有效的避免幻讀、不可重復(fù)讀、臟讀等問題,但會極大的降低數(shù)據(jù)庫的并發(fā)能力。

所以說不可重復(fù)讀和幻讀最大的區(qū)別,就在于如何通過鎖機(jī)制來解決他們產(chǎn)生的問題。

上文說的,是使用悲觀鎖機(jī)制來處理這兩種問題,但是MySQL、ORACLE、PostgreSQL等成熟的數(shù)據(jù)庫,出于性能考慮,都是使用了以樂觀鎖為理論基礎(chǔ)的MVCC(多版本并發(fā)控制)來避免這兩種問題。

####悲觀鎖和樂觀鎖####

  • 悲觀鎖

正如其名,它指的是對數(shù)據(jù)被外界(包括本系統(tǒng)當(dāng)前的其他事務(wù),以及來自外部系統(tǒng)的事務(wù)處理)修改持保守態(tài)度,因此,在整個數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制(也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在本系統(tǒng)中實(shí)現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù))。

在悲觀鎖的情況下,為了保證事務(wù)的隔離性,就需要一致性鎖定讀。讀取數(shù)據(jù)時給加鎖,其它事務(wù)無法修改這些數(shù)據(jù)。修改刪除數(shù)據(jù)時也要加鎖,其它事務(wù)無法讀取這些數(shù)據(jù)。

  • 樂觀鎖

相對悲觀鎖而言,樂觀鎖機(jī)制采取了更加寬松的加鎖機(jī)制。悲觀鎖大多數(shù)情況下依靠數(shù)據(jù)庫的鎖機(jī)制實(shí)現(xiàn),以保證操作最大程度的獨(dú)占性。但隨之而來的就是數(shù)據(jù)庫性能的大量開銷,特別是對長事務(wù)而言,這樣的開銷往往無法承受。

而樂觀鎖機(jī)制在一定程度上解決了這個問題。樂觀鎖,大多是基于數(shù)據(jù)版本( Version )記錄機(jī)制實(shí)現(xiàn)。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個版本標(biāo)識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表增加一個 “version” 字段來實(shí)現(xiàn)。讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數(shù)據(jù)的版本數(shù)據(jù)與數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本信息進(jìn)行比對,如果提交的數(shù)據(jù)版本號大于數(shù)據(jù)庫表當(dāng)前版本號,則予以更新,否則認(rèn)為是過期數(shù)據(jù)。

要說明的是,MVCC的實(shí)現(xiàn)沒有固定的規(guī)范,每個數(shù)據(jù)庫都會有不同的實(shí)現(xiàn)方式,這里討論的是InnoDB的MVCC。

####MVCC在MySQL的InnoDB中的實(shí)現(xiàn)
在InnoDB中,會在每行數(shù)據(jù)后添加兩個額外的隱藏的值來實(shí)現(xiàn)MVCC,這兩個值一個記錄這行數(shù)據(jù)何時被創(chuàng)建,另外一個記錄這行數(shù)據(jù)何時過期(或者被刪除)。 在實(shí)際操作中,存儲的并不是時間,而是事務(wù)的版本號,每開啟一個新事務(wù),事務(wù)的版本號就會遞增。 在可重讀Repeatable reads事務(wù)隔離級別下:

  • SELECT時,讀取創(chuàng)建版本號<=當(dāng)前事務(wù)版本號,刪除版本號為空或>當(dāng)前事務(wù)版本號。

  • INSERT時,保存當(dāng)前事務(wù)版本號為行的創(chuàng)建版本號

  • DELETE時,保存當(dāng)前事務(wù)版本號為行的刪除版本號

  • UPDATE時,插入一條新紀(jì)錄,保存當(dāng)前事務(wù)版本號為行創(chuàng)建版本號,同時保存當(dāng)前事務(wù)版本號到原來刪除的行

通過MVCC,雖然每行記錄都需要額外的存儲空間,更多的行檢查工作以及一些額外的維護(hù)工作,但可以減少鎖的使用,大多數(shù)讀操作都不用加鎖,讀數(shù)據(jù)操作很簡單,性能很好,并且也能保證只會讀取到符合標(biāo)準(zhǔn)的行,也只鎖住必要行。

我們不管從數(shù)據(jù)庫方面的教課書中學(xué)到,還是從網(wǎng)絡(luò)上看到,大都是上文中事務(wù)的四種隔離級別這一模塊列出的意思,RR級別是可重復(fù)讀的,但無法解決幻讀,而只有在Serializable級別才能解決幻讀。于是我就加了一個事務(wù)C來展示效果。在事務(wù)C中添加了一條teacher_id=1的數(shù)據(jù)commit,RR級別中應(yīng)該會有幻讀現(xiàn)象,事務(wù)A在查詢teacher_id=1的數(shù)據(jù)時會讀到事務(wù)C新加的數(shù)據(jù)。但是測試后發(fā)現(xiàn),在MySQL中是不存在這種情況的,在事務(wù)C提交后,事務(wù)A還是不會讀到這條數(shù)據(jù)??梢娫贛ySQL的RR級別中,是解決了幻讀的讀問題的。參見下圖

分析MySQL數(shù)據(jù)庫Innodb中的事務(wù)隔離級別和鎖的關(guān)系

讀問題解決了,根據(jù)MVCC的定義,并發(fā)提交數(shù)據(jù)時會出現(xiàn)沖突,那么沖突時如何解決呢?我們再來看看InnoDB中RR級別對于寫數(shù)據(jù)的處理。

####“讀”與“讀”的區(qū)別
可能有讀者會疑惑,事務(wù)的隔離級別其實(shí)都是對于讀數(shù)據(jù)的定義,但到了這里,就被拆成了讀和寫兩個模塊來講解。這主要是因?yàn)镸ySQL中的讀,和事務(wù)隔離級別中的讀,是不一樣的。

我們且看,在RR級別中,通過MVCC機(jī)制,雖然讓數(shù)據(jù)變得可重復(fù)讀,但我們讀到的數(shù)據(jù)可能是歷史數(shù)據(jù),是不及時的數(shù)據(jù),不是數(shù)據(jù)庫當(dāng)前的數(shù)據(jù)!這在一些對于數(shù)據(jù)的時效特別敏感的業(yè)務(wù)中,就很可能出問題。

對于這種讀取歷史數(shù)據(jù)的方式,我們叫它快照讀 (snapshot read),而讀取數(shù)據(jù)庫當(dāng)前版本數(shù)據(jù)的方式,叫當(dāng)前讀 (current read)。很顯然,在MVCC中:

  • 快照讀:就是select

    • select * from table ....;

  • 當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,處理的都是當(dāng)前的數(shù)據(jù),需要加鎖。

    • select * from table where ? lock in share mode;

    • select * from table where ? for update;

    • insert;

    • update ;

    • delete;

事務(wù)的隔離級別實(shí)際上都是定義了當(dāng)前讀的級別,MySQL為了減少鎖處理(包括等待其它鎖)的時間,提升并發(fā)能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些“當(dāng)前讀”,就需要另外的模塊來解決了。

###寫("當(dāng)前讀")
事務(wù)的隔離級別中雖然只定義了讀數(shù)據(jù)的要求,實(shí)際上這也可以說是寫數(shù)據(jù)的要求。上文的“讀”,實(shí)際是講的快照讀;而這里說的“寫”就是當(dāng)前讀了。
為了解決當(dāng)前讀中的幻讀問題,MySQL事務(wù)使用了Next-Key鎖。

####Next-Key鎖
Next-Key鎖是行鎖和GAP(間隙鎖)的合并,行鎖上文已經(jīng)介紹了,接下來說下GAP間隙鎖。

行鎖可以防止不同事務(wù)版本的數(shù)據(jù)修改提交時造成數(shù)據(jù)沖突的情況。但如何避免別的事務(wù)插入數(shù)據(jù)就成了問題。我們可以看看RR級別和RC級別的對比

RC級別:

事務(wù)A事務(wù)B
begin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2初三二班30

 


update class_teacher set class_name='初三四班' where teacher_id=30;

insert into class_teacher values (null,'初三二班',30);

commit;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2初三四班3010初三二班30

 


RR級別:

事務(wù)A事務(wù)B
begin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2初三二班30

update class_teacher set class_name='初三四班' where teacher_id=30;

insert into class_teacher values (null,'初三二班',30);

waiting....

select id,class_name,teacher_id from class_teacher where teacher_id=30;

idclass_nameteacher_id2初三四班30

commit;事務(wù)Acommit后,事務(wù)B的insert執(zhí)行。

通過對比我們可以發(fā)現(xiàn),在RC級別中,事務(wù)A修改了所有teacher_id=30的數(shù)據(jù),但是當(dāng)事務(wù)Binsert進(jìn)新數(shù)據(jù)后,事務(wù)A發(fā)現(xiàn)莫名其妙多了一行teacher_id=30的數(shù)據(jù),而且沒有被之前的update語句所修改,這就是“當(dāng)前讀”的幻讀。

RR級別中,事務(wù)A在update后加鎖,事務(wù)B無法插入新數(shù)據(jù),這樣事務(wù)A在update前后讀的數(shù)據(jù)保持一致,避免了幻讀。這個鎖,就是Gap鎖。

MySQL是這么實(shí)現(xiàn)的:

在class_teacher這張表中,teacher_id是個索引,那么它就會維護(hù)一套B+樹的數(shù)據(jù)關(guān)系,為了簡化,我們用鏈表結(jié)構(gòu)來表達(dá)(實(shí)際上是個樹形結(jié)構(gòu),但原理相同)

分析MySQL數(shù)據(jù)庫Innodb中的事務(wù)隔離級別和鎖的關(guān)系

如圖所示,InnoDB使用的是聚集索引,teacher_id身為二級索引,就要維護(hù)一個索引字段和主鍵id的樹狀結(jié)構(gòu)(這里用鏈表形式表現(xiàn)),并保持順序排列。

Innodb將這段數(shù)據(jù)分成幾個個區(qū)間

  • (negative infinity, 5],

  • (5,30],

  • (30,positive infinity);

update class_teacher set class_name='初三四班' where teacher_id=30;不僅用行鎖,鎖住了相應(yīng)的數(shù)據(jù)行;同時也在兩邊的區(qū)間,(5,30]和(30,positive infinity),都加入了gap鎖。這樣事務(wù)B就無法在這個兩個區(qū)間insert進(jìn)新數(shù)據(jù)。

受限于這種實(shí)現(xiàn)方式,Innodb很多時候會鎖住不需要鎖的區(qū)間。如下所示:

事務(wù)A事務(wù)B事務(wù)C
begin;begin;begin;

select id,class_name,teacher_id from class_teacher;

idclass_nameteacher_id1初三一班52初三二班30


update class_teacher set class_name='初一一班' where teacher_id=20;


insert into class_teacher values (null,'初三五班',10);

waiting .....

insert into class_teacher values (null,'初三五班',40);
commit;事務(wù)A commit之后,這條語句才插入成功commit;

commit;

update的teacher_id=20是在(5,30]區(qū)間,即使沒有修改任何數(shù)據(jù),Innodb也會在這個區(qū)間加gap鎖,而其它區(qū)間不會影響,事務(wù)C正常插入。

如果使用的是沒有索引的字段,比如update class_teacher set teacher_id=7 where class_name='初三八班(即使沒有匹配到任何數(shù)據(jù))',那么會給全表加入gap鎖。同時,它不能像上文中行鎖一樣經(jīng)過MySQL Server過濾自動解除不滿足條件的鎖,因?yàn)闆]有索引,則這些字段也就沒有排序,也就沒有區(qū)間。除非該事務(wù)提交,否則其它事務(wù)無法插入任何數(shù)據(jù)。

行鎖防止別的事務(wù)修改或刪除,GAP鎖防止別的事務(wù)新增,行鎖和GAP鎖結(jié)合形成的的Next-Key鎖共同解決了RR級別在寫數(shù)據(jù)時的幻讀問題。

###Serializable
這個級別很簡單,讀加共享鎖,寫加排他鎖,讀寫互斥。使用的悲觀鎖的理論,實(shí)現(xiàn)簡單,數(shù)據(jù)更加安全,但是并發(fā)能力非常差。如果你的業(yè)務(wù)并發(fā)的特別少或者沒有并發(fā),同時又要求數(shù)據(jù)及時可靠的話,可以使用這種模式。

這里要吐槽一句,不要看到select就說不會加鎖了,在Serializable這個級別,還是會加鎖的!

到此,相信大家對“分析MySQL數(shù)據(jù)庫Innodb中的事務(wù)隔離級別和鎖的關(guān)系”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI