重新學(xué)習(xí)MySQL數(shù)據(jù)庫(kù)9:Innodb中的事務(wù)隔離級(jí)別和鎖的關(guān)系

微信公眾號(hào)【黃小斜】大廠程序員,互聯(lián)網(wǎng)行業(yè)新知糕档,終身學(xué)習(xí)踐行者莉恼。關(guān)注后回復(fù)「Java」、「Python」速那、「C++」俐银、「大數(shù)據(jù)」、「機(jī)器學(xué)習(xí)」端仰、「算法」捶惜、「AI」、「Android」荔烧、「前端」吱七、「iOS」、「考研」鹤竭、「BAT」踊餐、「校招」、「筆試」臀稚、「面試」吝岭、「面經(jīng)」、「計(jì)算機(jī)基礎(chǔ)」烁涌、「LeetCode」 等關(guān)鍵字可以獲取對(duì)應(yīng)的免費(fèi)學(xué)習(xí)資料苍碟。

                 ![](https://upload-images.jianshu.io/upload_images/5447660-18582b4397932e01.jpeg)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點(diǎn)擊并拖拽以移動(dòng)")

Innodb中的事務(wù)隔離級(jí)別和鎖的關(guān)系

前言:

我們都知道事務(wù)的幾種性質(zhì),數(shù)據(jù)庫(kù)為了維護(hù)這些性質(zhì)撮执,尤其是一致性和隔離性微峰,一般使用加鎖這種方式。同時(shí)數(shù)據(jù)庫(kù)又是個(gè)高并發(fā)的應(yīng)用抒钱,同一時(shí)間會(huì)有大量的并發(fā)訪問蜓肆,如果加鎖過度颜凯,會(huì)極大的降低并發(fā)處理能力。所以對(duì)于加鎖的處理仗扬,可以說就是數(shù)據(jù)庫(kù)對(duì)于事務(wù)處理的精髓所在症概。這里通過分析MySQL中InnoDB引擎的加鎖機(jī)制,來拋磚引玉早芭,讓讀者更好的理解彼城,在事務(wù)處理中數(shù)據(jù)庫(kù)到底做了什么。

一次封鎖or兩段鎖退个?

因?yàn)橛写罅康牟l(fā)訪問募壕,為了預(yù)防死鎖,一般應(yīng)用中推薦使用一次封鎖法语盈,就是在方法的開始階段舱馅,已經(jīng)預(yù)先知道會(huì)用到哪些數(shù)據(jù),然后全部鎖住刀荒,在方法運(yùn)行之后代嗤,再全部解鎖。這種方式可以有效的避免循環(huán)死鎖缠借,但在數(shù)據(jù)庫(kù)中卻不適用干毅,因?yàn)樵谑聞?wù)開始階段,數(shù)據(jù)庫(kù)并不知道會(huì)用到哪些數(shù)據(jù)泼返。
數(shù)據(jù)庫(kù)遵循的是兩段鎖協(xié)議溶锭,將事務(wù)分成兩個(gè)階段,加鎖階段和解鎖階段(所以叫兩段鎖)

  • 加鎖階段:在該階段可以進(jìn)行加鎖操作符隙。在對(duì)任何數(shù)據(jù)進(jìn)行讀操作之前要申請(qǐng)并獲得S鎖(共享鎖趴捅,其它事務(wù)可以繼續(xù)加共享鎖,但不能加排它鎖)霹疫,在進(jìn)行寫操作之前要申請(qǐng)并獲得X鎖(排它鎖拱绑,其它事務(wù)不能再獲得任何鎖)。加鎖不成功丽蝎,則事務(wù)進(jìn)入等待狀態(tài)猎拨,直到加鎖成功才繼續(xù)執(zhí)行。
  • 解鎖階段:當(dāng)事務(wù)釋放了一個(gè)封鎖以后屠阻,事務(wù)進(jìn)入解鎖階段红省,在該階段只能進(jìn)行解鎖操作不能再進(jìn)行加鎖操作。
事務(wù) 加鎖/解鎖處理
begin国觉;
insert into test ..... 加insert對(duì)應(yīng)的鎖
update test set... 加update對(duì)應(yīng)的鎖
delete from test .... 加delete對(duì)應(yīng)的鎖
commit; 事務(wù)提交時(shí)吧恃,同時(shí)釋放insert、update麻诀、delete對(duì)應(yīng)的鎖

這種方式雖然無法避免死鎖痕寓,但是兩段鎖協(xié)議可以保證事務(wù)的并發(fā)調(diào)度是串行化(串行化很重要傲醉,尤其是在數(shù)據(jù)恢復(fù)和備份的時(shí)候)的。

事務(wù)中的加鎖方式

事務(wù)的四種隔離級(jí)別

在數(shù)據(jù)庫(kù)操作中呻率,為了有效保證并發(fā)讀取數(shù)據(jù)的正確性硬毕,提出的事務(wù)隔離級(jí)別。我們的數(shù)據(jù)庫(kù)鎖礼仗,也是為了構(gòu)建這些隔離級(jí)別存在的吐咳。

隔離級(jí)別 臟讀(Dirty Read) 不可重復(fù)讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重復(fù)讀(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會(huì)話中未提交事務(wù)修改的數(shù)據(jù)
  • 提交讀(Read Committed):只能讀取到已經(jīng)提交的數(shù)據(jù)元践。Oracle等多數(shù)數(shù)據(jù)庫(kù)默認(rèn)都是該級(jí)別 (不重復(fù)讀)
  • 可重復(fù)讀(Repeated Read):可重復(fù)讀挪丢。在同一個(gè)事務(wù)內(nèi)的查詢都是事務(wù)開始時(shí)刻一致的,InnoDB默認(rèn)級(jí)別卢厂。在SQL標(biāo)準(zhǔn)中,該隔離級(jí)別消除了不可重復(fù)讀惠啄,但是還存在幻象讀
  • 串行讀(Serializable):完全串行化的讀慎恒,每次讀都需要獲得表級(jí)共享鎖,讀寫相互都會(huì)阻塞

Read Uncommitted這種級(jí)別撵渡,數(shù)據(jù)庫(kù)一般都不會(huì)用融柬,而且任何操作都不會(huì)加鎖趋距,這里就不討論了粒氧。

MySQL中鎖的種類

MySQL中鎖的種類很多,有常見的表鎖和行鎖节腐,也有新加入的Metadata Lock等等,表鎖是對(duì)一整張表加鎖外盯,雖然可分為讀鎖和寫鎖,但畢竟是鎖住整張表翼雀,會(huì)導(dǎo)致并發(fā)能力下降饱苟,一般是做ddl處理時(shí)使用。

行鎖則是鎖住數(shù)據(jù)行狼渊,這種加鎖方法比較復(fù)雜箱熬,但是由于只鎖住有限的數(shù)據(jù),對(duì)于其它數(shù)據(jù)不加限制狈邑,所以并發(fā)能力強(qiáng)城须,MySQL一般都是用行鎖來處理并發(fā)事務(wù)。這里主要討論的也就是行鎖米苹。

Read Committed(讀取提交內(nèi)容)

在RC級(jí)別中糕伐,數(shù)據(jù)的讀取都是不加鎖的,但是數(shù)據(jù)的寫入蘸嘶、修改和刪除是需要加鎖的赤炒。效果如下

<pre>MySQL> show create table class_teacher \G
Table: class_teacher
Create Table: CREATE TABLE class_teacher (
id int(11) NOT NULL AUTO_INCREMENT,
class_name varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
teacher_id int(11) NOT NULL,
PRIMARY KEY (id),
KEY idx_teacher_id (teacher_id)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)
MySQL> select * from class_teacher;
+----+--------------+------------+
| id | class_name | teacher_id |
+----+--------------+------------+
| 1 | 初三一班 | 1 |
| 3 | 初二一班 | 2 |
| 4 | 初二二班 | 2 |
+----+--------------+------------+
</pre>

由于MySQL的InnoDB默認(rèn)是使用的RR級(jí)別氯析,所以我們先要將該session開啟成RC級(jí)別,并且設(shè)置binlog的模式

<pre>SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';(或者是MIXED)
</pre>

事務(wù)A 事務(wù)B
begin; begin;
update class_teacher set class_name='初三二班' where teacher_id=1; update class_teacher set class_name='初三三班' where teacher_id=1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
commit;

為了防止并發(fā)過程中的修改沖突莺褒,事務(wù)A中MySQL給teacher_id=1的數(shù)據(jù)行加鎖掩缓,并一直不commit(釋放鎖),那么事務(wù)B也就一直拿不到該行鎖遵岩,wait直到超時(shí)你辣。

這時(shí)我們要注意到,teacher_id是有索引的尘执,如果是沒有索引的class_name呢舍哄?update class_teacher set teacher_id=3 where class_name = '初三一班';
那么MySQL會(huì)給整張表的所有數(shù)據(jù)行的加行鎖。這里聽起來有點(diǎn)不可思議誊锭,但是當(dāng)sql運(yùn)行的過程中表悬,MySQL并不知道哪些數(shù)據(jù)行是 class_name = '初三一班'的(沒有索引嘛),如果一個(gè)條件無法通過索引快速過濾丧靡,存儲(chǔ)引擎層面就會(huì)將所有記錄加鎖后返回蟆沫,再由MySQL Server層進(jìn)行過濾。

但在實(shí)際使用過程當(dāng)中温治,MySQL做了一些改進(jìn)饭庞,在MySQL Server過濾條件,發(fā)現(xiàn)不滿足后熬荆,會(huì)調(diào)用unlock_row方法舟山,把不滿足條件的記錄釋放鎖 (違背了二段鎖協(xié)議的約束)。這樣做卤恳,保證了最后只會(huì)持有滿足條件記錄上的鎖累盗,但是每條記錄的加鎖操作還是不能省略的⊥涣眨可見即使是MySQL幅骄,為了效率也是會(huì)違反規(guī)范的。(參見《高性能MySQL》中文第三版p181)

這種情況同樣適用于MySQL的默認(rèn)隔離級(jí)別RR本今。所以對(duì)一個(gè)數(shù)據(jù)量很大的表做批量修改的時(shí)候拆座,如果無法使用相應(yīng)的索引,MySQL Server過濾數(shù)據(jù)的的時(shí)候特別慢冠息,就會(huì)出現(xiàn)雖然沒有修改某些行的數(shù)據(jù)挪凑,但是它們還是被鎖住了的現(xiàn)象。

Repeatable Read(可重讀)

這是MySQL中InnoDB默認(rèn)的隔離級(jí)別逛艰。我們姑且分“讀”和“寫”兩個(gè)模塊來講解躏碳。

讀就是可重讀,可重讀這個(gè)概念是一事務(wù)的多個(gè)實(shí)例在并發(fā)讀取數(shù)據(jù)時(shí)散怖,會(huì)看到同樣的數(shù)據(jù)行菇绵,有點(diǎn)抽象肄渗,我們來看一下效果。

RC(不可重讀)模式下的展現(xiàn)

事務(wù)A 事務(wù)B
begin; begin;
select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id --- --- --- 1 初三二班 1 2 初三一班 1
update class_teacher set class_name='初三三班' where id=1;
commit;
select id,class_name,teacher_id from class_teacher where teacher_id=1; id class_name teacher_id --- --- --- 1 初三三班 1 2 初三一班 1 讀到了事務(wù)B修改的數(shù)據(jù)咬最,和第一次查詢的結(jié)果不一樣翎嫡,是不可重讀的。
commit;

事務(wù)B修改id=1的數(shù)據(jù)提交之后永乌,事務(wù)A同樣的查詢惑申,后一次和前一次的結(jié)果不一樣,這就是不可重讀(重新讀取產(chǎn)生的結(jié)果不一樣)翅雏。這就很可能帶來一些問題圈驼,那么我們來看看在RR級(jí)別中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; id class_name teacher_id --- --- --- 1 初三二班 1 2 初三一班 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; id class_name teacher_id --- --- --- 1 初三二班 1 2 初三一班 1 沒有讀到事務(wù)B修改的數(shù)據(jù),和第一次sql讀取的一樣望几,是可重復(fù)讀的绩脆。沒有讀到事務(wù)C新添加的數(shù)據(jù)。
commit;

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

不可重復(fù)讀和幻讀的區(qū)別####

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

如果使用鎖機(jī)制來實(shí)現(xiàn)這兩種隔離級(jí)別捎谨,在可重復(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ù)提交蹭沛,這時(shí)事務(wù)A就會(huì)發(fā)現(xiàn)莫名其妙多了一條之前沒有的數(shù)據(jù)臂寝,這就是幻讀章鲤,不能通過行鎖來避免。需要Serializable隔離級(jí)別 咆贬,讀用讀鎖败徊,寫用寫鎖,讀鎖和寫鎖互斥素征,這么做可以有效的避免幻讀集嵌、不可重復(fù)讀、臟讀等問題御毅,但會(huì)極大的降低數(shù)據(jù)庫(kù)的并發(fā)能力根欧。

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

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

悲觀鎖和樂觀鎖####

  • 悲觀鎖

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

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

  • 樂觀鎖

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

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

要說明的是锯梁,MVCC的實(shí)現(xiàn)沒有固定的規(guī)范即碗,每個(gè)數(shù)據(jù)庫(kù)都會(huì)有不同的實(shí)現(xiàn)方式,這里討論的是InnoDB的MVCC涝桅。

MVCC在MySQL的InnoDB中的實(shí)現(xiàn)

在InnoDB中拜姿,會(huì)在每行數(shù)據(jù)后添加兩個(gè)額外的隱藏的值來實(shí)現(xiàn)MVCC烙样,這兩個(gè)值一個(gè)記錄這行數(shù)據(jù)何時(shí)被創(chuàng)建冯遂,另外一個(gè)記錄這行數(shù)據(jù)何時(shí)過期(或者被刪除)。 在實(shí)際操作中谒获,存儲(chǔ)的并不是時(shí)間蛤肌,而是事務(wù)的版本號(hào),每開啟一個(gè)新事務(wù)批狱,事務(wù)的版本號(hào)就會(huì)遞增裸准。 在可重讀Repeatable reads事務(wù)隔離級(jí)別下:

  • SELECT時(shí),讀取創(chuàng)建版本號(hào)<=當(dāng)前事務(wù)版本號(hào)赔硫,刪除版本號(hào)為空或>當(dāng)前事務(wù)版本號(hào)炒俱。
  • INSERT時(shí),保存當(dāng)前事務(wù)版本號(hào)為行的創(chuàng)建版本號(hào)
  • DELETE時(shí),保存當(dāng)前事務(wù)版本號(hào)為行的刪除版本號(hào)
  • UPDATE時(shí)权悟,插入一條新紀(jì)錄砸王,保存當(dāng)前事務(wù)版本號(hào)為行創(chuàng)建版本號(hào),同時(shí)保存當(dāng)前事務(wù)版本號(hào)到原來刪除的行

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

我們不管從數(shù)據(jù)庫(kù)方面的教課書中學(xué)到茧彤,還是從網(wǎng)絡(luò)上看到骡显,大都是上文中事務(wù)的四種隔離級(jí)別這一模塊列出的意思,RR級(jí)別是可重復(fù)讀的曾掂,但無法解決幻讀惫谤,而只有在Serializable級(jí)別才能解決幻讀。于是我就加了一個(gè)事務(wù)C來展示效果珠洗。在事務(wù)C中添加了一條teacher_id=1的數(shù)據(jù)commit溜歪,RR級(jí)別中應(yīng)該會(huì)有幻讀現(xiàn)象,事務(wù)A在查詢teacher_id=1的數(shù)據(jù)時(shí)會(huì)讀到事務(wù)C新加的數(shù)據(jù)许蓖。但是測(cè)試后發(fā)現(xiàn)蝴猪,在MySQL中是不存在這種情況的,在事務(wù)C提交后膊爪,事務(wù)A還是不會(huì)讀到這條數(shù)據(jù)自阱「土眩可見在MySQL的RR級(jí)別中奏夫,是解決了幻讀的讀問題的髓需。參見下圖

讀問題解決了劲阎,根據(jù)MVCC的定義矫户,并發(fā)提交數(shù)據(jù)時(shí)會(huì)出現(xiàn)沖突通危,那么沖突時(shí)如何解決呢熟尉?我們?cè)賮砜纯碔nnoDB中RR級(jí)別對(duì)于寫數(shù)據(jù)的處理南用。

“讀”與“讀”的區(qū)別

可能有讀者會(huì)疑惑跳芳,事務(wù)的隔離級(jí)別其實(shí)都是對(duì)于讀數(shù)據(jù)的定義芍锦,但到了這里,就被拆成了讀和寫兩個(gè)模塊來講解飞盆。這主要是因?yàn)镸ySQL中的讀娄琉,和事務(wù)隔離級(jí)別中的讀次乓,是不一樣的。

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

對(duì)于這種讀取歷史數(shù)據(jù)的方式簇搅,我們叫它快照讀 (snapshot read)完域,而讀取數(shù)據(jù)庫(kù)當(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ù)的隔離級(jí)別實(shí)際上都是定義了當(dāng)前讀的級(jí)別异旧,MySQL為了減少鎖處理(包括等待其它鎖)的時(shí)間,提升并發(fā)能力提佣,引入了快照讀的概念吮蛹,使得select不用加鎖。而update拌屏、insert這些“當(dāng)前讀”潮针,就需要另外的模塊來解決了。

寫("當(dāng)前讀")

事務(wù)的隔離級(jí)別中雖然只定義了讀數(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í)造成數(shù)據(jù)沖突的情況。但如何避免別的事務(wù)插入數(shù)據(jù)就成了問題。我們可以看看RR級(jí)別和RC級(jí)別的對(duì)比

RC級(jí)別:

事務(wù)A 事務(wù)B
begin; begin;
select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id --- --- --- 2 初三二班 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; id class_name teacher_id --- --- --- 2 初三四班 30 10 初三二班 30

RR級(jí)別:

事務(wù)A 事務(wù)B
begin; begin;
select id,class_name,teacher_id from class_teacher where teacher_id=30; id class_name teacher_id --- --- --- 2 初三二班 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; id class_name teacher_id --- --- --- 2 初三四班 30
commit; 事務(wù)Acommit后啊楚,事務(wù)B的insert執(zhí)行吠冤。

通過對(duì)比我們可以發(fā)現(xiàn),在RC級(jí)別中恭理,事務(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語(yǔ)句所修改涯保,這就是“當(dāng)前讀”的幻讀。

RR級(jí)別中周伦,事務(wù)A在update后加鎖夕春,事務(wù)B無法插入新數(shù)據(jù),這樣事務(wù)A在update前后讀的數(shù)據(jù)保持一致专挪,避免了幻讀及志。這個(gè)鎖,就是Gap鎖寨腔。

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

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

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

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

  • (negative infinity, 5],
  • (5,30],
  • (30,positive infinity);

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

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

事務(wù)A 事務(wù)B 事務(wù)C
begin; begin; begin;
select id,class_name,teacher_id from class_teacher; id class_name teacher_id --- --- --- 1 初三一班 5 2 初三二班 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之后金刁,這條語(yǔ)句才插入成功 commit;
commit;

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

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

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

Serializable

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

這里要吐槽一句,不要看到select就說不會(huì)加鎖了谨湘,在Serializable這個(gè)級(jí)別绽快,還是會(huì)加鎖的!

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末紧阔,一起剝皮案震驚了整個(gè)濱河市坊罢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌擅耽,老刑警劉巖活孩,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秫筏,居然都是意外死亡诱鞠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門这敬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來航夺,“玉大人,你說我怎么就攤上這事崔涂⊙羝” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵冷蚂,是天一觀的道長(zhǎng)缭保。 經(jīng)常有香客問我,道長(zhǎng)蝙茶,這世上最難降的妖魔是什么艺骂? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮隆夯,結(jié)果婚禮上钳恕,老公的妹妹穿的比我還像新娘。我一直安慰自己蹄衷,他們只是感情好忧额,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著愧口,像睡著了一般睦番。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耍属,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天托嚣,我揣著相機(jī)與錄音,去河邊找鬼厚骗。 笑死注益,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溯捆。 我是一名探鬼主播丑搔,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼提揍!你這毒婦竟也來了啤月?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤劳跃,失蹤者是張志新(化名)和其女友劉穎谎仲,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刨仑,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡郑诺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年夹姥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辙诞。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辙售,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出飞涂,到底是詐尸還是另有隱情旦部,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布较店,位于F島的核電站士八,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏梁呈。R本人自食惡果不足惜婚度,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望官卡。 院中可真熱鬧陕见,春花似錦、人聲如沸味抖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)仔涩。三九已至忍坷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間熔脂,已是汗流浹背佩研。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留霞揉,地道東北人旬薯。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像适秩,于是被迫代替她去往敵國(guó)和親绊序。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容