鎖在現實中的意義爲:封閉的器物,以鑰匙或暗碼開啓。在計算機中的鎖通常用來管理對共享資源的併發訪問,好比咱們java同窗熟悉的Lock,synchronized等都是咱們常見的鎖。固然在咱們的數據庫中也有鎖用來控制資源的併發訪問,這也是數據庫和文件系統的區別之一。java
一般來講對於通常的開發人員,在使用數據庫的時候通常懂點DQL(select),DML(insert,update,delete)就夠了。mysql
小明是一個剛剛畢業在互聯網公司工做的Java開發工程師,日常的工做就是完成PM的需求,固然在完成需求的同時確定逃脫不了spring,springmvc,mybatis的那一套框架,因此通常來講sql仍是本身手寫,遇到比較複雜的sql會從網上去百度一下。對於一些比較重要操做,好比交易啊這些,小明會用spring的事務來對數據庫的事務進行管理,因爲數據量比較小目前還涉及不了分佈式事務。git
前幾個月小明過得都還風調雨順,知道有一天,小明接了一個需求,商家有個配置項,叫優惠配置項,能夠配置買一送一,買一送二等等規則,固然這些配置是批量傳輸給後端的,這樣就有個問題每一個規則都得去匹配他究竟是刪除仍是添加仍是修改,這樣後端邏輯就比較麻煩,聰明的小明想到了一個辦法,直接刪除這個商家的配置,而後所有添加進去。小明立刻開發完畢,成功上線。github
開始上線沒什麼毛病,可是日誌常常會出現一些mysql-insert-deadlock異常。因爲小明經驗比較淺,對於這類型的問題第一次碰見,因而去問了他們組的老司機-大紅,大紅一看見這個問題,而後看了他的代碼以後,輸出了幾個命令看了幾個日誌,立刻定位了問題,告訴了小明:這是由於delete的時候會加間隙鎖,可是間隙鎖之間卻能夠兼容,可是插入新的數據的時候就會由於插入意向鎖會被間隙鎖阻塞,致使雙方被資源被互佔,致使死鎖。小明聽了以後似懂非懂,因爲大紅的事情比較多,不方便一直麻煩大紅,因此決定本身下來本身想。下班事後,小明回想大紅說的話,什麼是間隙鎖,什麼是插入意向鎖,看來做爲開發者對數據庫不該該只會寫SQL啊,否則遇到一些疑難雜症徹底無法解決啊。想完,因而小明就踏上了學習Mysql鎖這條不歸之路。算法
小明沒有着急去了解鎖這方面的知識,他首先先了解了下Mysql體系架構: spring
能夠發現Mysql由鏈接池組件、管理服務和工具組件、sql接口組件、查詢分析器組件、優化器組件、 緩衝組件、插件式存儲引擎、物理文件組成。小明發如今mysql中存儲引擎是以插件的方式提供的,在Mysql中有多種存儲引擎,每一個存儲引擎都有本身的特色。隨後小明在命令行中打出了:sql
show engines \G;
複製代碼
一看原來有這麼多種引擎。數據庫
又打出了下面的命令,查看當前數據庫默認的引擎:後端
show variables like '%storage_engine%';
複製代碼
小明恍然大悟:原來本身的數據庫是使用的InnoDB,依稀記得本身在上學的時候好像據說過有個引擎叫MyIsAM,小明想這兩個有啥不一樣呢?立刻查找了一下資料:
對比項 | InnoDB | MyIsAM |
---|---|---|
事務 | 支持 | 不支持 |
鎖 | 支持MVCC行鎖 | 表鎖 |
外鍵 | 支持 | 不支持 |
存儲空間 | 存儲空間因爲須要高速緩存,較大 | 可壓縮 |
適用場景 | 有必定量的update和Insert | 大量的select |
小明大概瞭解了一下InnoDB和MyIsAM的區別,因爲使用的是InnoDB,小明就沒有過多的糾結這一塊。緩存
小明在研究鎖以前,又回想到以前上學的時候教過的數據庫事務隔離性,其實鎖在數據庫中其功能之一也是用來實現事務隔離性。而事務的隔離性實際上是用來解決,髒讀,不可重複讀,幻讀幾類問題。
一個事務讀取到另外一個事務未提交的更新數據。 什麼意思呢?
時間點 | 事務A | 事務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; |
在事務A,B中,事務A在時間點2,4分別對user表中id=1的數據進行了查詢了,可是事務B在時間點3進行了修改,致使了事務A在4中的查詢出的結果實際上是事務B修改後的。破壞了數據庫中的隔離性。
在同一個事務中,屢次讀取同一數據返回的結果不一樣,和髒讀不一樣的是這裏讀取的是已經提交事後的。
時間點 | 事務A | 事務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; |
在事務B中提交的操做在事務A第二次查詢以前,可是依然讀到了事務B的更新結果,也破壞了事務的隔離性。
一個事務讀到另外一個事務已提交的insert數據。
時間點 | 事務A | 事務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; |
在事務A中查詢了兩次id大於1的,在第一次id大於1查詢結果中沒有數據,可是因爲事務B插入了一條Id=2的數據,致使事務A第二次查詢時能查到事務B中插入的數據。
事務中的隔離性:
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
未提交讀(RUC) | NO | NO | NO |
已提交讀(RC) | YES | NO | NO |
可重複讀(RR) | YES | YES | NO |
可串行化 | YES | YES | YES |
小明注意到在收集資料的過程當中,有資料寫到InnoDB和其餘數據庫有點不一樣,InnoDB的可重複讀其實就能解決幻讀了,小明心想:這InnoDB還挺牛逼的,我得好好看看究竟是怎麼個原理。
小明首先了解一下Mysql中常見的鎖類型有哪些:
在InnoDb中實現了兩個標準的行級鎖,能夠簡單的看爲兩個讀寫鎖:
兼容性:是指事務A得到一個某行某種鎖以後,事務B一樣的在這個行上嘗試獲取某種鎖,若是能當即獲取,則稱鎖兼容,反之叫衝突。
縱軸是表明已有的鎖,橫軸是表明嘗試獲取的鎖。
. | X | S |
---|---|---|
X | 衝突 | 衝突 |
S | 衝突 | 兼容 |
意向鎖在InnoDB中是表級鎖,和他的名字同樣他是用來表達一個事務想要獲取什麼。意向鎖分爲:
這個鎖有什麼用呢?爲何須要這個鎖呢? 首先說一下若是沒有這個鎖,若是要給這個表加上表鎖,通常的作法是去遍歷每一行看看他是否有行鎖,這樣的話效率過低,而咱們有意向鎖,只須要判斷是否有意向鎖便可,不須要再去一行行的去掃描。
在InnoDB中因爲支持的是行級的鎖,所以InnboDB鎖的兼容性能夠擴展以下:
. | IX | IS | X | S |
---|---|---|---|---|
IX | 兼容 | 兼容 | 衝突 | 衝突 |
IS | 兼容 | 兼容 | 衝突 | 兼容 |
X | 衝突 | 衝突 | 衝突 | 衝突 |
S | 衝突 | 兼容 | 衝突 | 兼容 |
自增加鎖是一種特殊的表鎖機制,提高併發插入性能。對於這個鎖有幾個特色:
在MySQL5.1.2版本以後,有了不少優化,能夠根據不一樣的模式來進行調整自增長鎖的方式。小明看到了這裏打開了本身的MySQL發現是5.7以後,因而便輸入了下面的語句,獲取到當前鎖的模式:
mysql> show variables like 'innodb_autoinc_lock_mode';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 2 |
+--------------------------+-------+
1 row in set (0.01 sec)
複製代碼
在MySQL中innodb_autoinc_lock_mode有3種配置模式:0、一、2,分別對應」傳統模式」, 「連續模式」, 「交錯模式」。
小明已經瞭解到了在InnoDB中有哪些鎖類型,可是如何去使用這些鎖,仍是得靠鎖算法。
記錄鎖是鎖住記錄的,這裏要說明的是這裏鎖住的是索引記錄,而不是咱們真正的數據記錄。
間隙鎖顧名思義鎖間隙,不鎖記錄。鎖間隙的意思就是鎖定某一個範圍,間隙鎖又叫gap鎖,其不會阻塞其餘的gap鎖,可是會阻塞插入間隙鎖,這也是用來防止幻讀的關鍵。
這個鎖本質是記錄鎖加上gap鎖。在RR隔離級別下(InnoDB默認),Innodb對於行的掃描鎖定都是使用此算法,可是若是查詢掃描中有惟一索引會退化成只使用記錄鎖。爲何呢? 由於惟一索引能肯定行數,而其餘索引不能肯定行數,有可能在其餘事務中會再次添加這個索引的數據會形成幻讀。
這裏也說明了爲何Mysql能夠在RR級別下解決幻讀。
插入意向鎖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.
能夠看出插入意向鎖是在插入的時候產生的,在多個事務同時寫入不一樣數據至同一索引間隙的時候,並不須要等待其餘事務完成,不會發生鎖等待。假設有一個記錄索引包含鍵值4和7,不一樣的事務分別插入5和6,每一個事務都會產生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,可是不會被互相鎖住,由於數據行並不衝突。
這裏要說明的是若是有間隙鎖了,插入意向鎖會被阻塞。
MVCC,多版本併發控制技術。在InnoDB中,在每一行記錄的後面增長兩個隱藏列,記錄建立版本號和刪除版本號。經過版本號和行鎖,從而提升數據庫系統併發性能。
在MVCC中,對於讀操做能夠分爲兩種讀:
在RR隔離級別下的快照讀,不是以begin事務開始的時間點做爲snapshot創建時間點,而是以第一條select語句的時間點做爲snapshot創建的時間點。之後的select都會讀取當前時間點的快照值。
在RC隔離級別下每次快照讀均會建立新的快照。
具體的原理是經過每行會有兩個隱藏的字段一個是用來記錄當前事務,一個是用來記錄回滾的指向Undolog。利用undolog就能夠讀取到以前的快照,不須要單獨開闢空間記錄。
小明到這裏,已經學習不少mysql鎖有關的基礎知識,因此決定本身建立一個表搞下實驗。首先建立了一個簡單的用戶表:
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;
複製代碼
而後插入了幾條實驗數據:
insert user select 20,333,333;
insert user select 25,555,555;
insert user select 20,999,999;
複製代碼
數據庫事務隔離選擇了RR
小明開啓了兩個事務,進行實驗1.
時間點 | 事務A | 事務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 |
小明開啓了兩個事務並輸入了上面的語句,發現事務B竟然出現了超時,小明看了一下本身明明是對name = 555這一行進行的加鎖,爲何我想插入name=556給我阻塞了。因而小明打開命令行輸入:
select * from information_schema.INNODB_LOCKS
複製代碼
發如今事務A中給555加了Next-key鎖,事務B插入的時候會首先進行插入意向鎖的插入,因而得出下面結論:
能夠看見事務B因爲間隙鎖和插入意向鎖的衝突,致使了阻塞。小明發現上面查詢條件用的是普通的非惟一索引,因而小明就試了一下主鍵索引:
時間點 | 事務A | 事務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
竟然發現事務B並無發生阻塞,哎這個是咋回事呢,小明有點疑惑,按照實驗1的套路應該會被阻塞啊,由於25-30之間會有間隙鎖。因而小明又祭出了命令行,發現只加了X記錄鎖。原來是由於惟一索引會降級記錄鎖,這麼作的理由是:非惟一索引加next-key鎖因爲不能肯定明確的行數有可能其餘事務在你查詢的過程當中,再次添加這個索引的數據,致使隔離性遭到破壞,也就是幻讀。惟一索引因爲明確了惟一的數據行,因此不須要添加間隙鎖解決幻讀。
上面測試了主鍵索引,非惟一索引,這裏還有個字段是沒有索引,若是對其加鎖會出現什麼呢?
時間點 | 事務A | 事務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 |
小明一看哎喲我去,這個咋回事呢,咋無論是用實驗1非間隙鎖範圍的數據,仍是用間隙鎖裏面的數據都不行,難道是加了表鎖嗎?
的確,若是用沒有索引的數據,其會對全部聚簇索引上都加上next-key鎖。
因此你們日常開發的時候若是對查詢條件沒有索引的,必定進行一致性讀,也就是加鎖讀,會致使全表加上索引,會致使其餘事務所有阻塞,數據庫基本會處於不可用狀態。
小明作完實驗以後總算是瞭解清楚了加鎖的一些基本套路,可是以前線上出現的死鎖又是什麼東西呢?
死鎖:是指兩個或兩個以上的事務在執行過程當中,因爭奪資源而形成的一種互相等待的現象。說明有等待纔會有死鎖,解決死鎖能夠經過去掉等待,好比回滾事務。
解決死鎖的兩個辦法:
小明到這裏,基本須要的基本功都有了,因而在本身的本地表中開始復現這個問題:
時間點 | 事務A | 事務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 |
能夠看見事務A出現被回滾了,而事務B成功執行。 具體每一個時間點發生了什麼呢?
時間點2:事務A刪除name = '777'的數據,須要對777這個索引加上next-Key鎖,可是其不存在,因此只對555-999之間加間隙鎖,同理事務B也對555-999之間加間隙鎖。間隙鎖之間是兼容的。
時間點3:事務A,執行Insert操做,首先插入意向鎖,可是555-999之間有間隙鎖,因爲插入意向鎖和間隙鎖衝突,事務A阻塞,等待事務B釋放間隙鎖。事務B同理,等待事務A釋放間隙鎖。因而出現了A->B,B->A迴路等待。
時間點4:事務管理器選擇回滾事務A,事務B插入操做執行成功。
這個問題總算是被小明找到了,就是由於間隙鎖,如今須要解決這個問題,這個問題的緣由是出現了間隙鎖,那就來去掉他吧:
通過考慮小明選擇了第四種,立刻進行了修復,而後上線觀察驗證,發現如今已經不會出現這個Bug了,這下小明總算能睡個安穩覺了。
小明經過基礎的學習和日常的經驗總結了以下幾點:
因爲篇幅有限不少東西並不能介紹全若是感興趣的同窗能夠閱讀《Mysql技術內幕-InnoDB引擎》第6章 以及 何大師的MySQL 加鎖處理分析。做者本人水平有限,若是有什麼錯誤,還請指正。
最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:github.com/javagrowing… 麻煩給個小星星喲。
若是你以爲這篇文章對你有文章,能夠關注個人技術公衆號,你的關注和轉發是對我最大的支持,O(∩_∩)O