MySQL 避免行锁升级为表锁——使用高效的索引
眾所周知,MySQL 的 InnoDB 存儲(chǔ)引擎支持事務(wù),支持行級(jí)鎖(innodb的行鎖是通過給索引項(xiàng)加鎖實(shí)現(xiàn)的)。得益于這些特性,數(shù)據(jù)庫支持高并發(fā)。如果 InnoDB 更新數(shù)據(jù)使用的不是行鎖,而是表鎖呢?是的,InnoDB 其實(shí)很容易就升級(jí)為表鎖,屆時(shí)并發(fā)性將大打折扣了。
經(jīng)過我操作驗(yàn)證,得出行鎖升級(jí)為表鎖的原因之一是: SQL 語句中未使用到索引,或者說使用的索引未被數(shù)據(jù)庫認(rèn)可(相當(dāng)于沒有使用索引)。
我相信,MySQL InnoDB 存儲(chǔ)引擎引發(fā)表鎖的原因肯定不止一個(gè)因素,針對(duì)其解決方法也不是只有一種。
據(jù)掘金上另一位作者【Blink-前端】,提出行鎖升級(jí)為表鎖與 事務(wù)的隔離級(jí)別 有關(guān),并給出了事例。當(dāng)然,我同意這個(gè)說法,因?yàn)槭聞?wù)的隔離性是靠加鎖來實(shí)現(xiàn)的,而加鎖勢(shì)必會(huì)影響并發(fā)。本篇只針對(duì) 索引影響并發(fā) 作出說明,并特別希望有朋友能提出質(zhì)疑并給出獨(dú)特見解,萬分感謝。
普通索引
既然談及索引是影響并發(fā)的決定因素之一,那我們就來了解一下索引這位主角。
常用的索引有三類:主鍵、唯一索引、普通索引。主鍵 不由分說,自帶最高效的索引屬性;唯一索引指的是該屬性值重復(fù)率為0,一般可作為業(yè)務(wù)主鍵,例如學(xué)號(hào);普通索引 與前者不同的是,屬性值的重復(fù)率大于0,不能作為唯一指定條件,例如學(xué)生姓名。接下來我要說明是 “普通索引對(duì)并發(fā)的影響”。
為什么我會(huì)想到 “普通索引對(duì)并發(fā)有影響”?這源自【掘金】微信群拋出的一個(gè)問題:
mysql 5.6 在 update 和 delete 的時(shí)候,where 條件如果不存在索引字段,那么這個(gè)事務(wù)是否會(huì)導(dǎo)致表鎖?
有人回答:
只有主鍵和唯一索引才是行鎖,普通索引是表鎖。
我針對(duì) “普通索引是表鎖” 進(jìn)行了驗(yàn)證,結(jié)果發(fā)現(xiàn)普通索引并不一定會(huì)引發(fā)表鎖,在普通索引中,是否引發(fā)表鎖取決于普通索引的高效程度。
上文提及的“高效”是相對(duì)主鍵和唯一索引而言,也許“高效”并不是一個(gè)很好的解釋,明白在一般i情況下,“普通索引”效率低于其他兩者即可。
屬性值重復(fù)率高
為了突出效果,我將“普通索引”建立在一個(gè)“值重復(fù)率”高的屬性下。以相對(duì)極端的方式,擴(kuò)大對(duì)結(jié)果的影響。
我會(huì)創(chuàng)建一張“分?jǐn)?shù)等級(jí)表”,屬性有“id”、“score(分?jǐn)?shù))”、“l(fā)evel(等級(jí))”,模擬一個(gè)半自動(dòng)的業(yè)務(wù)——“分?jǐn)?shù)”已被自動(dòng)導(dǎo)入,而“等級(jí)”需要手工更新。
操作步驟如下:
取消 事務(wù)自動(dòng)提交:
mysql> set autocommit = off;Query OK, 0 rows affected (0.02 sec)mysql> show variables like "autocommit";+--------------------------+-------+| Variable_name | Value |+--------------------------+-------+| autocommit | OFF |+--------------------------+-------+1 rows in set (0.01 sec)建表、創(chuàng)建索引、插入數(shù)據(jù):
DROP TABLE IF EXISTS `test1`;CREATE TABLE `test1` (`ID` int(5) NOT NULL AUTO_INCREMENT ,`SCORE` int(3) NOT NULL ,`LEVEL` int(2) NULL DEFAULT NULL ,PRIMARY KEY (`ID`))ENGINE=InnoDB DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;ALTER TABLE `test2` ADD INDEX index_name ( `SCORE` );INSERT INTO `test1`(`SCORE`) VALUE (100);……INSERT INTO `test1`(`SCORE`) VALUE (0);……“SCORE” 屬性的“值重復(fù)率”奇高,達(dá)到了 50%,劍走偏鋒:
mysql> select * from test1;+----+-------+-------+| ID | SCORE | LEVEL |+----+-------+-------+| 1 | 100 | NULL || 2 | 0 | NULL || 5 | 100 | NULL || 6 | 100 | NULL || 7 | 100 | NULL || 8 | 100 | NULL || 9 | 100 | NULL || 10 | 100 | NULL || 11 | 100 | NULL || 12 | 100 | NULL || 13 | 100 | NULL || 14 | 0 | NULL || 15 | 0 | NULL || 16 | 0 | NULL || 17 | 0 | NULL || 18 | 0 | NULL || 19 | 0 | NULL || 20 | 0 | NULL || 21 | 0 | NULL || 22 | 0 | NULL || 23 | 0 | NULL || 24 | 100 | NULL || 25 | 0 | NULL || 26 | 100 | NULL || 27 | 0 | NULL |+----+-------+-------+25 rows in set開啟兩個(gè)事務(wù)(一個(gè)窗口對(duì)應(yīng)一個(gè)事務(wù)),并選定數(shù)據(jù):
-- SESSION_1,選定 SCORE = 100 的數(shù)據(jù)mysql> BEGIN;SELECT t.* FROM `test1` t WHERE t.`SCORE` = 100 FOR UPDATE;Query OK, 0 rows affected+----+-------+-------+| ID | SCORE | LEVEL |+----+-------+-------+| 1 | 100 | NULL || 5 | 100 | NULL || 6 | 100 | NULL || 7 | 100 | NULL || 8 | 100 | NULL || 9 | 100 | NULL || 10 | 100 | NULL || 11 | 100 | NULL || 12 | 100 | NULL || 13 | 100 | NULL || 24 | 100 | NULL || 26 | 100 | NULL |+----+-------+-------+12 rows in set再打開一個(gè)窗口:
-- SESSION_2,選定 SCORE = 0 的數(shù)據(jù)mysql> BEGIN;SELECT t.* FROM `test1` t WHERE t.`SCORE` = 0 FOR UPDATE;Query OK, 0 rows affected+----+-------+-------+| ID | SCORE | LEVEL |+----+-------+-------+| 2 | 0 | NULL || 14 | 0 | NULL || 15 | 0 | NULL || 16 | 0 | NULL || 17 | 0 | NULL || 18 | 0 | NULL || 19 | 0 | NULL || 20 | 0 | NULL || 21 | 0 | NULL || 22 | 0 | NULL || 23 | 0 | NULL || 25 | 0 | NULL || 27 | 0 | NULL |+----+-------+-------+13 rows in setsession_1 窗口,更新“LEVEL”失敗:
mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;1205 - Lock wait timeout exceeded; try restarting transaction在之前的操作中,session_1 選擇了 SCORE = 100 的數(shù)據(jù),session_2 選擇了 SCORE = 0 的數(shù)據(jù),看似兩個(gè)事務(wù)井水不犯河水,但是在 session_1 事務(wù)中更新自己鎖定的數(shù)據(jù)失敗,只能說明在此時(shí)引發(fā)了表鎖。別著急,剛剛走向了一個(gè)極端——索引屬性值重復(fù)性奇高,接下來走向另一個(gè)極端。
屬性值重復(fù)率低
還是同一張表,將數(shù)據(jù)刪除只剩下兩條,“SCORE” 的 “值重復(fù)率” 為 0:
mysql> delete from test1 where id > 2;Query OK, 23 rows affectedmysql> select * from test1;+----+-------+-------+| ID | SCORE | LEVEL |+----+-------+-------+| 1 | 100 | NULL || 2 | 0 | NULL |+----+-------+-------+2 rows in set關(guān)閉兩個(gè)事務(wù)操作窗口,重新開啟 session_1 和 session_2,并選擇各自需要的數(shù)據(jù):
-- SESSION_1,選定 SCORE = 100 的數(shù)據(jù)mysql> BEGIN;SELECT t.* FROM `test1` t WHERE t.`SCORE` = 100 FOR UPDATE;Query OK, 0 rows affected+----+-------+-------+| ID | SCORE | LEVEL |+----+-------+-------+| 1 | 100 | NULL |+----+-------+-------+1 row in set-- -----------------新窗口----------------- ---- SESSION_2,選定 SCORE = 0 的數(shù)據(jù)mysql> BEGIN;SELECT t.* FROM `test1` t WHERE t.`SCORE` = 0 FOR UPDATE;Query OK, 0 rows affected+----+-------+-------+| ID | SCORE | LEVEL |+----+-------+-------+| 2 | 0 | NULL |+----+-------+-------+1 row in setsession_1 更新數(shù)據(jù)成功:
mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;Query OK, 1 row affectedRows matched: 1 Changed: 1 Warnings: 0相同的表結(jié)構(gòu),相同的操作,兩個(gè)不同的結(jié)果讓人出乎意料。第一個(gè)結(jié)果讓人覺得“普通索引”引發(fā)表鎖,第二個(gè)結(jié)果推翻了前者,兩個(gè)操作中,唯一不同的是索引屬性的“值重復(fù)率”。根據(jù) 單一變量 證明法,可以得出結(jié)論:當(dāng)“值重復(fù)率”低時(shí),甚至接近主鍵或者唯一索引的效果,“普通索引”依然是行鎖;當(dāng)“值重復(fù)率”高時(shí),MySQL 不會(huì)把這個(gè)“普通索引”當(dāng)做索引,即造成了一個(gè)沒有索引的 SQL,此時(shí)引發(fā)表鎖。
小結(jié)
索引不是越多越好,索引存在一個(gè)和這個(gè)表相關(guān)的文件里,占用硬盤空間,寧缺勿濫,每個(gè)表都有主鍵(id),操作能使用主鍵盡量使用主鍵。
同 JVM 自動(dòng)優(yōu)化 java 代碼一樣,MySQL 也具有自動(dòng)優(yōu)化 SQL 的功能。低效的索引將被忽略,這也就倒逼開發(fā)者使用正確且高效的索引。
總結(jié)
以上是生活随笔為你收集整理的MySQL 避免行锁升级为表锁——使用高效的索引的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql多表统计查询示例
- 下一篇: 关于MySQL出现锁等待lock wai