【MySQL】談談鎖的類型

前言

MySQL中的鎖分爲表鎖以及行鎖,從字面意思就可以得知,表鎖是對一整張表進行加鎖,而行鎖是針對於特定的行。在Server層面,提供了表鎖的實現,而行鎖則由存儲引擎實現。Innodb引擎支持行鎖,Myisam則不支持行鎖。

下面從鎖模式以及加鎖方法來大致闡述Mysql中的鎖。


鎖模式

鎖模式分爲讀鎖、寫鎖、意向鎖

讀鎖

讀鎖,也稱共享鎖(Share Lock),可以簡稱爲S鎖

某個事務對某行或某表加了讀鎖後,其他事務依然可以讀取,但不能修改。

可以同時有多個事務對某行或某表加讀鎖。


寫鎖

寫鎖,也稱獨佔鎖(Exclusive Lock)、排它鎖等,可以簡稱爲X鎖。

某個事務對某行或某表加了寫鎖後,其他事務不可以讀取或更改記錄。

同一時刻,只能有一個事務對對某行或某表加寫鎖。


意向鎖

這裏的意向鎖也分爲讀意向鎖(Intention Share Lock,簡稱IS鎖)以及寫意向鎖(Intention Exclusive Lock,簡稱IX鎖)。

當事務1對錶中的某條記錄加X鎖後,事務2想對整張表加X鎖,於是事務2需要遍歷該表中的所有記錄,判斷是否有記錄存在X鎖。如果有一條記錄被加了X鎖,則事務2需要等待事務1完成。

這種遍歷的方式非常低效,Mysql在後來引入了意向鎖的概念,用來解決這種問題。當事務1對某條記錄加X鎖前,首先需要對錶加IX鎖。當事務2需要對錶加X鎖時,只需要判斷表上是否含有IX鎖,如果有,則進行等待。

當然,意向鎖不會和行鎖衝突,意向鎖只會阻塞對錶的S鎖或X鎖。事務1對錶加IX鎖,然後對記錄a加X鎖,事務2需要修改記錄b時,並不需要判斷是否存在意向鎖。


加鎖方法

記錄鎖

記錄鎖,也稱爲Record Lock,是行鎖中最簡單的實現。比如,對於表student,id爲主鍵,name爲普通索引,age爲普通字段

update student set name="tom" where id=1;

則會鎖住主鍵索引上id爲1的索引記錄,記住,鎖的是索引項,並不是真正的數據行。

update student set age=18 where name='張三';

則首先會在name=張三的二級索引加鎖,然後拿到主鍵id=11後,還會在id=11的主鍵索引上加鎖。不清楚聚集索引與非聚集索引的同學,可以參考我的這一篇文章淺析Innodb的聚集索引與非聚集索引

如果是這樣子的語句呢?

update student set name="jack" where age=22;

很明顯,沒有用到索引列,則Mysql會進行全表掃描,客戶端發送更新語句到server後,server首先通知存儲引擎取出第一條數據,並對其加上X鎖,若滿足age=20,則更新name爲張三,最後釋放鎖。接着取出第二條,並依然對其加X鎖,但不滿足條件的話,則釋放鎖,一直循環下去。

事實上,當where條件中包含索引列時,Mysql可能也會選擇不走索引查詢,當執行全表掃描的代價小於(查詢二級索引+主鍵索引,即需要回表)時,Mysql此時會直接進行全表掃描。


間隙鎖

間隙鎖也稱爲Gap Lock,顧名思義,鎖的是一段間隙,至於這段間隙有多大,什麼時候會觸發間隙鎖,需要分情況討論。

在可重複讀(RR)的隔離級別下,纔會有間隙鎖的產生。不清楚數據庫的隔離級別,可以參考我的另外一篇文章事務隔離級別

產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,爲了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 

以下情況均處於RR的隔離級別下:

現在有這樣一張表:其中id爲主鍵索引,a爲普通索引

CREATE TABLE t_lock (
  `id` int(11) NOT NULL,
  `a` int(11),
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `index_a`(`a`) USING BTREE
) ENGINE = InnoDB;

有以下的初始數據:

第一種情況,使用主鍵索引,查找一條存在的記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_lock where id=1 for update;
+----+------+
| id | a    |
+----+------+
|  1 |    4 |
+----+------+
1 row in set (0.00 sec)

mysql> 

select ....for update,使用的是當前讀,也稱爲一致性鎖定讀,將會在後面進行說明。

這種方式不會產生間隙鎖,只會產生一條記錄鎖,鎖住主鍵索引中id=1的索引項。其他事物更新、新增、刪除其他id不受影響。

第二種情況,使用主鍵索引,使用範圍查找記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_lock where id>4 and id<16 for update;
+----+------+
| id | a    |
+----+------+
|  7 |   11 |
+----+------+
1 row in set (0.00 sec)

mysql> 

我們嘗試在另外一個終端增加數據:

insert into t_lock values(3,8);   成功

insert into t_lock values(5,8);   阻塞

insert into t_lock values(13,8);  阻塞

insert into t_lock values(16,8);  阻塞

insert into t_lock values(23,8);  成功

id>4 and id<16將會鎖定間隙(4,16),其中不包含7和14,其他事務嘗試在該區間內刪除、更新、新增記錄都會被阻塞。

第三種情況,使用主鍵索引,查找一條不存在的記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_lock where id=9 for update;
Empty set (0.00 sec)

mysql> 

在另外一個終端嘗試添加數據:

insert into t_lock values(3,8);   成功

insert into t_lock values(8,8);   阻塞

insert into t_lock values(9,8);   阻塞

insert into t_lock values(13,8);  阻塞

insert into t_lock values(15,8);  成功

可以看得出來,因爲id=9的記錄不存在,而9屬於(7,14),因此,此時的間隙鎖的範圍爲(7,14)。


以上,討論的是間隙鎖在主鍵索引上的表現,其實和在唯一索引上的表現相同,以下討論在普通索引上的表現。

還是原來的表,原來的數據,我複製一下:id爲主鍵索引,a爲普通索引

第一種情況,使用普通索引,查找一條存在的記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_lock where a=9 for update;
+----+------+
| id | a    |
+----+------+
|  2 |    9 |
+----+------+
1 row in set (0.00 sec)

mysql> 

在另外一個終端嘗試添加數據:

insert into t_lock values(2,8);  阻塞

insert into t_lock values(9,8);  阻塞

insert into t_lock values(100,8); 阻塞

insert into t_lock values(8,10);  阻塞

insert into t_lock values(3,6);  成功

insert into t_lock values(8,11);  成功

實驗證明:a=9將會產生間隙鎖,鎖住的二級索引的範圍的規律是,以a=9,id=2爲基礎,向上尋找最近的二級索引項,即a=6,id=4,再向下尋找最近的二級索引項,即a=11,id=7,鎖住的間隙即爲二者間隙。

當a相同時,id將會升序排序。

本例中,values(3,6)爲a=6,id=4索引的左邊,因此能執行成功,同理,values(8,11)爲二級索引a=11,id=7的右邊,因此也能執行成功。

第二種情況,使用普通索引,使用範圍查找記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_lock where a>5 and a<11 for update;
+----+------+
| id | a    |
+----+------+
|  4 |    6 |
|  2 |    9 |
+----+------+
2 rows in set (0.00 sec)

在另外一個終端嘗試添加數據:

insert into t_lock values(0,4);  成功

insert into t_lock values(0,5);  阻塞

insert into t_lock values(3,5);  阻塞

insert into t_lock values(3,11); 阻塞

insert into t_lock values(6,11); 阻塞

insert into t_lock values(8,11); 成功

同理,在a>5 and a<11的範圍內,找到最左邊的二級索引項,即a=4,id=1,那麼如果在此索引項的左邊,即可插入成功,因此values(0,4)插入成功。

最右邊的二級索引項,即a=11,id=7,那麼如果在此索引項的右邊,即可插入成功,因此values(8,11)插入成功。

第三種情況,使用普通索引,查找一條不存在的記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_lock where a=10 for update;
Empty set (0.00 sec)

mysql>

在另外一個終端嘗試添加數據:

insert into t_lock values(3,9); 阻塞

insert into t_lock values(6,11); 阻塞

insert into t_lock values(8,11); 成功

同理,最左二級索引項爲a=9,id=2,因此values(3,9)阻塞,最右二級索引爲a=11,id=7,因此values(8,11)成功。


通過二者的對比,可以發現:

在普通索引上,不管怎麼查,只要加鎖,必定會產生間隙鎖。

在主鍵索引或唯一索引上,只要記錄存在,則間隙鎖會變成記錄鎖。


臨鍵鎖

臨鍵鎖也稱爲Next-key Lock,是記錄鎖與間隙鎖的組合,即包含索引記錄,也包含索引區間。


快照讀和當前讀

快照讀

簡單的select操作(不包括 select ... lock in share mode, select ... for update),讀取的是歷史數據,不保證是最新的數據。

當前讀

比如以下語句:

  • select ... lock in share mode
  • select ... for update
  • insert
  • update
  • delete

在RR級別下,快照讀是通過MVCC(多版本控制)和undo log來實現的,當前讀是通過加記錄鎖和間隙鎖,即臨鍵鎖來實現的。

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