MySQL InnoDB 鎖事

本文首發於個人微信公衆號《andyqian》,  期待你的關注  ~

前言

在數據庫中,通常通過鎖來解決併發下數據一致性問題,從而避免數據產生髒亂。在保證數據一致性問題的前提下,通過鎖範圍又分爲不同種類,在 MySQL 中,存儲引擎就支持不同類型鎖。如: MyISAM 只支持表鎖。InnoDB 支持:行鎖,表鎖,Gap 鎖等等。今天就來聊聊 MySQL InnoDB 的 " 鎖事" 。

鎖類型

既然提到了鎖,鎖類型是逃不開的話題,在 InnoDB 中,行鎖分爲共享鎖(Share) 和 排他 (Exclusive) 鎖。其詳細概念如下:

  1. 共享(Share)鎖 :簡稱 (S) 鎖, 持有該鎖的事務允許讀取行數據。
  2. 排他(Exclusive)鎖:簡稱 (X)鎖 持有該鎖的事務允許 update 或 delete 行數據。

備註:

  1. 此處的行鎖不能簡單的理解爲鎖定一行數據,而是 行數據。行數據包括:單行,多行,理解這區別,對後面理解(Record 鎖,Gap 鎖)會有莫大的幫助。

兼容關係

S 鎖X 鎖S 鎖兼容不兼容X 鎖不兼容不兼容

文字解析

當事務 T1 在行 t 上持有 S (共享) 鎖時,事務 T2 對行 t 上 請求持有鎖的結果如下:

  1. 當事務 T2 請求獲取 S (共享) 鎖時,將立即授予,此時 事務 T1,T2 同時持有 S (共享) 鎖。
  2. 當事務 T2 請求獲取 X (排他) 鎖時,此時T2 將會處於鎖等待狀態,等待事務T1 釋放 S (共享)鎖後再進行獲取,如果T2 鎖

例子: 事務1持有S 鎖, 事務2 請求持有S 鎖。

  1. 事務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)
  1. 事務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. 事務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)
  1. 事務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

備註

  1. 表鎖的超時時間不受: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 (可重複讀)。

備註

  1. 在 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 (可重複讀)

注意事項

  1. 該鎖在 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 後,會提示獲取鎖失敗。如下所示:

  1. 鎖獲取超時後,提示信息:
> 1205 - Lock wait timeout exceeded; try restarting transaction
> Time: 30.89s
  1. 查看鎖超時時間, (默認爲 50 秒)。
show variables like "innodb_lock_wait_timeout";
  1. 修改全局鎖超時時間。
set global innodb_lock_wait_timeout = 30;
  1. 修改當前會話鎖超時時間
set session innodb_lock_wait_timeout = 20;

備註

  1. 修改 global 超時時,當前 session 超時時間不變,其他 session 改變。
  2. 修改 session 超時時,當前 session 超時時間改變,其他 session 不改變。
  3. 在 Navicat 等客戶端工具中,一個 Query 爲一個 session 。

小結

上述是我對 MySQL InnoDB 鎖以及常見鎖的理解,如有誤之處,還請多多指出。談到這裏,我覺得這和 Java 中 JUC 包提供的職責是相同的,其設計思路也有相通之處。此時有人要問了:Java 中不是還有CAS 無鎖方案嗎?數據庫中有類似的概念及實現嗎?你還別不信, 在 InnoDB 存儲引擎中的 MVCC 與 Java 中 CAS 的就像極了,我們下篇就來詳細講講。


相關閱讀:

你該瞭解的Java註釋

說說單元測試

一個Java細節

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章