MySQL INNODB鎖分析心得

場景重現

有一個多門店點餐系統,點餐就是個下訂單的過程,數據庫設計上是一個訂單表存儲了全部店鋪的訂單信息,則有一個order表,而且每條記錄都會關聯business_idmysql

DDL:sql

CREATE TABLE `o` (
  `id` int(11) NOT NULL,
  `business_id` int(11) DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
複製代碼

插入數據:數據庫

INSERT INTO test_innodb.`order` (id,business_id,status) VALUES 
(0,0,0)
,(5,5,5)
,(10,10,10)
,(11,10,15)
,(20,20,20)
,(25,25,25);
複製代碼
  • 更新店鋪id爲10的訂單狀態爲3

UPDATE o SET status = 3 WHERE business_id = 10;bash

session A session B
begin;
UPDATE o SET status = 3 WHERE business_id = 10;
begin;
INSERT INTO o VALUES(30, 10, 3) (BLOCKED)

查詢當前INNODB_LOCKS的狀況session

MySQL [test_innodb]> SELECT * FROM information_schema.INNODB_LOCKS;
+-------------------+-------------+-----------+-----------+-----------------------+------------+------------+-----------+----------+------------------------+
| lock_id           | lock_trx_id | lock_mode | lock_type | lock_table            | lock_index | lock_space | lock_page | lock_rec | lock_data              |
+-------------------+-------------+-----------+-----------+-----------------------+------------+------------+-----------+----------+------------------------+
| 41043937:3410:3:1 | 41043937    | X         | RECORD    | `test_innodb`.`order` | PRIMARY    |       3410 |         3 |        1 | supremum pseudo-record |
| 41043889:3410:3:1 | 41043889    | X         | RECORD    | `test_innodb`.`order` | PRIMARY    |       3410 |         3 |        1 | supremum pseudo-record |
+-------------------+-------------+-----------+-----------+-----------------------+------------+------------+-----------+----------+------------------------+
複製代碼

能夠看到當前lockdata中顯示了supremum,實際上因爲business_id不是索引,Mysql須要從根結點開始遍歷全部主鍵索引,過濾找到匹配的值,因此全部記錄包括 [30, supremum+] 的主鍵索引都被鎖住了。數據庫設計

此時給business_id加上一個普通索引,重放上面的session狀況,再次查詢INNODB_LOCKS的狀況性能

MySQL [test_innodb]> SELECT * FROM information_schema.INNODB_LOCKS;
+-------------------+-------------+-----------+-----------+-----------------------+-----------------------+------------+-----------+----------+-----------+
| lock_id           | lock_trx_id | lock_mode | lock_type | lock_table            | lock_index            | lock_space | lock_page | lock_rec | lock_data |
+-------------------+-------------+-----------+-----------+-----------------------+-----------------------+------------+-----------+----------+-----------+
| 41042271:3410:4:6 | 41042271    | X,GAP     | RECORD    | `test_innodb`.`order` | order_business_id_IDX |       3410 |         4 |        6 | 20, 20    |
| 41034805:3410:4:6 | 41034805    | X,GAP     | RECORD    | `test_innodb`.`order` | order_business_id_IDX |       3410 |         4 |        6 | 20, 20    |
+-------------------+-------------+-----------+-----------+-----------------------+-----------------------+------------+-----------+----------+-----------+
複製代碼

lock_type列中出現了新的字眼GAP,這是爲了不新數據插入產生的一種鎖,根據目前SQL執行的狀況,在普通索引business_id (5, 20)會被鎖,主鍵索引id (5,20) 也會被加上鎖。這是因爲Mysql 先經過普通索引business_id 符合搜索值爲10的索引,這個過程左邊第一個不符合的條件爲5,右邊第一個不符合的條件爲20,而當中的每一次遍歷都會同時鎖上主鍵索引。優化

理論分析

索引基礎知識

  1. 經過非主鍵索引查詢時,Mysql先找到非主鍵索引,再找到主鍵索引,再找到記錄(B+樹)
  2. 非惟一索引搜索時,會向左向右找到第一個不符合條件的索引(二分查找)
  3. Mysql Innodb的鎖做用是索引
  4. 一個SQL中若是有屢次加鎖操做,他們並非在一個指令內完成的

Next-Key Lock

衆所周知,使用了INNODB引擎後天然就會有行鎖的概念,而行鎖又分爲S鎖和X鎖,分別表明了讀鎖和寫鎖,他們之間有以下關係:ui

S X
S 共享 衝突
X 衝突 衝突

SHARE MODE、FOR UPDATE 、UPDATE、DELETE、INSERT 都會產生不一樣的鎖類型,具體能夠參考MySQL的官方文檔,這裏咱們暫時只須要了解是行鎖便可spa

再有,大部分場景下咱們都會沿用Mysql的默認事務隔離級別:Repeatable read,簡稱RR(可重複讀),這個機制自己能夠避免髒讀重複讀的問題,不過沒法解決幻讀問題,除非將隔離級別上升到Serializable(可串行化),可是這樣性能損耗太大。

MySQL爲了更好地解決幻讀問題,在INNODB引擎裏的可重複讀隔離級別下加入了Gap Lock(間隙鎖)的機制。對於間隙鎖能夠先大體理解爲,MySQL爲了不新的數據插入形成幻讀,加大了行鎖的粒度,鎖住了某個索引的數據區間

因此 MVCC(RR的特性) + Gap Lock + Row Lock 解決了髒讀、重複讀、幻讀問題。

  • 從而,這裏就產生了 Next-Key Lock 的定義: Gap Lock + Row Lock,這是一個前開後閉的區間。

MVCC的特性比較好理解和分析,這裏重點分析RR下的MySQL的加鎖過程。 首先,這裏總結了幾條MySQL的加鎖和優化原則:

  • 原則 1:加鎖的基本單位是 next-key lock。next-key lock 是前開後閉區間。
  • 原則 2:查找過程當中訪問到的對象纔會加鎖。
  • 優化 1:索引上的等值查詢,給惟一索引加鎖的時候,next-key lock 退化爲行鎖。
  • 優化 2:索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock 退化爲間隙鎖。
  • 一個 bug:惟一索引上的範圍查詢會訪問到不知足條件的第一個值爲止。

案例分析

  • 等值查詢間隙鎖
  • 非惟一索引等值鎖
  • 主鍵索引範圍鎖
  • 非惟一索引範圍鎖
  • 惟一索引範圍鎖
  • 死鎖的例子

附錄

MVCC + Next-Key Lock 沒有辦法解決下面這種例外狀況:

# 事務T1,REPEATABLE READ隔離級別下
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM hero WHERE number = 30;
Empty set (0.01 sec)

# 此時事務T2執行了:INSERT INTO hero VALUES(30, 'g關羽', '魏'); 並提交

mysql> UPDATE hero SET country = '蜀' WHERE number = 30;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM hero WHERE number = 30;
+--------+---------+---------+
| number | name    | country |
+--------+---------+---------+
|     30 | g關羽   | 蜀      |
+--------+---------+---------+
1 row in set (0.01 sec)
複製代碼

在REPEATABLE READ隔離級別下,T1第一次執行普通的SELECT語句時生成了一個ReadView,以後T2向hero表中新插入了一條記錄便提交了,ReadView並不能阻止T1執行UPDATE或者DELETE語句來對改動這個新插入的記錄(由於T2已經提交,改動該記錄並不會形成阻塞),可是這樣一來這條新記錄的trx_id隱藏列就變成了T1的事務id,以後T1中再使用普通的SELECT語句去查詢這條記錄時就能夠看到這條記錄了,也就把這條記錄返回給客戶端了。由於這個特殊現象的存在,你也能夠認爲InnoDB中的MVCC並不能完徹底全的禁止幻讀。

參考:mp.weixin.qq.com/s/wSlNZcQka…

相關文章
相關標籤/搜索