概述
鎖是計算機協調多個進程或純線程并發訪問某一資源的機制,這些資源包括CPU、內存、I/O等,而在數據庫中,數據也是一種供許多用戶(進程/線程)共享的資源。如何保證數據并發訪問的一致性、有效性是所在有數據庫必須解決的一個問題,數據庫的鎖便是解決這個問題的重要工具。數據庫的事務具有4個屬性,即ACID,其中C即是一致性,所以在數據庫中,鎖和事務總是息息相關的。
MySQL中的鎖
在MySQL中,不同的存儲引擎,可能支持的鎖會有所不同。下面的內容主要圍繞使用比較多的InnoDB和MyISAM進行描述。
MySQL中的鎖,從粒度即范圍劃分為3類:全局鎖、表級鎖、行級鎖。實際上,其它的如BDB存儲引擎還有頁級鎖,這里不做介紹。
- 全局鎖
鎖住整個數據庫實例,使得整個庫處于只讀狀態,會阻塞DML和DDL語句,由MySQL的SQL層實現。可以使用以下命令為數據庫加全局鎖(簡稱FTWRL)
FLUSH TABLES WITH READ LOCK;
釋放全局鎖命令如下
UNLOCK TABLES;
此外,在客戶端異常斷開后,全局鎖會自動釋放。全局鎖的典型使用場景是,做全庫邏輯備份(mysqldump)。
- 表級鎖
針對當前操作的整張表加鎖,實現簡單,消耗資源較少,InnoDB和MyISAM均支持表級鎖。表級鎖分為表鎖和MySQL5.5版本引入的元數據鎖(MDL)兩種。MDL不需要顯式使用,在訪問或操作一個表的時候會被自動加上。當對一個表做增刪改查操作的時候,加MDL讀鎖;當要對表做結構變更操作的時候,加MDL寫鎖。 MDL可以避免讀寫數據時突然變更了表結構等元數據而導致出現問題。
相關表級鎖的一些命令
-- 手動增加表鎖(READ為共享讀鎖,WRITE為獨占寫鎖)
LOCK TABLE 表名 READ | WRITE;
-- 查看表鎖情況
SHOW OPEN TABLES;
-- 刪除表鎖
UNLOCK TABLES;
表級鎖從資源管理角度又分為表共享讀鎖(共享鎖)和表獨占寫鎖(排他鎖)兩種形式。如果對數據表加了共享讀鎖,當前會話只能讀取加鎖的表數據,同時不能增刪改(報錯),也不能讀取其它表數據;其它的會話則可以讀取加鎖的表數據,增刪改將被阻塞,但可以操作其它表數據。
表共享讀鎖的演示
打開一個客戶端會話1,對tdep表加共享讀鎖,會話1可以讀取tdep表的數據,但不能進行增刪改,同時也不能操作其它表。
會話1對tdep表加共享讀鎖的表現
此時打開另外一個客戶端會話2,可以讀取tdep表數據,對tdep表進行增刪改將被阻塞,但可以操作其它數據表。
其它會話表現
表獨占寫鎖演示
打開一個客戶端會話1,對tdep表加獨占寫鎖,會話1可以對tdep表進行增刪改查,但依舊不可以操作其它表。
會話1對tdep表加獨占寫鎖的表現
打開另外一個客戶端會話2,會話可以操作其它數據表,但對tdep任何的操作包括select查詢都會被阻塞。
其它會話的表現
查看表級鎖定的爭用狀態
mysql> SHOW STATUS LIKE 'table_locks%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 99 |
| Table_locks_waited | 0 |
+-----------------------+-------+
其中table_locks_immediate表示產生表級鎖定的次數;table_locks_waited表示出現表級鎖定爭用而發生等待的次數。
- 行級鎖
針對某行數據加鎖(也可能鎖定行之間的間隙),是一種排他鎖,防止其它事務修改此行。部分存儲引擎支持,比如InnoDB,下面主要講的就是InnoDB的行級鎖。
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,因此InnoDB這種行鎖實現特點意味著:只有通過
索引條件檢索的數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖(自動使用表鎖,不需要人員干預)!
InnoDB的行級鎖,按照鎖定范圍來說,分為三種:
記錄鎖(Record Locks),鎖定索引中一條記錄;間隙鎖(Gap Locks),要么鎖住索引記錄中間的值,要么鎖住第一個索引記錄前面的值或者最后一個索引記錄后面的值,其用于防止插入或更新間隙內的數據;Next-Key鎖,是索引記錄上的記錄鎖和在索引記錄之前的間隙鎖的組合。
按照功能來說,分為兩種:
共享鎖(S),允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖;排他鎖(X),允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖。
對于INSERT、UPDATE、DELETE語句,InnoDB會自動給涉及數據集加排他鎖(X);對于普通SELECT語句,InnoDB不會加任何鎖,事務可以通過以下語句顯示給記錄集加共享鎖或排他鎖。
-- 使用LOCK IN SHARE MODE手動添加共享鎖(S)
SELECT * FROM table_name WHERE condition LOCK IN SHARE MODE;
-- 使用FOR UPDATE手動添加排他鎖(x)
SELECT * FROM table_name WHERE condition FOR UPDATE;
使用COMMIT或ROLLBACK語句釋放鎖。
查看行級鎖爭用狀態
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 0 |
| Innodb_row_lock_time_avg | 0 |
| Innodb_row_lock_time_max | 0 |
| Innodb_row_lock_waits | 0 |
+-------------------------------+-------+
其中第2、3、5項是優化或分析問題的比較常用指標
Innodb_row_lock_current_waits:當前正在等待鎖定的數量;
Innodb_row_lock_time:從系統啟動到現在鎖定總時間長度;
Innodb_row_lock_time_avg:每次等待所花平均時間;
Innodb_row_lock_time_max:從系統啟動到現在等待最常的一次所花的時間;
Innodb_row_lock_waits:系統啟動后到現在總共等待的次數;
行鎖基本演示
使用2個會話分別連接MySQL服務器,首先使用會話1更新tdep表中id為3的記錄,其中id為主鍵,有索引,所以會自動為該記錄加上排他鎖(X),示例中關閉了自動提交,以驗證行鎖的效果。
會話1的表現
此時會話2可以更新其它記錄,也可以讀取id為3的記錄,但更新id為3的記錄將被阻塞
其它會話的表現
行讀鎖(共享鎖)演示
使用會話1為tdep表中id為3的記錄加上共享鎖,基本上可以做任何的操作
會話1加行讀鎖的表現
會話2可以訪問其它表的數據以及未鎖定的其它行,id為3的行可以讀取但不能更新
其它會話的表現
行寫鎖(排他鎖)演示
會話1對tdep表中id為3的記錄加了排他鎖,基本上可以做任何的操作
會話1加行寫鎖的表現
會話2可以訪問tdep表,但不能該記錄進行更新或刪除,同時也不能對該記錄進行加共享鎖或排他鎖。
其它會話的表現
- 表級鎖和行級鎖的比較
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,并發度最低;
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高;
總結
關于InnoDB的行級鎖,這里主要介紹了記錄鎖。InnoDB行級鎖是通過給索引實現的,只有通過索引條件檢索的數據,InnoDB才使用行級鎖,否則,InnoDB將自動使用表鎖。
對于INSERT、UPDATE、DELETE語句,InnoDB會自動給涉及數據記錄加排他鎖(X),這意味著其它會話不能再對這些記錄進行INSERT、UPDATE、DELETE操作以及使用LOCK IN SHARE MODE或FOR UPDATE對涉及的數據記錄進行加讀鎖或寫鎖,非要操作將被阻塞;
對于普通SELECT語句,InnoDB不會加任何鎖,但可以通過LOCK IN SHARE MODE或FOR UPDATE語句顯示給記錄集加共享鎖或排他鎖。
一條記錄被加了排他鎖之后,其它會話不能再對該記錄加鎖,包括共享鎖和排他鎖。但如果一條記錄加的是共享鎖,則其它會話可以對該記錄加共享鎖,但不能加排他鎖。即一條記錄可以同時被多個會話加共享鎖,但只能被一個會話加排他鎖。
不同于表的獨占寫鎖,行的排他鎖可以允許其它會話讀取行記錄,而表的獨占寫鎖則不允許其它會話讀取表中的記錄。即行級鎖作用是限制了其它會話對加鎖的記錄進行增刪改,但不限制讀取。