有一個多門店點餐系統,點餐就是個下訂單的過程,數據庫設計上是一個訂單表存儲了全部店鋪的訂單信息,則有一個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);
複製代碼
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,而當中的每一次遍歷都會同時鎖上主鍵索引。優化
衆所周知,使用了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 解決了髒讀、重複讀、幻讀問題。
MVCC的特性比較好理解和分析,這裏重點分析RR下的MySQL的加鎖過程。 首先,這裏總結了幾條MySQL的加鎖和優化原則:
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並不能完徹底全的禁止幻讀。