MySQL數據庫——鎖機制

1 認識鎖機制

在認識鎖機制前,首先思考一個問題:在同一時刻,用戶A和用戶B同時要獲取並修改sh_goods表中id等於2的stock庫存量值,此時會發生什麼呢?

假設在初始情況下,sh_ goods表中id等於2的stock庫存量值爲500。

在不添加鎖的前提下,用戶A關閉自動提交,將stock的值修改爲300,然後查詢當前stock值爲300(修改但未提交);與此同時用戶B也獲取stock,它的值卻爲500。當用戶A提交了修改後,用戶B獲取到的值又變爲300。整個操作過程出現了兩個大的問題,一是用戶B第1次查詢stock字段的值與用戶A不同,二是用戶B前後兩次讀取的stock值不同,從而產生了用戶併發操作時數據不一 致的情況。

解決辦法就是,在用戶A和用戶B同時向sh_goods表發出請求操作時,根據系統內部設定的操作優先級(獲取數據優先或修改數據優先的原則),鎖住指定用戶(如A)要操作的資源(sh_goods 表),同時讓另外一個用戶(如B)排隊等候,直到鎖定資源的用戶(如A)操作完成,並釋放鎖後,再讓另一個用戶(如B)對資源進行操作。其中,對資源加鎖的方式,可以採用修改事務隔離級別(前面章節已講)的方式實現。

簡單地說,鎖機制就是爲了保證多用戶併發操作時,能使被操作的數據資源保持一致性的設計規則。又因MySQL數據庫自身設計的特點,利用多種存儲引擎處理不同特定的應用場景,所以鎖機制在不同存儲引擎中的表現也有一-定的區別。

根據存儲引擎的不同,MySQL中常見的鎖有兩種,分別爲表級鎖(如MyISAM、MEMORY存儲引擎)和行級鎖(如InnoDB存儲引擎)。另外InnoDB存儲引擎中還含有表級鎖,具體內容會在後面的小節詳細講解,此處瞭解即可。

表級鎖是MySQL中鎖的作用範圍(鎖的粒度)最大的一種鎖,它鎖定的是用戶操作資源所在的整個數據表,有效地避免了死鎖的發生,且具有加鎖速度快、消耗資源小的特點。

正所謂事物都有兩面性,表級鎖的優勢同樣給它帶來了一定的缺陷,因其鎖定的粒度大,在併發操作時發生鎖衝突的概率也大。

行級鎖是MySQL中鎖的作用範圍最小的一種鎖,它僅鎖定用戶操作所涉及的記錄資源,有效地減少了鎖定資源競爭的發生,具有較高處理併發操作的能力,提升系統的整體性MySQL數據庫原理、設計與應用能。但同時也因其鎖定的粒度過小,每次加鎖和解鎖所消耗的資源也會更多,發生死鎖的可能性更高。

另外,根據鎖在MySQL中的狀態也可將其分爲“隱式”與“顯式”。所謂“隱式”鎖指的是MySQL服務器本身對數據資源的爭用進行管理,它完全由服務器自動執行。而“顯式”鎖指的是用戶根據實際需求,對操作的數據顯式地添加鎖,同樣在使用完數據資源後也需要用戶對其進行解鎖。

小提示:在瞭解死鎖前,首先要理解什麼是鎖等待。所謂鎖等待指的是一個用戶(線程)等待其他用戶(線程)釋放鎖的過程。而死鎖可以簡單地理解爲兩個或多個用戶(線程)在互相等待對方釋放鎖而出現的一種“僵持”狀態,若無外力作用,它們將永遠處於鎖等待的狀態,此時就可以說系統產生了死鎖或處於死鎖狀態。

2 表級鎖

在實際應用中,表級鎖根據操作的不同可以分爲讀鎖和寫鎖。讀鎖表示用戶讀取(如SELECT查詢)數據資源時添加的鎖,此時其他用戶雖然不可以修改或增加數據資源,但是可以讀取該數據資源,因此讀鎖也可以稱爲共享鎖;而寫鎖表示用戶對數據資源執行寫(如INSERT,UPDATE.DELETE等)操作時添加的鎖,此時除了當前添加寫鎖的用戶外,其他用戶都不能對其進行讀/寫操作,因此寫鎖也可以稱爲排他鎖或獨佔鎖。

MyISAM存儲引擎表是MySQL數據庫中最典型的表級鎖,下面就以此存儲引擎的表級鎖爲例詳細講解“隱式”讀/寫的表級鎖和“顯式”讀/寫表級鎖的添加。

1.“隱式”讀/寫的表級鎖

當用戶對MyISAM存儲引擎表執行SELECT查詢操作前,服務器會“自動”地爲其添加一個表級的讀鎖,執行INSERT.UPDATE.DELETE等寫操作前,服務器會“自動”地爲其添加一個表級的寫鎖;直到查詢完畢,服務器再“自動”地爲其解鎖。執行時間可以看作是“隱式”表級鎖讀/寫的生命週期,且該生命週期的持續時間一般都比較短暫。

默認情況下,服務器在“自動”添加“隱式”鎖時,表的更新操作優先級高於表的查詢操作。在添加寫鎖時,若表中沒有任何鎖則添加,否則將其插人到寫鎖等待的隊列中;在添加讀鎖時,若表中沒有寫鎖則添加,否則將其插人到讀鎖等待的隊列中。

2.“顯式”讀/寫的表級鎖

在實際應用中,可以根據開發需求,對要操作的數據表進行“顯式”地添加表級鎖。其基本語法格式如下。

LOCK TABLES 數據表名 READ [LOCAL]| WRITE,

在上述語法中,LOCKTABLES可以同時鎖定多張數據表。READ表示表級的讀鎖,添加此鎖的用戶可以讀取該表但不能對此表進行寫操作,否則系統會報錯;此時其他用戶可以讀取此表,若執行對此表的寫操作則會進入等待隊列。WRITE 表示表級的寫鎖,添加此鎖的用戶可以對該表進行讀/寫操作,在釋放鎖之前,不允許其他用戶訪問與操作。

需要注意的是,在爲MyISAM存儲引擎表設置“顯式”讀鎖時,若添加LOCAL關鍵字,則在不發生鎖衝突的情況下,未添加此鎖的其他用戶可以在表的末尾實現併發插人數據的功能。

此外,對於表級鎖來說,雖然鎖本身消耗的資源很少,但是鎖定的粒度卻很大,當多個用戶訪問時,會造成鎖資源的競爭,降低了併發處理的能力。因此,從數據庫優化的角度來考慮,應該儘量減少表級鎖定的時間,進而提高多用戶的併發能力。此時,對於用戶添加的“顯式”表級鎖,需要使用MySQL提供的UNLOCKTABLES語句釋放鎖。

值得一提的是,用戶設置的“顯式”表級鎖僅在當前會話內有效,若會話期間內未釋放鎖,在會話結束後也會自動釋放。

爲了讀者更好地理解,下面通過一個具體的案例進行演示。具體步驟如下。
(1)創建MyISAM表並插人2條測試數據。

mysql> CREATE TABLE mydb.table_lock(id int)ENGINE=MyISAM;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO mydb.table_lock VALUES(1),(2);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

(2)設置“顯式”讀的表級鎖。
打開兩個客戶端A和B,在客戶端A中爲mydb.table_lock設置“顯式”讀的表級鎖後,然後分別在客戶端A和客戶端B中執行SELECT和UPDATE操作。具體SQL語句及執行結果如下。

# ① 在客戶端A中添加表級讀鎖
mysql> LOCK TABLE mydb.table_lock READ;
Query OK, 0 rows affected (0.00 sec)
# ② 在客戶端A中執行SELECT和UPDATE操作
mysql> SELECT * FROM mydb.table_lock \G
*************************** 1. row ***************************
id: 1
*************************** 2. row ***************************
id: 2
2 rows in set (0.00 sec)
mysql> UPDATE mydb.table_lock SET id = 3 WHERE id = 1;
ERROR 1099 (HY000): Table 'table_lock' was locked with a READ lock and can't be updated
mysql> SELECT * FROM mydb.mt \G
ERROR 1100 (HY000): Table 'mt' was not locked with LOCK TABLES
# ③ 在客戶端B中執行SELECT和UPDATE操作
mysql> SELECT * FROM mydb.table_lock \G
*************************** 1. row ***************************
id: 1
*************************** 2. row ***************************
id: 2
2 rows in set (0.00 sec)
mysql> UPDATE mydb.table_lock SET id = 3 WHERE id = 1;
# 此處光標會不停閃爍,進入鎖等待狀態
# ④ 在客戶端A中釋放鎖
mysql> UNLOCK TABLES;
Query OK, 0 rows affected (0.00 sec)
# ⑤ 客戶端B在客戶端A釋放鎖後,會立即執行③中等待的寫鎖
mysql> UPDATE mydb.table_lock SET id=3 WHERE id=1;
Query OK, 1 row affected (5.64 sec)
Rows matched: 1  Changed: 1  Warnings: 0

從以上的操作可以看出,添加表級讀鎖的客戶端A僅能對mydb.table_lock執行讀取操作,不能執行寫操作,也不能操作其他未鎖定的數據表,如mydb.mt。對於未添加鎖的客戶端B則可以執行SELECT操作,但是執行UPDATE操作則會進入鎖等待狀態,只有客戶端A結束會話或執行UNLOCK TABLES釋放鎖時,客戶端B的操作纔會被執行。具體SQL語句及執行結果如下。

# ① 在客戶端A中添加表級讀鎖
mysql> LOCK TABLE mydb.table_lock READ LOCAL;
Query OK, 0 rows affected (0.00 sec)
# ② 在客戶端B中,插入一條記錄
mysql> INSERT INTO mydb.table_lock VALUES(4);
Query OK, 1 row affected (0.00 sec)

(3)併發插人操作。
在MyISAM存儲引擎的數據表中,還支持併發插入操作,用於減少讀操作與寫操作對錶的競爭情況。實現語法爲LOCK… READ LOCAL,具體SQL語句及執行結果如下。

# ① 在客戶端A中添加表級讀鎖
mysql> LOCK TABLE mydb.table_lock READ LOCAL;
Query OK, 0 rows affected (0.00 sec)
# ② 在客戶端B中,插入一條記錄
mysql> INSERT INTO mydb.table_lock VALUES(4);
Query OK, 1 row affected (0.00 sec)

從上述執行結果可知,即使客戶端A中已添加了表級讀鎖,在未釋放此讀鎖時,在客戶端B中依然可以實現數據插人操作,此操作也稱爲併發插人。
需要注意的是,併發插人的數據不能是DELETE操作刪除的記錄,並且只能在表中最後的一行記錄後繼續增加新記錄。
(4)設置“顯式”寫的表級鎖。

# ① 在客戶端A中添加表級寫鎖
mysql> LOCK TABLE mydb.table_lock WRITE;
Query OK, 0 rows affected (0.00 sec)
# ② 在客戶端A中執行更新和查詢操作
mysql> UPDATE mydb.table_lock SET id = 1 WHERE id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> SELECT * FROM mydb.table_lock \G
*************************** 1. row ***************************
id: 3
*************************** 2. row ***************************
id: 1
*************************** 3. row ***************************
id: 4
3 rows in set (0.00 sec)
# ③ 在客戶端B中執行查詢操作
mysql> SELECT * FROM mydb.table_lock;
# 此處光標會不停閃爍,進入鎖等待狀態

從上述操作可以看出,添加了寫鎖的用戶,可以執行讀/寫操作(如增刪改查),而其他用戶不論執行任何操作(如SELECT),都只能處於等待狀態,直到寫鎖被釋放,才能夠執行。

3 行級鎖

InnoDB存儲引擎的鎖機制相對於MyISAM存儲引擎的鎖複雜一些,原因在於它既有表級鎖又有行級鎖。其中,InnoDB表級鎖的應用與MyISAM表級鎖的相同,這裏不再贅述。那麼InnoDB存儲引擎的表什麼時候添加表級鎖,什麼時候添加行級鎖呢?只有通過索引條件檢索的數據InnoDB存儲引擎纔會使用行級鎖,否則將使用表級鎖。

InnoDB的行級鎖根據操作的不同也分爲共享鎖和排他鎖。爲了讀者更好地理解,下面以“隱式”的行級鎖和“顯式”的行級鎖爲例進行詳細講解。

1.“隱式”行級鎖
當用戶對InnoDB存儲引擎表執行INSERT.UPDATE.DELETE等寫操作前,服務器會“自動”地爲通過索引條件檢索的記錄添加行級排他鎖;直到操作語句執行完畢,服務器再“自動”地爲其解鎖。

而語句的執行時間可以看作是“隱式”行級鎖的生命週期,且該生命週期的持續時間一般都比較短暫。通常情況下,若要增加行級鎖的生命週期,最常使用的方式是事務處理,讓其在事務提交或回滾後再釋放行級鎖,使行級鎖的生命週期與事務的相同。

爲了讀者更好地理解,下面在事務中演示“隱式”行級鎖的使用。具體步驟如下。

(1)創建InnoDB表並插人測試數據。

mysql> CREATE TABLE mydb.row_lock (
    ->   id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    ->   name VARCHAR(60) NOT NULL,
    ->   cid INT UNSIGNED,
    ->   KEY cid (cid)
    -> )DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO mydb.row_lock (name, cid) VALUES ('鉛筆', 3),
    -> ('風扇', 6), ('綠蘿', 1), ('書包', 9), ('紙巾', 20);
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

(2)設置“隱式”行級鎖。
打開兩個客戶端A和B,在客戶端A中爲mydb.row_lock設置“隱式”行級的排他鎖後,然後在客戶端B中執行SELECT和DELETE操作。具體SQL語句及執行結果如下。

# ① 在客戶端A中,修改cid等於3的name值
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE mydb.row_lock SET name = 'cc' WHERE cid = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# ② 在客戶端B中,刪除cid等於2和3的記錄
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM mydb.row_lock WHERE cid = 2;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM mydb.row_lock WHERE cid = 3;
# 此處光標會不停閃爍,進入鎖等待狀態
# ③ 在客戶端A和B中,回滾以上的操作
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

從以上執行結果可知,一個客戶端對InnoDB表執行UPDATE操作時,對符合索引條件的記錄會隱式地添加一個行級鎖,與此同時其他用戶不能再執行寫操作,但可以操作不符合索引條件的記錄(如刪除cid等於2的記錄)。

2.“顯式"行級鎖

對於InnoDB表來說,若要保證當前事務中查詢出的數據不會被其他事務更新或刪除,利用普通的SELECT語句是無法辦到的,此時需要利用MySQL提供的“鎖定讀取”的方式爲查詢操作顯式地添加行級鎖。其基本語法格式如下。

SELECT 語句 FOR UPDATE|LOCK IN SHARE MODE

在上述語法中,只需在正常的SELECT語句後添加FOR UPDATE或LOCK IN SHARE MODE即可實現“鎖定讀取”,前者表示在查詢時添加行級排他鎖,後者表示在查詢時添加行級共享鎖。

用戶在向InnoDB表顯式添加行級鎖時,InnoDB存儲引擎首先會“自動”地向此表添加一個意向鎖,然後再添加行級鎖。此意向鎖是一個隱式的表級鎖,多個意向鎖之間不會產生衝突且互相兼容。意向鎖是由MySQL服務器根據行級鎖是共享鎖還是排他鎖,自動添加意向共享鎖或意向排他鎖,不能人爲干預。

意向鎖的作用就是標識表中的某些記錄正在被鎖定或其他用戶將要鎖定表中的某些記錄。相對行級鎖,意向鎖的鎖定粒度更大,用於在行級鎖中添加表級鎖時判斷它們之間是否能夠互相兼容。好處就是大大節約了存儲引擎對鎖處理的性能,更加方便地解決了行級鎖與表級鎖之間的衝突。

爲了讀者更好地理解,下面通過一個表格展示表級的共享/排他鎖與意向共享/排他鎖之間的兼容性關係,具體如表所示。

表 表級共享/排他鎖與意 向共享/排他鎖之間的關係

表級共享鎖 表級排他鎖 意向共享鎖 意向排他鎖
表級共享鎖 兼容 衝突 兼容 衝突
表級排他鎖 衝突 衝突 衝突 衝突
意向共享鎖 兼容 衝突 兼容 兼容
意向排他鎖 衝突 衝突 兼容 兼容

需要注意的是,InnoDB表中當前用戶的意向鎖若與其他用戶要添加的表級鎖衝突時,有可能會發生死鎖而產生錯誤。
接下來利用上面創建的mydb.row_lock表,演示添加行級排他鎖時客戶端A和客戶端B執行SQL語句的狀態,具體步驟如下。

# ① 在客戶端A中,爲cid等於3的記錄添加行級排他鎖
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM mydb.row_lock WHERE cid = 3 FOR UPDATE;
+----+------+------+
| id | name | cid  |
+----+------+------+
|  1 | 鉛筆  |    3 |
+----+------+------+
1 row in set (0.00 sec)
# ② 在客戶端B中,爲cid等於2的記錄添加隱式行級排他鎖,設置表級排他鎖
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE mydb.row_lock SET name = 'lili' WHERE cid = 2;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
mysql> LOCK TABLE mydb.row_lock READ;
# 此處光標會不停閃爍,進入鎖等待狀態
# ③ 回滾以上的操作並釋放表級鎖

從以上的執行結果可知,在客戶端A中爲cid等於3的記錄添加行級排他鎖後,在客戶端B中,可以爲除cid等於3外的記錄添加行級排他鎖(如cid等於2的隱式排他鎖),但是在爲表添加表級共享鎖時會發生衝突,進行鎖等待狀態。

此外,默認的情況下,當InnoDB處於REPEATABLE READ(可重複讀)的隔離級別時,行級鎖實際上是一個next-key鎖,它是由間隙鎖(gaplock)和記錄鎖(recordlock)組成的。其中,記錄鎖(recordlock)就是前面講解的行鎖;間隙鎖指的是在記錄索引之間的間隙、負無窮到第1個索引記錄之間或最後1個索引記錄到正無窮之間添加的鎖,它的作用就是在併發時防止其他事務在間隙插入記錄,解決了事務幻讀的問題。

爲了讀者更好地理解,下面爲mydb.row_lock表添加行鎖,查看間隙鎖是否存在。具體步驟如下。

# ① 在客戶端A中,爲cid等於3的記錄添加行鎖
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM mydb.row_lock WHERE cid=3 FOR UPDATE;
+----+------+------+
| id | name | cid  |
+----+------+------+
|  1 | 鉛筆  |    3 |
+----+------+------+
1 row in set (0.00 sec)
# ② 在客戶端B中,插入cid等於1、2、5、6的記錄
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('電視', 1);
# 此處光標會不停閃爍,進入鎖等待狀態
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('電視', 2);
# 此處光標會不停閃爍,進入鎖等待狀態
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('電視', 5);
# 此處光標會不停閃爍,進入鎖等待狀態
mysql> INSERT INTO mydb.row_lock(name, cid) VALUES('電視', 6);
Query OK, 1 row affected (0.00 sec)

在上述操作中,客戶端A在cid等於3的記錄中添加了行鎖,理論上其他用戶在併發時可以插入除cid等於3的任意記錄,但是因爲間隙鎖的存在,服務器也會鎖定當前表中cid(值分別爲1、3、6、9、20)值爲3的記錄左右的間隙,間隙的區間範圍爲[1 ,3)和[3,6)。

值得一提的是,在執行SELECT-FOR UPDATE時,若檢索時未使用索引,則InnoDB存儲引擎會給全表添加一個表級鎖,併發時不允許其他用戶進行插人。另外,若查詢條件使用的是單字段的唯一性索引,InnoDB存儲引擎的行級鎖不會設置間隙鎖。

間隙鎖的使用雖然解決了事務幻讀的情況,但是也會造成行鎖定的範圍變大,若在開發時想要禁止間隙鎖的使用,可以將事務的隔離級別更改爲READ COMMITTED(讀取提交)。

多學一招:查看InnoDB表的鎖
InnoDB存儲引擎的鎖比較複雜,讀者可以在添加一個行鎖後,使用SHOW ENGINE INNODB
STATUS語句查看當前表中添加的鎖的類型。另外,在查看時要保證開啓系統變量innodb_status_output_locks
才能獲取鎖定的信息。例如,查看mydb.row_lock 表添加的鎖,部分信息如下。

 TABLE LOCK table `mydb`.`row_lock` trx id 10386 lock mode IX RECORD LOCKS space id 247 page no 4 n bits 80 index cid of table `mydb`.`row_lock` trx id 10386 lock_mode X 
 ︙(此處省略部分內容)
  RECORD LOCKS space id 247 page no 3 n bits 80 index PRIMARY of table `mydb`.`row_lock` trx id 10386 lock_mode X locks rec but not gap
 ︙(此處省略部分內容)  
 RECORD LOCKS space id 247 page no 4 n bits 80 index cid of table ``mydb`.`row_lock` trx id 10386 lock_mode X locks gap before rec
 ︙(此處省略部分內容) 

在上述信息中,“IX”表示mydb. row_ lock 中添加了一個意向排他鎖,“X”表示next-key lock的排他鎖,“X
locks rec but not gap”表示記錄鎖,“X locks gap before rec”表示間隙鎖。
它們之間的關係爲“IX”在“X"之前添加,而“X”是由“X locks rec but not gap" 和“X locks gap
before rec”組成的。


超全面的測試IT技術課程,0元立即加入學習!有需要的朋友戳:


騰訊課堂測試技術學習地址

歡迎轉載,但未經作者同意請保留此段聲明,並在文章頁面明顯位置給出原文鏈接。

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