1.鎖?1.1何為鎖 鎖在現(xiàn)實中的意義為:封閉的器物,以鑰匙或暗碼開啟。在計算機(jī)中的鎖一般用來管理對共享資源的并發(fā)訪問,比如我們java同學(xué)熟悉的Lock,synchronized等都是我們常見的鎖。當(dāng)然在我們的數(shù)據(jù)庫中也有鎖用來控制資源的并發(fā)訪問,這也是數(shù)據(jù)庫和文件系統(tǒng)的區(qū)別之一。 1.2為什么要懂?dāng)?shù)據(jù)庫鎖? 通常來說對于一般的開發(fā)人員,在使用數(shù)據(jù)庫的時候一般懂點(diǎn)DQL(select),DML(insert,update,delete)就夠了。 小明是一個剛剛畢業(yè)在互聯(lián)網(wǎng)公司工作的Java開發(fā)工程師,平常的工作就是完成PM的需求,當(dāng)然在完成需求的同時肯定逃脫不了spring,springmvc,mybatis的那一套框架,所以一般來說sql還是自己手寫,遇到比較復(fù)雜的sql會從網(wǎng)上去百度一下。對于一些比較重要操作,比如交易啊這些,小明會用spring的事務(wù)來對數(shù)據(jù)庫的事務(wù)進(jìn)行管理,由于數(shù)據(jù)量比較小目前還涉及不了分布式事務(wù)。 前幾個月小明過得都還風(fēng)調(diào)雨順,知道有一天,小明接了一個需求,商家有個配置項,叫優(yōu)惠配置項,可以配置買一送一,買一送二等等規(guī)則,當(dāng)然這些配置是批量傳輸給后端的,這樣就有個問題每個規(guī)則都得去匹配他到底是刪除還是添加還是修改,這樣后端邏輯就比較麻煩,聰明的小明想到了一個辦法,直接刪除這個商家的配置,然后全部添加進(jìn)去。小明馬上開發(fā)完畢,成功上線。 開始上線沒什么毛病,但是日志經(jīng)常會出現(xiàn)一些mysql-insert-deadlock異常。由于小明經(jīng)驗比較淺,對于這類型的問題第一次遇見,于是去問了他們組的老司機(jī)-大紅,大紅一看見這個問題,然后看了他的代碼之后,輸出了幾個命令看了幾個日志,馬上定位了問題,告訴了小明:這是因為delete的時候會加間隙鎖,但是間隙鎖之間卻可以兼容,但是插入新的數(shù)據(jù)的時候就會因為插入意向鎖會被間隙鎖阻塞,導(dǎo)致雙方被資源被互占,導(dǎo)致死鎖。小明聽了之后似懂非懂,由于大紅的事情比較多,不方便一直麻煩大紅,所以決定自己下來自己想。下班過后,小明回想大紅說的話,什么是間隙鎖,什么是插入意向鎖,看來作為開發(fā)者對數(shù)據(jù)庫不應(yīng)該只會寫SQL啊,不然遇到一些疑難雜癥完全沒法解決啊。想完,于是小明就踏上了學(xué)習(xí)Mysql鎖這條不歸之路。 2.InnoDB2.1mysql體系架構(gòu) 小明沒有著急去了解鎖這方面的知識,他首先先了解了下Mysql體系架構(gòu): 可以發(fā)現(xiàn)Mysql由連接池組件、管理服務(wù)和工具組件、sql接口組件、查詢分析器組件、優(yōu)化器組件、 緩沖組件、插件式存儲引擎、物理文件組成。小明發(fā)現(xiàn)在mysql中存儲引擎是以插件的方式提供的,在Mysql中有多種存儲引擎,每個存儲引擎都有自己的特點(diǎn)。隨后小明在命令行中打出了: show engines \G; 一看原來有這么多種引擎。 又打出了下面的命令,查看當(dāng)前數(shù)據(jù)庫默認(rèn)的引擎: show variables like '%storage_engine%'; 小明恍然大悟:原來自己的數(shù)據(jù)庫是使用的InnoDB,依稀記得自己在上學(xué)的時候好像聽說過有個引擎叫MyIsAM,小明想這兩個有啥不同呢?馬上查找了一下資料: 對比項 InnoDB MyIsAM 事務(wù) 支持 不支持 鎖 支持MVCC行鎖 表鎖 外鍵 支持 不支持 存儲空間 存儲空間由于需要高速緩存,較大 可壓縮 適用場景 有一定量的update和Insert 大量的select 小明大概了解了一下InnoDB和MyIsAM的區(qū)別,由于使用的是InnoDB,小明就沒有過多的糾結(jié)這一塊。 2.2事務(wù)的隔離性 小明在研究鎖之前,又回想到之前上學(xué)的時候教過的數(shù)據(jù)庫事務(wù)隔離性,其實鎖在數(shù)據(jù)庫中其功能之一也是用來實現(xiàn)事務(wù)隔離性。而事務(wù)的隔離性其實是用來解決,臟讀,不可重復(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é)果其實是事務(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ù)的隔離性。 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ù)讀其實就能解決幻讀了,小明心想:這InnoDB還挺牛逼的,我得好好看看到底是怎么個原理。 2.3 InnoDB鎖類型 小明首先了解一下Mysql中常見的鎖類型有哪些: 2.3.1 S or X 在InnoDb中實現(xiàn)了兩個標(biāo)準(zhǔn)的行級鎖,可以簡單的看為兩個讀寫鎖:
縱軸是代表已有的鎖,橫軸是代表嘗試獲取的鎖。 . 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中innodb_autoinc_lock_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對于行的掃描鎖定都是使用此算法,但是如果查詢掃描中有唯一索引會退化成只使用記錄鎖。為什么呢? 因為唯一索引能確定行數(shù),而其他索引不能確定行數(shù),有可能在其他事務(wù)中會再次添加這個索引的數(shù)據(jù)會造成幻讀。
2.4.4 插入意向鎖 插入意向鎖Mysql官方對其的解釋:
可以看出插入意向鎖是在插入的時候產(chǎn)生的,在多個事務(wù)同時寫入不同數(shù)據(jù)至同一索引間隙的時候,并不需要等待其他事務(wù)完成,不會發(fā)生鎖等待。假設(shè)有一個記錄索引包含鍵值4和7,不同的事務(wù)分別插入5和6,每個事務(wù)都會產(chǎn)生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因為數(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)建新的快照。
3.加鎖分析小明到這里,已經(jīng)學(xué)習(xí)很多mysql鎖有關(guān)的基礎(chǔ)知識,所以決定自己創(chuàng)建一個表搞下實驗。首先創(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ù)據(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 實驗1 小明開啟了兩個事務(wù),進(jì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ù)制代碼 發(fā)現(xiàn)在事務(wù)A中給555加了Next-key鎖,事務(wù)B插入的時候會首先進(jìn)行插入意向鎖的插入,于是得出下面結(jié)論: 可以看見事務(wù)B由于間隙鎖和插入意向鎖的沖突,導(dǎo)致了阻塞。3.2 實驗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)疑惑,按照實驗1的套路應(yīng)該會被阻塞啊,因為25-30之間會有間隙鎖。于是小明又祭出了命令行,發(fā)現(xiàn)只加了X記錄鎖。原來是因為唯一索引會降級記錄鎖,這么做的理由是:非唯一索引加next-key鎖由于不能確定明確的行數(shù)有可能其他事務(wù)在你查詢的過程中,再次添加這個索引的數(shù)據(jù),導(dǎo)致隔離性遭到破壞,也就是幻讀。唯一索引由于明確了唯一的數(shù)據(jù)行,所以不需要添加間隙鎖解決幻讀。 3.3 實驗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 小明一看哎喲我去,這個咋回事呢,咋不管是用實驗1非間隙鎖范圍的數(shù)據(jù),還是用間隙鎖里面的數(shù)據(jù)都不行,難道是加了表鎖嗎? 的確,如果用沒有索引的數(shù)據(jù),其會對所有聚簇索引上都加上next-key鎖。 所以大家平常開發(fā)的時候如果對查詢條件沒有索引的,一定進(jìn)行一致性讀,也就是加鎖讀,會導(dǎo)致全表加上索引,會導(dǎo)致其他事務(wù)全部阻塞,數(shù)據(jù)庫基本會處于不可用狀態(tài)。 4.回到事故4.1 死鎖 小明做完實驗之后總算是了解清楚了加鎖的一些基本套路,但是之前線上出現(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 這個問題總算是被小明找到了,就是因為間隙鎖,現(xiàn)在需要解決這個問題,這個問題的原因是出現(xiàn)了間隙鎖,那就來去掉他吧:
經(jīng)過考慮小明選擇了第四種,馬上進(jìn)行了修復(fù),然后上線觀察驗證,發(fā)現(xiàn)現(xiàn)在已經(jīng)不會出現(xiàn)這個Bug了,這下小明總算能睡個安穩(wěn)覺了。 4.4 如何防止死鎖 小明通過基礎(chǔ)的學(xué)習(xí)和平常的經(jīng)驗總結(jié)了如下幾點(diǎn):
最后作者本人水平有限,如果有什么錯誤,還請指正。 最后的最后,需要架構(gòu)師資料的請私信我:“資料”。 |
|