前言
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來實現的,當前讀是通過加記錄鎖和間隙鎖,即臨鍵鎖來實現的。