為什么開(kāi)發(fā)人員必須要了解數(shù)據(jù)庫(kù)鎖?
當(dāng)然在我們的數(shù)據(jù)庫(kù)中也有鎖用來(lái)控制資源的并發(fā)訪問(wèn),這也是數(shù)據(jù)庫(kù)和文件系統(tǒng)的區(qū)別之一。
為什么要懂?dāng)?shù)據(jù)庫(kù)鎖?
通常來(lái)說(shuō)對(duì)于一般的開(kāi)發(fā)人員,在使用數(shù)據(jù)庫(kù)的時(shí)候一般懂點(diǎn) DQL(select),DML(insert,update,delete)就夠了。
小明是一個(gè)剛剛畢業(yè)在互聯(lián)網(wǎng)公司工作的 Java 開(kāi)發(fā)工程師,平常的工作就是完成 PM 的需求。
當(dāng)然在完成需求的同時(shí)肯定逃脫不了 Spring,Spring MVC,Mybatis 的那一套框架,所以一般來(lái)說(shuō) SQL 還是自己手寫,遇到比較復(fù)雜的 SQL 會(huì)從網(wǎng)上去百度一下。
對(duì)于一些比較重要操作,比如交易啊這些,小明會(huì)用 Spring 的事務(wù)來(lái)對(duì)數(shù)據(jù)庫(kù)的事務(wù)進(jìn)行管理,由于數(shù)據(jù)量比較小目前還涉及不到分布式事務(wù)。
前幾個(gè)月小明過(guò)得都還風(fēng)調(diào)雨順,直到有一天,小明接了一個(gè)需求,商家有個(gè)配置項(xiàng),叫優(yōu)惠配置項(xiàng),可以配置買一送一,買一送二等等規(guī)則。
當(dāng)然這些配置是批量傳輸給后端的,這樣就有個(gè)問(wèn)題每個(gè)規(guī)則都得去匹配,他到底是刪除還是添加還是修改,這樣后端邏輯就比較麻煩。
聰明的小明想到了一個(gè)辦法,直接刪除這個(gè)商家的配置,然后全部添加進(jìn)去。小明馬上開(kāi)發(fā)完畢,成功上線。
開(kāi)始上線沒(méi)什么毛病,但是日志經(jīng)常會(huì)出現(xiàn)一些 mysql-insert-deadlock 異常。
由于小明經(jīng)驗(yàn)比較淺,對(duì)于這類型的問(wèn)題第一次遇見(jiàn),于是去問(wèn)了他們組的老司機(jī)大紅。
大紅一看見(jiàn)這個(gè)問(wèn)題,然后看了他的代碼之后,輸出了幾個(gè)命令看了幾個(gè)日志,馬上定位了問(wèn)題,告訴了小明:這是因?yàn)?delete 的時(shí)候會(huì)加間隙鎖。
但是間隙鎖之間卻可以兼容,但是插入新的數(shù)據(jù)的時(shí)候就會(huì)因?yàn)椴迦胍庀蜴i會(huì)被間隙鎖阻塞,導(dǎo)致雙方資源被互占,導(dǎo)致死鎖。
小明聽(tīng)了之后似懂非懂,由于大紅的事情比較多,不方便一直麻煩大紅,所以決定自己下來(lái)自己想。
下班過(guò)后,小明回想大紅說(shuō)的話,什么是間隙鎖,什么是插入意向鎖,看來(lái)作為開(kāi)發(fā)者對(duì)數(shù)據(jù)庫(kù)不應(yīng)該只會(huì)寫 SQL 啊,不然遇到一些疑難雜癥完全沒(méi)法解決啊。想完,于是小明就踏上了學(xué)習(xí) MySQL 鎖這條不歸之路。
什么是 InnoDB?
MySQL 體系架構(gòu)
小明沒(méi)有著急去了解鎖這方面的知識(shí),他首先先了解了下 MySQL 體系架構(gòu):
可以發(fā)現(xiàn) MySQL 由連接池組件、管理服務(wù)和工具組件、SQL 接口組件、查詢分析器組件、優(yōu)化器組件、 緩沖組件、插件式存儲(chǔ)引擎、物理文件組成。
小明發(fā)現(xiàn)在 MySQL 中存儲(chǔ)引擎是以插件的方式提供的,在 MySQL 中有多種存儲(chǔ)引擎,每個(gè)存儲(chǔ)引擎都有自己的特點(diǎn)。
隨后小明在命令行中打出了:
- show engines \G;
一看原來(lái)有這么多種引擎。又打出了下面的命令,查看當(dāng)前數(shù)據(jù)庫(kù)默認(rèn)的引擎:
- show variables like '%storage_engine%';
小明恍然大悟:原來(lái)自己的數(shù)據(jù)庫(kù)是使用的 InnoDB,依稀記得自己在上學(xué)的時(shí)候好像聽(tīng)說(shuō)過(guò)有個(gè)引擎叫 MyIsAM,小明想這兩個(gè)有啥不同呢?
馬上查找了一下資料:
小明大概了解了一下 InnoDB 和 MyIsAM 的區(qū)別,由于使用的是 InnoDB,小明就沒(méi)有過(guò)多的糾結(jié)這一塊。
事務(wù)的隔離性
小明在研究鎖之前,又回想到之前上學(xué)的時(shí)候教過(guò)的數(shù)據(jù)庫(kù)事務(wù)隔離性,其實(shí)鎖在數(shù)據(jù)庫(kù)中其功能之一也是用來(lái)實(shí)現(xiàn)事務(wù)隔離性。而事務(wù)的隔離性其實(shí)是用來(lái)解決臟讀,不可重復(fù)讀,幻讀幾類問(wèn)題。
臟讀
一個(gè)事務(wù)讀取到另一個(gè)事務(wù)未提交的更新數(shù)據(jù)。什么意思呢?
在事務(wù) A,B 中,事務(wù) A 在時(shí)間點(diǎn) 2,4 分別對(duì) user 表中 id = 1 的數(shù)據(jù)進(jìn)行了查詢。
但是事務(wù) B 在時(shí)間點(diǎn) 3 進(jìn)行了修改,導(dǎo)致了事務(wù) A 在 4 中的查詢出的結(jié)果其實(shí)是事務(wù) B 修改后的。這樣就破壞了數(shù)據(jù)庫(kù)中的隔離性。
不可重復(fù)讀
在同一個(gè)事務(wù)中,多次讀取同一數(shù)據(jù)返回的結(jié)果不同,不可重復(fù)讀和臟讀不同的是這里讀取的是已經(jīng)提交過(guò)后的數(shù)據(jù)。
在事務(wù) B 中提交的操作在事務(wù) A 第二次查詢之前,但是依然讀到了事務(wù) B 的更新結(jié)果,也破壞了事務(wù)的隔離性。
幻讀
一個(gè)事務(wù)讀到另一個(gè)事務(wù)已提交的 insert 數(shù)據(jù)。
在事務(wù) A 中查詢了兩次 id 大于 1 的,在第一次 id 大于 1 查詢結(jié)果中沒(méi)有數(shù)據(jù),但是由于事務(wù) B 插入了一條 id = 2 的數(shù)據(jù),導(dǎo)致事務(wù) A 第二次查詢時(shí)能查到事務(wù) B 中插入的數(shù)據(jù)。
事務(wù)中的隔離性:
小明注意到在收集資料的過(guò)程中,有資料寫到 InnoDB 和其他數(shù)據(jù)庫(kù)有點(diǎn)不同,InnoDB 的可重復(fù)讀其實(shí)就能解決幻讀了,小明心想:這 InnoDB 還挺牛逼的,我得好好看看到底是怎么個(gè)原理。
InnoDB 鎖類型
小明首先了解了 MySQL 中常見(jiàn)的鎖類型有哪些:
S or X
在 InnoDB 中實(shí)現(xiàn)了兩個(gè)標(biāo)準(zhǔn)的行級(jí)鎖,可以簡(jiǎn)單的看為兩個(gè)讀寫鎖:
S 共享鎖:又叫讀鎖,其他事務(wù)可以繼續(xù)加共享鎖,但是不能繼續(xù)加排他鎖。
X 排他鎖:又叫寫鎖,一旦加了寫鎖之后,其他事務(wù)就不能加鎖了。
兼容性:是指事務(wù) A 獲得一個(gè)某行某種鎖之后,事務(wù) B 同樣的在這個(gè)行上嘗試獲取某種鎖,如果能立即獲取,則稱鎖兼容,反之叫沖突。
縱軸是代表已有的鎖,橫軸是代表嘗試獲取的鎖。
意向鎖
意向鎖在 InnoDB 中是表級(jí)鎖,和它的名字一樣它是用來(lái)表達(dá)一個(gè)事務(wù)想要獲取什么。
意向鎖分為:
- 意向共享鎖:表達(dá)一個(gè)事務(wù)想要獲取一張表中某幾行的共享鎖。
- 意向排他鎖:表達(dá)一個(gè)事務(wù)想要獲取一張表中某幾行的排他鎖。
這個(gè)鎖有什么用呢?為什么需要這個(gè)鎖呢?首先說(shuō)一下如果沒(méi)有這個(gè)鎖,要給這個(gè)表加上表鎖,一般的做法是去遍歷每一行看看它是否有行鎖,這樣的話效率太低。
而我們有意向鎖,只需要判斷是否有意向鎖即可,不需要再去一行行的去掃描。
在 InnoDB 中由于支持的是行級(jí)的鎖,因此 InnboDB 鎖的兼容性可以擴(kuò)展如下:
自增長(zhǎng)鎖
自增長(zhǎng)鎖是一種特殊的表鎖機(jī)制,提升并發(fā)插入性能。
對(duì)于這個(gè)鎖有幾個(gè)特點(diǎn):
- 在 SQL 執(zhí)行完就釋放鎖,并不是事務(wù)執(zhí)行完。
- 對(duì)于 insert...select 大數(shù)據(jù)量插入會(huì)影響插入性能,因?yàn)闀?huì)阻塞另外一個(gè)事務(wù)執(zhí)行。
- 自增算法可以配置。
在 MySQL 5.1.2 版本之后,有了很多優(yōu)化,可以根據(jù)不同的模式來(lái)調(diào)整自增加鎖的方式。
小明看到了這里打開(kāi)了自己的 MySQL 發(fā)現(xiàn)是 5.7 之后,便輸入了下面的語(yǔ)句,獲取到當(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,分別對(duì)應(yīng):
傳統(tǒng)模式:也就是我們最上面的使用表鎖。
連續(xù)模式:對(duì)于插入的時(shí)候可以確定行數(shù)的使用互斥量,對(duì)于不能確定行數(shù)的使用表鎖的模式。
交錯(cuò)模式:所有的都使用互斥量,為什么叫交錯(cuò)模式呢,有可能在批量插入時(shí)自增值不是連續(xù)的,當(dāng)然一般來(lái)說(shuō)如果不看重自增值連續(xù)一般選擇這個(gè)模式,性能是最好的。
InnoDB 鎖算法
小明已經(jīng)了解到了在 InnoDB 中有哪些鎖類型,但是如何去使用這些鎖,還是得靠鎖算法。
記錄鎖(Record-Lock)
記錄鎖是鎖住記錄的,這里要說(shuō)明的是這里鎖住的是索引記錄,而不是我們真正的數(shù)據(jù)記錄:
如果鎖的是非主鍵索引,會(huì)在自己的索引上面加鎖之后然后再去主鍵上面加鎖鎖住。
如果沒(méi)有表上沒(méi)有索引(包括沒(méi)有主鍵),則會(huì)使用隱藏的主鍵索引進(jìn)行加鎖。
如果要鎖的列沒(méi)有索引,則會(huì)進(jìn)行全表記錄加鎖。
間隙鎖
間隙鎖顧名思義鎖間隙,不鎖記錄。鎖間隙的意思就是鎖定某一個(gè)范圍,間隙鎖又叫 gap 鎖,其不會(huì)阻塞其他的 gap 鎖,但是會(huì)阻塞插入間隙鎖,這也是用來(lái)防止幻讀的關(guān)鍵。

next-key 鎖
這個(gè)鎖本質(zhì)是記錄鎖加上 gap 鎖。在 RR 隔離級(jí)別下(InnoDB 默認(rèn)),InnoDB 對(duì)于行的掃描鎖定都是使用此算法,但是如果查詢掃描中有唯一索引會(huì)退化成只使用記錄鎖。
為什么呢? 因?yàn)槲ㄒ凰饕艽_定行數(shù),而其他索引不能確定行數(shù),有可能在其他事務(wù)中會(huì)再次添加這個(gè)索引的數(shù)據(jù)造成幻讀。
這里也說(shuō)明了為什么 MySQL 可以在 RR 級(jí)別下解決幻讀。
插入意向鎖
插入意向鎖 MySQL 官方對(duì)其的解釋:
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.
可以看出插入意向鎖是在插入的時(shí)候產(chǎn)生的,在多個(gè)事務(wù)同時(shí)寫入不同數(shù)據(jù)至同一索引間隙的時(shí)候,并不需要等待其他事務(wù)完成,不會(huì)發(fā)生鎖等待。
假設(shè)有一個(gè)記錄索引包含鍵值 4 和 7,不同的事務(wù)分別插入 5 和 6,每個(gè)事務(wù)都會(huì)產(chǎn)生一個(gè)加在 4-7 之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會(huì)被互相鎖住,因?yàn)閿?shù)據(jù)行并不沖突。
這里要說(shuō)明的是如果有間隙鎖了,插入意向鎖會(huì)被阻塞。
MVCC
MVCC,多版本并發(fā)控制技術(shù)。在 InnoDB 中,在每一行記錄的后面增加兩個(gè)隱藏列,記錄創(chuàng)建版本號(hào)和刪除版本號(hào)。通過(guò)版本號(hào)和行鎖,從而提高數(shù)據(jù)庫(kù)系統(tǒng)并發(fā)性能。
在 MVCC 中,對(duì)于讀操作可以分為兩種讀:
- 快照讀:讀取的歷史數(shù)據(jù),簡(jiǎn)單的 select 語(yǔ)句,不加鎖,MVCC 實(shí)現(xiàn)可重復(fù)讀,使用的是 MVCC 機(jī)制讀取 undo 中的已經(jīng)提交的數(shù)據(jù)。所以它的讀取是非阻塞的。
- 當(dāng)前讀:需要加鎖的語(yǔ)句,update,insert,delete,select...for update 等等都是當(dāng)前讀。
在 RR 隔離級(jí)別下的快照讀,不是以 begin 事務(wù)開(kāi)始的時(shí)間點(diǎn)作為 snapshot 建立時(shí)間點(diǎn),而是以第一條 select 語(yǔ)句的時(shí)間點(diǎn)作為 snapshot 建立的時(shí)間點(diǎn)。以后的 select 都會(huì)讀取當(dāng)前時(shí)間點(diǎn)的快照值。
在 RC 隔離級(jí)別下每次快照讀均會(huì)創(chuàng)建新的快照。
具體的原理是通過(guò)每行會(huì)有兩個(gè)隱藏的字段一個(gè)是用來(lái)記錄當(dāng)前事務(wù),一個(gè)是用來(lái)記錄回滾的指向 Undolog。利用 Undolog 就可以讀取到之前的快照,不需要單獨(dú)開(kāi)辟空間記錄。
加鎖分析
小明到這里,已經(jīng)學(xué)習(xí)很多 MySQL 鎖有關(guān)的基礎(chǔ)知識(shí),所以決定自己創(chuàng)建一個(gè)表搞下實(shí)驗(yàn)。
首先創(chuàng)建了一個(gè)簡(jiǎn)單的用戶表:
- 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ù)庫(kù)事務(wù)隔離選擇了 RR。
實(shí)驗(yàn) 1
小明開(kāi)啟了兩個(gè)事務(wù),進(jìn)行了實(shí)驗(yàn) 1,如下圖:
小明開(kāi)啟了兩個(gè)事務(wù)并輸入了上面的語(yǔ)句,發(fā)現(xiàn)事務(wù) B 居然出現(xiàn)了超時(shí),小明看了一下自己明明是對(duì) name = 555 這一行進(jìn)行的加鎖,為什么我想插入 name = 556 給我阻塞了。
于是小明打開(kāi)命令行輸入:
- select * from information_schema.INNODB_LOCKS
發(fā)現(xiàn)在事務(wù) A 中給 555 加了 next-key 鎖,事務(wù) B 插入的時(shí)候會(huì)首先進(jìn)行插入意向鎖的插入。
于是得出下面結(jié)果:
可以看見(jiàn)事務(wù) B 由于間隙鎖和插入意向鎖的沖突,導(dǎo)致了阻塞。
實(shí)驗(yàn) 2
小明發(fā)現(xiàn)上面查詢條件用的是普通的非唯一索引,于是小明就試了一下主鍵索引:
居然發(fā)現(xiàn)事務(wù) B 并沒(méi)有發(fā)生阻塞,哎這個(gè)是咋回事呢,小明有點(diǎn)疑惑,按照實(shí)驗(yàn) 1 的套路應(yīng)該會(huì)被阻塞啊,因?yàn)?25-30 之間會(huì)有間隙鎖。
于是小明又祭出了命令行,發(fā)現(xiàn)只加了 X 記錄鎖。原來(lái)是因?yàn)槲ㄒ凰饕龝?huì)降級(jí)記錄鎖。
這么做的理由是:非唯一索引加 next-key 鎖由于不能確定明確的行數(shù)有可能其他事務(wù)在你查詢的過(guò)程中,再次添加這個(gè)索引的數(shù)據(jù),導(dǎo)致隔離性遭到破壞,也就是幻讀。
唯一索引由于明確了唯一的數(shù)據(jù)行,所以不需要添加間隙鎖解決幻讀。
實(shí)驗(yàn) 3
上面測(cè)試了主鍵索引,非唯一索引,這里還有個(gè)字段是沒(méi)有索引,如果對(duì)其加鎖會(huì)出現(xiàn)什么呢?
小明一看哎喲我去,這個(gè)咋回事呢,咋不管是用實(shí)驗(yàn) 1 非間隙鎖范圍的數(shù)據(jù),還是用間隙鎖里面的數(shù)據(jù)都不行,難道是加了表鎖嗎?
的確,如果用沒(méi)有索引的數(shù)據(jù),其會(huì)對(duì)所有聚簇索引上都加上 next-key 鎖。
所以大家平常開(kāi)發(fā)的時(shí)候如果對(duì)查詢條件沒(méi)有索引的,一定進(jìn)行一致性讀,也就是加鎖讀,會(huì)導(dǎo)致全表加上索引,會(huì)導(dǎo)致其他事務(wù)全部阻塞,數(shù)據(jù)庫(kù)基本會(huì)處于不可用狀態(tài)。
回到事故
死鎖
小明做完實(shí)驗(yàn)之后總算是了解清楚了加鎖的一些基本套路,但是之前線上出現(xiàn)的死鎖又是什么東西呢?
死鎖是指兩個(gè)或兩個(gè)以上的事務(wù)在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的一種互相等待的現(xiàn)象。說(shuō)明有等待才會(huì)有死鎖,解決死鎖可以通過(guò)去掉等待,比如回滾事務(wù)。
解決死鎖的兩個(gè)辦法:
- 等待超時(shí):當(dāng)某一個(gè)事務(wù)等待超時(shí)之后回滾該事務(wù),另外一個(gè)事務(wù)就可以執(zhí)行了。
但是這樣做效率較低,會(huì)出現(xiàn)等待時(shí)間,還有個(gè)問(wèn)題是如果這個(gè)事務(wù)所占的權(quán)重較大,已經(jīng)更新了很多數(shù)據(jù)了,但是被回滾了,就會(huì)導(dǎo)致資源浪費(fèi)。
- 等待圖(wait-for-graph):等待圖用來(lái)描述事務(wù)之間的等待關(guān)系,當(dāng)這個(gè)圖如果出現(xiàn)回路如下:
事務(wù)就出現(xiàn)回滾,通常來(lái)說(shuō) InnoDB 會(huì)選擇回滾權(quán)重較小的事務(wù),也就是 undo 較小的事務(wù)。
線上問(wèn)題
小明到這里,基本需要的基本功都有了,于是在自己的本地表中開(kāi)始復(fù)現(xiàn)這個(gè)問(wèn)題:
可以看見(jiàn)事務(wù) A 出現(xiàn)被回滾了,而事務(wù) B 成功執(zhí)行。具體每個(gè)時(shí)間點(diǎn)發(fā)生了什么呢?
時(shí)間點(diǎn) 2:事務(wù) A 刪除 name = '777' 的數(shù)據(jù),需要對(duì) 777 這個(gè)索引加上 next-key 鎖,但是其不存在。
所以只對(duì) 555-999 之間加間隙鎖,同理事務(wù) B 也對(duì) 555-999 之間加間隙鎖。間隙鎖之間是兼容的。
時(shí)間點(diǎn) 3:事務(wù) A,執(zhí)行 insert 操作,首先插入意向鎖,但是 555-999 之間有間隙鎖。
由于插入意向鎖和間隙鎖沖突,事務(wù) A 阻塞,等待事務(wù) B 釋放間隙鎖。事務(wù) B 同理,等待事務(wù) A 釋放間隙鎖。于是出現(xiàn)了 A->B,B->A 回路等待。
時(shí)間點(diǎn) 4:事務(wù)管理器選擇回滾事務(wù) A,事務(wù) B 插入操作執(zhí)行成功。
修復(fù) Bug
這個(gè)問(wèn)題總算是被小明找到了,就是因?yàn)殚g隙鎖,現(xiàn)在需要解決這個(gè)問(wèn)題。
這個(gè)問(wèn)題的原因是出現(xiàn)了間隙鎖,那就來(lái)去掉它吧:
- 方案一:隔離級(jí)別降級(jí)為 RC,在 RC 級(jí)別下不會(huì)加入間隙鎖,所以就不會(huì)出現(xiàn)毛病了,但是在 RC 級(jí)別下會(huì)出現(xiàn)幻讀,可提交讀都破壞隔離性的毛病,所以這個(gè)方案不行。
- 方案二:隔離級(jí)別升級(jí)為可序列化,小明經(jīng)過(guò)測(cè)試后發(fā)現(xiàn)不會(huì)出現(xiàn)這個(gè)問(wèn)題,但是在可序列化級(jí)別下,性能會(huì)較低,會(huì)出現(xiàn)較多的鎖等待,同樣的也不考慮。
- 方案三:修改代碼邏輯,不要直接刪,改成每個(gè)數(shù)據(jù)由業(yè)務(wù)邏輯去判斷哪些是更新,哪些是刪除,那些是添加,這個(gè)工作量稍大,小明寫這個(gè)直接刪除的邏輯就是為了不做這些復(fù)雜的事的,所以這個(gè)方案先不考慮。
- 方案四:較少的修改代碼邏輯,在刪除之前,可以通過(guò)快照查詢(不加鎖),如果查詢沒(méi)有結(jié)果,則直接插入;如果有通過(guò)主鍵進(jìn)行刪除,在之前第三節(jié)實(shí)驗(yàn) 2 中,通過(guò)唯一索引會(huì)降級(jí)為記錄鎖,所以不存在間隙鎖。
經(jīng)過(guò)考慮小明選擇了第四種,馬上進(jìn)行了修復(fù),然后上線觀察驗(yàn)證,發(fā)現(xiàn)現(xiàn)在已經(jīng)不會(huì)出現(xiàn)這個(gè) Bug 了,這下小明總算能睡個(gè)安穩(wěn)覺(jué)了。
如何防止死鎖
小明通過(guò)基礎(chǔ)的學(xué)習(xí)和平常的經(jīng)驗(yàn)總結(jié)了如下幾點(diǎn):
- 以固定的順序訪問(wèn)表和行。交叉訪問(wèn)更容易造成事務(wù)等待回路。
- 盡量避免大事務(wù),占有的資源鎖越多,越容易出現(xiàn)死鎖。建議拆成小事務(wù)。
- 降低隔離級(jí)別。如果業(yè)務(wù)允許(上面也分析了,某些業(yè)務(wù)并不能允許),將隔離級(jí)別調(diào)低也是較好的選擇,比如將隔離級(jí)別從 RR 調(diào)整為 RC,可以避免掉很多因?yàn)? gap 鎖造成的死鎖。
- 為表添加合理的索引。防止沒(méi)有索引出現(xiàn)表鎖,出現(xiàn)死鎖的概率會(huì)突增。
最后
由于篇幅有限很多東西并不能介紹全,如果感興趣的同學(xué)可以閱讀《MySQL 技術(shù)內(nèi)幕-InnoDB 引擎》第 6 章,以及何大師的 MySQL 加鎖處理分析。