| 對比項 | InnoDB | MyIsAM |
|---|---|---|
| 事務(wù) | 支持 | 不支持 |
| 鎖 | 支持MVCC行鎖 | 表鎖 |
| 外鍵 | 支持 | 不支持 |
| 存儲空間 | 存儲空間由于需要高速緩存,較大 | 可壓縮 |
| 適用場景 | 有一定量的update和Insert | 大量的select |
小明大概了解了一下InnoDB和MyIsAM的區(qū)別,由于使用的是InnoDB,小明就沒有過多的糾結(jié)這一塊。
2.2事務(wù)的隔離性
小明在研究鎖之前,又回想到之前上學(xué)的時候教過的數(shù)據(jù)庫事務(wù)隔離性,其實(shí)鎖在數(shù)據(jù)庫中其功能之一也是用來實(shí)現(xiàn)事務(wù)隔離性。而事務(wù)的隔離性其實(shí)是用來解決,臟讀,不可重復(fù)讀,幻讀幾類問題。
2.2.1 臟讀
一個事務(wù)讀取到另一個事務(wù)未提交的更新數(shù)據(jù)。 什么意思呢?
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from user where id = 1; | begin; |
| 3 | update user set namm = 'test' where id = 1; | |
| 4 | select * from user where id = 1; | |
| 5 | commit; | commit; |
在事務(wù)A,B中,事務(wù)A在時間點(diǎn)2,4分別對user表中id=1的數(shù)據(jù)進(jìn)行了查詢了,但是事務(wù)B在時間點(diǎn)3進(jìn)行了修改,導(dǎo)致了事務(wù)A在4中的查詢出的結(jié)果其實(shí)是事務(wù)B修改后的。破壞了數(shù)據(jù)庫中的隔離性。
2.2.2 不可重復(fù)讀
在同一個事務(wù)中,多次讀取同一數(shù)據(jù)返回的結(jié)果不同,和臟讀不同的是這里讀取的是已經(jīng)提交過后的。
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from user where id = 1; | begin; |
| 3 | update user set namm = 'test' where id = 1; | |
| 4 | commit; | |
| 5 | select * from user where id = 1; | |
| 6 | commit; | |
| 在事務(wù)B中提交的操作在事務(wù)A第二次查詢之前,但是依然讀到了事務(wù)B的更新結(jié)果,也破壞了事務(wù)的隔離性。 |
在事務(wù)B中提交的操作在事務(wù)A第二次查詢之前,但是依然讀到了事務(wù)B的更新結(jié)果,也破壞了事務(wù)的隔離性。
2.2.3 幻讀
一個事務(wù)讀到另一個事務(wù)已提交的insert數(shù)據(jù)。
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from user where id > 1; | begin; |
| 3 | insert user select 2; | |
| 4 | commit; | |
| 5 | select * from user where id > 1; | |
| 6 | commit; |
在事務(wù)A中查詢了兩次id大于1的,在第一次id大于1查詢結(jié)果中沒有數(shù)據(jù),但是由于事務(wù)B插入了一條Id=2的數(shù)據(jù),導(dǎo)致事務(wù)A第二次查詢時能查到事務(wù)B中插入的數(shù)據(jù)。
事務(wù)中的隔離性:
| 隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|---|
| 未提交讀(RUC) | NO | NO | NO |
| 已提交讀(RC) | YES | NO | NO |
| 可重復(fù)讀(RR) | YES | YES | NO |
| 可串行化 | YES | YES | YES |
小明注意到在收集資料的過程中,有資料寫到InnoDB和其他數(shù)據(jù)庫有點(diǎn)不同,InnoDB的可重復(fù)讀其實(shí)就能解決幻讀了,小明心想:這InnoDB還挺牛逼的,我得好好看看到底是怎么個原理。
2.3 InnoDB鎖類型
小明首先了解一下Mysql中常見的鎖類型有哪些:
2.3.1 S or X
在InnoDb中實(shí)現(xiàn)了兩個標(biāo)準(zhǔn)的行級鎖,可以簡單的看為兩個讀寫鎖:
兼容性:是指事務(wù)A獲得一個某行某種鎖之后,事務(wù)B同樣的在這個行上嘗試獲取某種鎖,如果能立即獲取,則稱鎖兼容,反之叫沖突。
縱軸是代表已有的鎖,橫軸是代表嘗試獲取的鎖。
| . | X | S |
|---|---|---|
| X | 沖突 | 沖突 |
| S | 沖突 | 兼容 |
2.3.2 意向鎖
意向鎖在InnoDB中是表級鎖,和他的名字一樣他是用來表達(dá)一個事務(wù)想要獲取什么。意向鎖分為:
這個鎖有什么用呢?為什么需要這個鎖呢? 首先說一下如果沒有這個鎖,如果要給這個表加上表鎖,一般的做法是去遍歷每一行看看他是否有行鎖,這樣的話效率太低,而我們有意向鎖,只需要判斷是否有意向鎖即可,不需要再去一行行的去掃描。
在InnoDB中由于支持的是行級的鎖,因此InnboDB鎖的兼容性可以擴(kuò)展如下:
| . | IX | IS | X | S |
|---|---|---|---|---|
| IX | 兼容 | 兼容 | 沖突 | 沖突 |
| IS | 兼容 | 兼容 | 沖突 | 兼容 |
| X | 沖突 | 沖突 | 沖突 | 沖突 |
| S | 沖突 | 兼容 | 沖突 | 兼容 |
2.3.3 自增長鎖
自增長鎖是一種特殊的表鎖機(jī)制,提升并發(fā)插入性能。對于這個鎖有幾個特點(diǎn):
在MySQL5.1.2版本之后,有了很多優(yōu)化,可以根據(jù)不同的模式來進(jìn)行調(diào)整自增加鎖的方式。小明看到了這里打開了自己的MySQL發(fā)現(xiàn)是5.7之后,于是便輸入了下面的語句,獲取到當(dāng)前鎖的模式:
mysql> show variables like 'innodb_autoinc_lock_mode'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_autoinc_lock_mode | 2 | +--------------------------+-------+ 1 row in set (0.01 sec)
在MySQL中innodbautoinclock_mode有3種配置模式:0、1、2,分別對應(yīng)”傳統(tǒng)模式”, “連續(xù)模式”, “交錯模式”。
2.4InnoDB鎖算法
小明已經(jīng)了解到了在InnoDB中有哪些鎖類型,但是如何去使用這些鎖,還是得靠鎖算法。
2.4.1 記錄鎖(Record-Lock)
記錄鎖是鎖住記錄的,這里要說明的是這里鎖住的是索引記錄,而不是我們真正的數(shù)據(jù)記錄。
2.4.2 間隙鎖
間隙鎖顧名思義鎖間隙,不鎖記錄。鎖間隙的意思就是鎖定某一個范圍,間隙鎖又叫g(shù)ap鎖,其不會阻塞其他的gap鎖,但是會阻塞插入間隙鎖,這也是用來防止幻讀的關(guān)鍵。

2.4.3 next-key鎖
這個鎖本質(zhì)是記錄鎖加上gap鎖。在RR隔離級別下(InnoDB默認(rèn)),Innodb對于行的掃描鎖定都是使用此算法,但是如果查詢掃描中有唯一索引會退化成只使用記錄鎖。為什么呢? 因?yàn)槲ㄒ凰饕艽_定行數(shù),而其他索引不能確定行數(shù),有可能在其他事務(wù)中會再次添加這個索引的數(shù)據(jù)會造成幻讀。
這里也說明了為什么Mysql可以在RR級別下解決幻讀。
2.4.4 插入意向鎖
插入意向鎖Mysql官方對其的解釋:
An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.
可以看出插入意向鎖是在插入的時候產(chǎn)生的,在多個事務(wù)同時寫入不同數(shù)據(jù)至同一索引間隙的時候,并不需要等待其他事務(wù)完成,不會發(fā)生鎖等待。假設(shè)有一個記錄索引包含鍵值4和7,不同的事務(wù)分別插入5和6,每個事務(wù)都會產(chǎn)生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因?yàn)閿?shù)據(jù)行并不沖突。
這里要說明的是如果有間隙鎖了,插入意向鎖會被阻塞。
2.5 MVCC
MVCC,多版本并發(fā)控制技術(shù)。在InnoDB中,在每一行記錄的后面增加兩個隱藏列,記錄創(chuàng)建版本號和刪除版本號。通過版本號和行鎖,從而提高數(shù)據(jù)庫系統(tǒng)并發(fā)性能。
在MVCC中,對于讀操作可以分為兩種讀:
在RR隔離級別下的快照讀,不是以begin事務(wù)開始的時間點(diǎn)作為snapshot建立時間點(diǎn),而是以第一條select語句的時間點(diǎn)作為snapshot建立的時間點(diǎn)。以后的select都會讀取當(dāng)前時間點(diǎn)的快照值。
在RC隔離級別下每次快照讀均會創(chuàng)建新的快照。
具體的原理是通過每行會有兩個隱藏的字段一個是用來記錄當(dāng)前事務(wù),一個是用來記錄回滾的指向Undolog。利用undolog就可以讀取到之前的快照,不需要單獨(dú)開辟空間記錄。
3.加鎖分析
小明到這里,已經(jīng)學(xué)習(xí)很多mysql鎖有關(guān)的基礎(chǔ)知識,所以決定自己創(chuàng)建一個表搞下實(shí)驗(yàn)。首先創(chuàng)建了一個簡單的用戶表:
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL, `comment` varchar(11) CHARACTER SET utf8 DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
然后插入了幾條實(shí)驗(yàn)數(shù)據(jù):
insert user select 20,333,333; insert user select 25,555,555; insert user select 20,999,999;
數(shù)據(jù)庫事務(wù)隔離選擇了RR
3.1 實(shí)驗(yàn)1
小明開啟了兩個事務(wù),進(jìn)行實(shí)驗(yàn)1.
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from user where name = '555' for update; | begin; |
| 3 | insert user select 31,'556','556'; | |
| 4 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
小明開啟了兩個事務(wù)并輸入了上面的語句,發(fā)現(xiàn)事務(wù)B居然出現(xiàn)了超時,小明看了一下自己明明是對name = 555這一行進(jìn)行的加鎖,為什么我想插入name=556給我阻塞了。于是小明打開命令行輸入:
select * from information_schema.INNODB_LOCKS
發(fā)現(xiàn)在事務(wù)A中給555加了Next-key鎖,事務(wù)B插入的時候會首先進(jìn)行插入意向鎖的插入,于是得出下面結(jié)論:

可以看見事務(wù)B由于間隙鎖和插入意向鎖的沖突,導(dǎo)致了阻塞。
3.2 實(shí)驗(yàn)2
小明發(fā)現(xiàn)上面查詢條件用的是普通的非唯一索引,于是小明就試了一下主鍵索引:
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from user where id = 25 for update; | begin; |
| 3 | insert user select 26,'666','666'; | |
| 4 | Query OK, 1 row affected (0.00 sec) | |
| Records: 1 Duplicates: 0 Warnings: 0 |
居然發(fā)現(xiàn)事務(wù)B并沒有發(fā)生阻塞,哎這個是咋回事呢,小明有點(diǎn)疑惑,按照實(shí)驗(yàn)1的套路應(yīng)該會被阻塞啊,因?yàn)?5-30之間會有間隙鎖。于是小明又祭出了命令行,發(fā)現(xiàn)只加了X記錄鎖。原來是因?yàn)槲ㄒ凰饕龝导売涗涙i,這么做的理由是:非唯一索引加next-key鎖由于不能確定明確的行數(shù)有可能其他事務(wù)在你查詢的過程中,再次添加這個索引的數(shù)據(jù),導(dǎo)致隔離性遭到破壞,也就是幻讀。唯一索引由于明確了唯一的數(shù)據(jù)行,所以不需要添加間隙鎖解決幻讀。

3.3 實(shí)驗(yàn)3
上面測試了主鍵索引,非唯一索引,這里還有個字段是沒有索引,如果對其加鎖會出現(xiàn)什么呢?
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from user where comment = '555' for update; | begin; |
| 3 | insert user select 26,'666','666'; | |
| 4 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
| 5 | insert user select 31,'3131','3131'; | |
| 6 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
| 7 | insert user select 10,'100','100'; | |
| 8 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
| 小明一看哎喲我去,這個咋回事呢,咋不管是用實(shí)驗(yàn)1非間隙鎖范圍的數(shù)據(jù),還是用間隙鎖里面的數(shù)據(jù)都不行,難道是加了表鎖嗎? |
的確,如果用沒有索引的數(shù)據(jù),其會對所有聚簇索引上都加上next-key鎖。

所以大家平常開發(fā)的時候如果對查詢條件沒有索引的,一定進(jìn)行一致性讀,也就是加鎖讀,會導(dǎo)致全表加上索引,會導(dǎo)致其他事務(wù)全部阻塞,數(shù)據(jù)庫基本會處于不可用狀態(tài)。
4.回到事故
4.1 死鎖
小明做完實(shí)驗(yàn)之后總算是了解清楚了加鎖的一些基本套路,但是之前線上出現(xiàn)的死鎖又是什么東西呢?
死鎖:是指兩個或兩個以上的事務(wù)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象。說明有等待才會有死鎖,解決死鎖可以通過去掉等待,比如回滾事務(wù)。
解決死鎖的兩個辦法:

就出現(xiàn)回滾,通常來說InnoDB會選擇回滾權(quán)重較小的事務(wù),也就是undo較小的事務(wù)。
4.2 線上問題
小明到這里,基本需要的基本功都有了,于是在自己的本地表中開始復(fù)現(xiàn)這個問題:
| 時間點(diǎn) | 事務(wù)A | 事務(wù)B |
|---|---|---|
| 1 | begin; | begin; |
| 2 | delete from user where name = '777'; | delete from user where name = '666'; |
| 3 | insert user select 27,'777','777'; | insert user select 26,'666','666'; |
| 4 | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | Query OK, 1 row affected (14.32 sec) Records: 1 Duplicates: 0 Warnings: 0 |
可以看見事務(wù)A出現(xiàn)被回滾了,而事務(wù)B成功執(zhí)行。 具體每個時間點(diǎn)發(fā)生了什么呢?
時間點(diǎn)2:事務(wù)A刪除name = '777'的數(shù)據(jù),需要對777這個索引加上next-Key鎖,但是其不存在,所以只對555-999之間加間隙鎖,同理事務(wù)B也對555-999之間加間隙鎖。間隙鎖之間是兼容的。
時間點(diǎn)3:事務(wù)A,執(zhí)行Insert操作,首先插入意向鎖,但是555-999之間有間隙鎖,由于插入意向鎖和間隙鎖沖突,事務(wù)A阻塞,等待事務(wù)B釋放間隙鎖。事務(wù)B同理,等待事務(wù)A釋放間隙鎖。于是出現(xiàn)了A->B,B->A回路等待。
時間點(diǎn)4:事務(wù)管理器選擇回滾事務(wù)A,事務(wù)B插入操作執(zhí)行成功。

4.3 修復(fù)BUG
這個問題總算是被小明找到了,就是因?yàn)殚g隙鎖,現(xiàn)在需要解決這個問題,這個問題的原因是出現(xiàn)了間隙鎖,那就來去掉他吧:
經(jīng)過考慮小明選擇了第四種,馬上進(jìn)行了修復(fù),然后上線觀察驗(yàn)證,發(fā)現(xiàn)現(xiàn)在已經(jīng)不會出現(xiàn)這個Bug了,這下小明總算能睡個安穩(wěn)覺了。
4.4 如何防止死鎖
小明通過基礎(chǔ)的學(xué)習(xí)和平常的經(jīng)驗(yàn)總結(jié)了如下幾點(diǎn):
最后
由于篇幅有限很多東西并不能介紹全如果感興趣的同學(xué)可以閱讀《Mysql技術(shù)內(nèi)幕-InnoDB引擎》第6章 以及 何大師的MySQL 加鎖處理分析。作者本人水平有限,如果有什么錯誤,還請指正。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。
標(biāo)簽:甘南 拉薩 安徽 葫蘆島 嘉峪關(guān) 吐魯番 洛陽
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《開發(fā)人員為什么必須要了解數(shù)據(jù)庫鎖詳解》,本文關(guān)鍵詞 開發(fā),人員,為什么,必,須要,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。