本文首發於個人微信公衆號《andyqian》, 期待你的關注 ~
前言
在數據庫中,通常通過鎖來解決併發下數據一致性問題,從而避免數據產生髒亂。在保證數據一致性問題的前提下,通過鎖範圍又分爲不同種類,在 MySQL 中,存儲引擎就支持不同類型鎖。如: MyISAM 只支持表鎖。InnoDB 支持:行鎖,表鎖,Gap 鎖等等。今天就來聊聊 MySQL InnoDB 的 " 鎖事" 。
鎖類型
既然提到了鎖,鎖類型是逃不開的話題,在 InnoDB 中,行鎖分爲共享鎖(Share) 和 排他 (Exclusive) 鎖。其詳細概念如下:
- 共享(Share)鎖 :簡稱 (S) 鎖, 持有該鎖的事務允許讀取行數據。
- 排他(Exclusive)鎖:簡稱 (X)鎖 持有該鎖的事務允許 update 或 delete 行數據。
備註:
- 此處的行鎖不能簡單的理解爲鎖定一行數據,而是 行數據。行數據包括:單行,多行,理解這區別,對後面理解(Record 鎖,Gap 鎖)會有莫大的幫助。
兼容關係:
S 鎖X 鎖S 鎖兼容不兼容X 鎖不兼容不兼容
文字解析:
當事務 T1 在行 t 上持有 S (共享) 鎖時,事務 T2 對行 t 上 請求持有鎖的結果如下:
- 當事務 T2 請求獲取 S (共享) 鎖時,將立即授予,此時 事務 T1,T2 同時持有 S (共享) 鎖。
- 當事務 T2 請求獲取 X (排他) 鎖時,此時T2 將會處於鎖等待狀態,等待事務T1 釋放 S (共享)鎖後再進行獲取,如果T2 鎖
例子: 事務1持有S 鎖, 事務2 請求持有S 鎖。
- 事務1
begin;
mysql> select * from t_base_info where oid = 1 lock in share mode;
+-----+----------+---------------------+---------------------+
| oid | name | create_time | updated_time |
+-----+----------+---------------------+---------------------+
| 1 | andyqian | 2020-03-21 14:34:08 | 2020-03-21 14:34:08 |
+-----+----------+---------------------+---------------------+
1 row in set (0.00 sec)
- 事務2
begin;
mysql> select * from t_base_info where oid = 1 lock in share mode;
+-----+----------+---------------------+---------------------+
| oid | name | create_time | updated_time |
+-----+----------+---------------------+---------------------+
| 1 | andyqian | 2020-03-21 14:34:08 | 2020-03-21 14:34:08 |
+-----+----------+---------------------+---------------------+
1 row in set (0.00 sec)
例子: 事務1持有S 鎖, 事務2 請求持有X 鎖。
- 事務1
begin;
mysql> select * from t_base_info where oid = 1 lock in share mode;
+-----+----------+---------------------+---------------------+
| oid | name | create_time | updated_time |
+-----+----------+---------------------+---------------------+
| 1 | andyqian | 2020-03-21 14:34:08 | 2020-03-21 14:34:08 |
+-----+----------+---------------------+---------------------+
1 row in set (0.00 sec)
- 事務2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_base_info where oid = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
以上例子也佐證了上面的理論。
常見鎖
在 InnoDB 存儲引擎, REPEATABLE READ (可重複讀) 隔離級別下,爲我們提供了多種鎖,接下來我們一探究竟。(下述所有例子:在 Innodb,REPEATABLE READ (可重複讀)隔離級別 運行,請確保一致)
1. 表鎖
鎖定範圍:整表。
特點:獲取鎖效率高,有效避免死鎖 (破壞了死鎖的競爭條件)。但嚴重影響性能,併發性低,在生產系統上,表鎖簡直屬於災難。
事務1
begin;
lock table t_base_info read;
事務2:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t_base_info(name,create_time,updated_time)values("name",now(),now());
結果:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
備註:
- 表鎖的超時時間不受:innodb_lock_wait_timeout 限制,而是受:lock_wait_timeout 限制。
mysql> show variables like "lock_wait_timeout";
+-------------------+----------+
| Variable_name | Value |
+-------------------+----------+
| lock_wait_timeout | 31536000 |
+-------------------+----------+
1 row in set (0.00 sec)
單位爲秒,默認爲31536000 秒,365 天。
可通過下述命令進行修改:
set global lock_wait_timeout = 10;
2. Record 鎖
鎖定範圍:索引記錄,如果表中沒有索引記錄,則會自動創建一個隱藏的聚簇索引。
特點:只鎖定單條索引記錄,獲取鎖效率稍低,可能會產生死鎖, 但併發高,性能好。
適用隔離級別: REPEATABLE READ (可重複讀),READ COMMITTED (提交讀),READ UNCOMMITTED (未提交讀)。
3. Gap 鎖
鎖定範圍:顧名思義,鎖定的是一個範圍。
特點:Gap 鎖: 鎖定一個範圍,鎖定後該範圍不支持新增,其他事物在該範圍中insert,delete 需要lock wait 直至釋放。
適用隔離級別: REPEATABLE READ (可重複讀)。
備註:
- 在 READ UNCOMMITTED (未提交讀),READ COMMITTED (提交讀) 隔離級別下無效。
例子:
事務1
begin;
select * from t_base_info where oid < 8 for update;
事務2:
begin;
mysql> insert into t_base_info(oid,name,create_time,updated_time)
values(4,"name4",now(),now());
結果:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
4. Next-Key 鎖
鎖定範圍:顧名思義,鎖定的是一個範圍。
特點:Next-Lock:Recod 鎖 + Gap 鎖的集合。其中鎖定的範圍是:當前記錄和範圍。
適用隔離級別:REPEATABLE READ (可重複讀)
注意事項:
- 該鎖在 READ UNCOMMITTED (未提交讀),READ COMMITTED (提交讀) 隔離級別下無效。
事務1
begin;
select * from t_base_info where oid < 8 for update;
事務2:
begin;
mysql> update t_base_info set name = "andyqian008" where oid = 8;
結果:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
鎖超時
在高併發下,熱點數據的鎖競爭是非常常見的。在 MySQL 中採用超時機制,當獲取鎖時間超過 innodb_lock_wait_timeout 後,會提示獲取鎖失敗。如下所示:
- 鎖獲取超時後,提示信息:
> 1205 - Lock wait timeout exceeded; try restarting transaction
> Time: 30.89s
- 查看鎖超時時間, (默認爲 50 秒)。
show variables like "innodb_lock_wait_timeout";
- 修改全局鎖超時時間。
set global innodb_lock_wait_timeout = 30;
- 修改當前會話鎖超時時間
set session innodb_lock_wait_timeout = 20;
備註:
- 修改 global 超時時,當前 session 超時時間不變,其他 session 改變。
- 修改 session 超時時,當前 session 超時時間改變,其他 session 不改變。
- 在 Navicat 等客戶端工具中,一個 Query 爲一個 session 。
小結
上述是我對 MySQL InnoDB 鎖以及常見鎖的理解,如有誤之處,還請多多指出。談到這裏,我覺得這和 Java 中 JUC 包提供的職責是相同的,其設計思路也有相通之處。此時有人要問了:Java 中不是還有CAS 無鎖方案嗎?數據庫中有類似的概念及實現嗎?你還別不信, 在 InnoDB 存儲引擎中的 MVCC 與 Java 中 CAS 的就像極了,我們下篇就來詳細講講。
相關閱讀:
《說說單元測試》
《一個Java細節》