MySQL 避免行鎖升級爲表鎖——使用高效的索引

  衆所周知,MySQL 的 InnoDB 存儲引擎支持事務,默認是行鎖。得益於這些特性,數據庫支持高併發。若是 InnoDB 更新數據使用的不是行鎖,而是表鎖呢?是的,InnoDB 其實很容易就升級爲表鎖,屆時併發性將大打折扣了。

  通過我操做驗證,得出行鎖升級爲表鎖的緣由之一是: SQL 語句中未使用到索引,或者說使用的索引未被數據庫承認(至關於沒有使用索引)。javascript

  我相信,MySQL InnoDB 存儲引擎引起表鎖的緣由確定不止一個因素,針對其解決方法也不是隻有一種。前端

  【掘金】上另外一位做者【Blink-前端】,提出行鎖升級爲表鎖與 事務的隔離級別 有關,並給出了事例。固然,我贊成這個說法,由於事務的隔離性是靠加鎖來實現的,而加鎖勢必會影響併發。本篇只針對 索引影響併發 做出說明,並特別但願有朋友能提出質疑並給出獨特看法,萬分感謝。java

普通索引

  既然談及索引是影響併發的決定因素之一,那咱們就來了解一下索引這位主角。mysql

  經常使用的索引有三類:主鍵、惟一索引、普通索引。主鍵 不禁分說,自帶最高效的索引屬性;惟一索引 指的是該屬性值重複率爲0,通常可做爲業務主鍵,例如學號;普通索引 與前者不一樣的是,屬性值的重複率大於0,不能做爲惟一指定條件,例如學生姓名。接下來我要說明是 「普通索引對併發的影響」。sql

  爲何我會想到 「普通索引對併發有影響」?這源自【掘金】微信羣拋出的一個問題:數據庫

mysql 5.6 在 update 和 delete 的時候,where 條件若是不存在索引字段,那麼這個事務是否會致使表鎖?微信

  有人回答:session

只有主鍵和惟一索引纔是行鎖,普通索引是表鎖。併發

  我針對 「普通索引是表鎖」 進行了驗證,結果發現普通索引並不必定會引起表鎖,在普通索引中,是否引起表鎖取決於普通索引的高效程度。高併發

  上文說起的「高效」是相對主鍵和惟一索引而言,也許「高效」並非一個很好的解釋,明白在通常i狀況下,「普通索引」效率低於其餘二者便可。

屬性值重複率高

  爲了突出效果,我將「普通索引」創建在一個「值重複率」高的屬性下。以相對極端的方式,擴大對結果的影響。

  我會建立一張「分數等級表」,屬性有「id」、「score(分數)」、「level(等級)」,模擬一個半自動的業務——「分數」已被自動導入,而「等級」須要手工更新。

  操做步驟以下:

  1. 取消 MySQL 的 事務自動提交
  2. 建表,id自增,並給「score(分數)」建立普通索引
  3. 插入分數值,等級爲 null
  4. 開啓兩個事務 session_一、session_2,兩個事務以「score」爲條件指定不一樣值,鎖定數據
  5. session_1 和 session_2 前後更新各自事務鎖定內容的「level」
  6. 觀察數據庫對兩個事務的響應

  取消 事務自動提交

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)複製代碼

  建表、建立索引、插入數據:

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" 屬性的「值重複率」奇高,達到了 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複製代碼

  開啓兩個事務(一個窗口對應一個事務),並選定數據:

-- SESSION_1,選定 SCORE = 100 的數據
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複製代碼

  再打開一個窗口:

-- SESSION_2,選定 SCORE = 0 的數據
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 set複製代碼

  session_1 窗口,更新「LEVEL」失敗:

mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;
1205 - Lock wait timeout exceeded; try restarting transaction複製代碼

  在以前的操做中,session_1 選擇了 `SCORE` = 100 的數據,session_2 選擇了 `SCORE` = 0 的數據,看似兩個事務井水不犯河水,可是在 session_1 事務中更新本身鎖定的數據失敗,只能說明在此時引起了表鎖。彆着急,剛剛走向了一個極端——索引屬性值重複性奇高,接下來走向另外一個極端。   

屬性值重複率低

  仍是同一張表,將數據刪除只剩下兩條,「SCORE」 的 「值重複率」 爲 0:

mysql> delete from test1 where id > 2;
Query OK, 23 rows affected

mysql> select * from test1;
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  1 |   100 | NULL  |
|  2 |     0 | NULL  |
+----+-------+-------+
2 rows in set複製代碼

  關閉兩個事務操做窗口,從新開啓 session_1 和 session_2,並選擇各自須要的數據:

-- SESSION_1,選定 SCORE = 100 的數據
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 的數據
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 set複製代碼

  session_1 更新數據成功:

mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0複製代碼

  相同的表結構,相同的操做,兩個不一樣的結果讓人出乎意料。第一個結果讓人以爲「普通索引」引起表鎖,第二個結果推翻了前者,兩個操做中,惟一不一樣的是索引屬性的「值重複率」。根據 單一變量 證實法,能夠得出結論:當「值重複率」低時,甚至接近主鍵或者惟一索引的效果,「普通索引」依然是行鎖;當「值重複率」高時,MySQL 不會把這個「普通索引」當作索引,即形成了一個沒有索引的 SQL,此時引起表鎖

小結

  索引不是越多越好,索引存在一個和這個表相關的文件裏,佔用硬盤空間,寧缺勿濫,每一個表都有主鍵(id),操做能使用主鍵儘可能使用主鍵。

  同 JVM 自動優化 java 代碼同樣,MySQL 也具備自動優化 SQL 的功能。低效的索引將被忽略,這也就倒逼開發者使用正確且高效的索引。

  轉載請註明出處:zhoupq.com/MySQL-%E9%8…

相關文章
相關標籤/搜索