為了提高InnoDB存儲(chǔ)引擎的并發(fā)性,InnoDB采用了細(xì)粒度的行鎖燃领,并在針對不同的鎖使用不同級別鎖或不使用鎖棕孙。本文將介紹InnoDB中使用的七種鎖,分別是共享鎖(share书蚪,S鎖)/排它鎖(exclusive,X鎖)迅栅、意向鎖殊校、記錄鎖、間隙鎖读存、臨鍵鎖为流、插入意向鎖呕屎、自增鎖。
1. 共享鎖(S鎖)/排它鎖(X鎖)(Share Lock/Exclusive Lock)
根據(jù)字面意思敬察,不難理解:
共享鎖秀睛,是不同的事務(wù)會(huì)話可以共享使用的鎖。排它鎖莲祸,是一種只能被某個(gè)會(huì)話獨(dú)享使用的鎖蹂安。
對于不同的會(huì)話A和B,如果A已經(jīng)獲取了某條數(shù)據(jù)R上的共享鎖锐帜,B也可以獲取R上的共享鎖田盈,但是不能獲取R上排它鎖。此種情況下抹估,B必須等待A釋放共享鎖后缠黍,才能獲取排它鎖。
如果A已經(jīng)獲取了某條數(shù)據(jù)R上的排它鎖药蜻,那么B既不能獲取R的共享鎖和R的排它鎖瓷式,必須等待A釋放數(shù)據(jù)R上的排它鎖;
等待语泽,具體表現(xiàn)為阻塞
鎖之間的兼容性如下表:
鎖類型 | X | S |
---|---|---|
X | Conflict | Conflict |
S | Conflict | Compatible |
注意:此處的鎖既可以表鎖贸典,也可以是行鎖
從上表可以看到排它鎖與其他的鎖都互不兼容,共享鎖之間是互相兼容踱卵。
那么如何才能獲取數(shù)據(jù)R上共享鎖or排它鎖呢廊驼?
#表定義
CREATE TABLE `t` (
`id` int NOT NULL ,
`code` int NOT NULL ,
PRIMARY KEY (`id`)
);
select id from t where id = 1 for update; # 獲取id=1數(shù)據(jù)行的排它鎖
select id from t where id =1 lock in share mode; # 獲取id=1數(shù)據(jù)行上的共享鎖
2.意向鎖(Intention Lock)
InnoDB支持多粒度的鎖( multiple granularity locking),允許表鎖和行鎖同時(shí)存在惋砂。
InnoDB通過意向鎖實(shí)現(xiàn)了多粒度的鎖妒挎。
意向鎖是表級別的鎖,表示了該會(huì)話在之后會(huì)使用的鎖(共享鎖or排它鎖)
意向鎖有兩種:共享意向鎖(IS)西饵、排它意向鎖(XS)
- 共享意向鎖表示一個(gè)事務(wù)即將需要獲得該表中某些行的共享鎖酝掩;
select lock in share mode; #可以獲得IS鎖
- 排它意向鎖表示一個(gè)事務(wù)即將需要獲得該表中某些行的排它鎖;
select ... for update; # 獲得XS鎖
意向鎖協(xié)議規(guī)定如下:
- 在一個(gè)事務(wù)獲得某條(些)數(shù)據(jù)行R上的共享鎖之前眷柔,該事務(wù)必須要獲得該表上的共享意向鎖(IS)期虾;
- 在一個(gè)事務(wù)獲得某條(些)數(shù)據(jù)行R上的排它鎖之前,該事務(wù)必須要獲得該表上的排它意向鎖(XS)驯嘱;
S鎖镶苞,X鎖,IS鎖鞠评,IX鎖兼容性如下表所示:
注意:以下鎖均指的是表級別的鎖茂蚓,而非行級別的鎖。
鎖類型 | X | S | IX | IS |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
S | Conflict | Compatible | Conflict | Compatible |
IX | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
對于IX和X的組合,如果一個(gè)事務(wù)獲得表的X鎖煌贴,那么另一個(gè)事務(wù)如果想獲得IX時(shí)就會(huì)被阻塞御板,等待X鎖的釋放锥忿。
意向鎖僅僅表明意向牛郑,所以意向鎖不會(huì)阻塞除掃描全表之外的(比如"LOCK TABLES ... WRITE")。
3.記錄鎖(Record lock)
記錄鎖是一種加在索引數(shù)據(jù)記錄上鎖敬鬓,以防止其他事務(wù)對于該數(shù)據(jù)記錄進(jìn)行修改淹朋、刪除。
例如:
select id from t where id = 1 for update; # 當(dāng)前事務(wù)將獲得id=1這行記錄的排它記錄鎖
select id from t where id = 1 lock in share mode; # 當(dāng)前事務(wù)將獲得id=1這行記錄的共享鎖
通過update 钉答、delete操作數(shù)據(jù)時(shí)础芍,也會(huì)相應(yīng)的獲取對應(yīng)索引記錄的記錄鎖;
注意id為表t的主鍵数尿,innoDB默認(rèn)為主鍵加上了聚集索引(唯一性索引)仑性;
4.間隙鎖
間隙鎖是加載某個(gè)索引記錄區(qū)間上的鎖,間隙鎖防止其他事務(wù)在鎖定區(qū)間上插入新的記錄右蹦,避免不可重復(fù)讀诊杆。
那么如何獲取間隙鎖呢?
首先何陆,我們?yōu)楸韙插入幾條數(shù)據(jù)記錄:
mysql> insert into t value (1,1);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value (8,8);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value (20,20);
Query OK, 1 row affected (0.00 sec)
現(xiàn)在數(shù)據(jù)庫里面有3條數(shù)據(jù)晨汹,索引值為1,8,20。InnoDB會(huì)將索引id的區(qū)間進(jìn)行劃分為:
(-infinite贷盲,1)淘这,
(1,8)巩剖,
(8,20)铝穷,
(20,+infinite)四個(gè)區(qū)間佳魔;
例1 :
#事務(wù)會(huì)話A 曙聂,where 中 1 和 8 都是 索引值
select id from t where id between 1 and 8 lock in share mode; #獲取區(qū)間(1,8)上的共享間隙鎖;
其他事務(wù)如果想在(1,8)區(qū)間內(nèi)插入新的數(shù)據(jù)記錄吃引,將會(huì)阻塞直到超時(shí),如下所示:
#事務(wù)會(huì)話B
mysql> insert into t value (7,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
例2:
#事務(wù)A獲取間隙鎖筹陵,此時(shí)where中的4和14都不是實(shí)際存在的索引值
mysql> select id from t where id between 4 and 14 lock in share mode;
+----+
| id |
+----+
| 8 |
+----+
1 row in set (0.00 sec)
mysql> insert into t value (-1,"test");
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value(2,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (7,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (10,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value(21,"test");
Query OK, 1 row affected (0.00 sec)
通過實(shí)驗(yàn),我們看到(1,8)镊尺,(8,20)這兩個(gè)區(qū)間被鎖住了朦佩,禁止了其他事務(wù)在這兩個(gè)區(qū)間中插入新的數(shù)據(jù)。
間隙鎖庐氮,一個(gè)非常關(guān)鍵的點(diǎn)就是如何確定鎖定區(qū)間范圍语稠?
InnoDB是通過對于where中范圍查找中的最小值sMin和最大值sMax,其中鎖定區(qū)間最小值lMin和最大值lMax。
- lMin的值為表索引值中小于or等于sMin中的最大值仙畦,對于sMin=4的情況输涕,lMin=1。
- lMax的值為表索引值中大于or等于sMax中的最小值慨畸,對于sMax=14的情況下莱坎,lMax=20.
所以例2中區(qū)間的范圍是(1,20)也即(1,8)和(8,20)。
注意新增加code不是唯一性索引
5. 臨鍵鎖(next-key lock)
臨鍵鎖是記錄鎖和間隔鎖的結(jié)合寸士,它既可以鎖定索引記錄也可以鎖定索引記錄之間的區(qū)間檐什。臨鍵鎖主要的目的是解決幻讀的問題。
在隔離級別是可重復(fù)性讀(RR)下弱卡,并且innodb_locks_unsafe_for_binlog的值為OFF時(shí)乃正,InnoDB默認(rèn)使用的就是使用臨鍵鎖(next-key lock)。
在使用臨鍵鎖是婶博,如果查詢條件中含有唯一性索引瓮具,InnoDB會(huì)進(jìn)行鎖降級,變?yōu)橛涗涙i凡人。形式如:select id from t where id =1 for update名党;
如果查詢條件不是唯一性索引,那么SQL會(huì)獲得什么鎖呢划栓?
# 修改表t的結(jié)構(gòu)兑巾,為code增加索引,該索引是普通索引(Secondary Index)并且非唯一性索引(Unique Index)忠荞,并增加數(shù)據(jù)記錄(15,8)
mysql> ALTER TABLE `t`
ADD INDEX `idx_code` (`code`) ;
mysql> insert into t value(15,8);
修改后蒋歌,數(shù)據(jù)表的記錄為:
(1,1)
(8委煤,8)
(15堂油,8)
(20,20)
從而對于索引 idx_code碧绞,我們可以得到其對應(yīng)的區(qū)間分別為:
(-infinite府框,1],(1,8],(8,20],(20,+infinite)。
此時(shí)我們使用code為查詢條件進(jìn)行查詢:
事務(wù)A:
mysql> select id from t where code = 8 lock in share mode;
+----+
| id |
+----+
| 8 |
| 15 |
+----+
2 rows in set (0.00 sec)
對于事務(wù)A讥邻,當(dāng)執(zhí)行完加鎖讀的語句后迫靖,InnoDB會(huì)使用next-key進(jìn)行加鎖,由于code不是唯一性索引兴使,所以InnoDB不會(huì)對鎖進(jìn)行降級系宜。由于表中存在兩個(gè)索引,所以會(huì)分別進(jìn)行加鎖发魄。對于聚集索引列id盹牧,由于加鎖讀命中了兩條俩垃,id=8 和 id=15的記錄會(huì)加鎖。同時(shí)也會(huì)使用間隙鎖汰寓,對code對應(yīng)區(qū)間中的(1,8)和(8,20)進(jìn)行加鎖口柳。
現(xiàn)在通過事務(wù)B對以上結(jié)論進(jìn)行驗(yàn)證。
事務(wù)B:
mysql> insert into t value (-1,-1);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value (2,2);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (9,9);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (21,21);
Query OK, 1 row affected (0.00 sec)
mysql> select * from t where id = 8 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 15 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
通過事務(wù)B的insert和加鎖讀語句有滑,驗(yàn)證事務(wù)A獲得鎖跃闹。
在進(jìn)一步討論這個(gè)問題,如果code列沒有索引俺孙,那么將會(huì)加什么鎖呢辣卒?
事務(wù)A:
mysql> select id from t where code = 8 lock in share mode;
+----+
| id |
+----+
| 8 |
| 15 |
+----+
2 rows in set (0.00 sec)
由于code沒有索引掷贾,為了找到code=8的數(shù)據(jù)記錄睛榄,InnoDB將會(huì)進(jìn)行全表掃描,此時(shí)全部的數(shù)據(jù)記錄將會(huì)被鎖定想帅,其他事務(wù)無法進(jìn)行操作场靴。
接下來利用事務(wù)B進(jìn)行驗(yàn)證:
事務(wù)B:
mysql> insert into t value (-1,-1);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (2,2);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (9,9);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (21,21);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 8 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 15 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 20 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
可以看到表t所有間隙都被鎖定,驗(yàn)證上述分析結(jié)論港准。
6. 插入意向鎖(Insert Intention )
InnoDB為了提高數(shù)據(jù)插入并發(fā)的效率旨剥,引入了插入意向鎖。
插入意向鎖是一種間隙鎖浅缸,當(dāng)事務(wù)視圖想向一個(gè)區(qū)間插入數(shù)據(jù)時(shí)轨帜,首先獲得插入意向鎖。
插入間隙鎖規(guī)定衩椒,如果多個(gè)事務(wù)向同一區(qū)間插入數(shù)據(jù)時(shí)蚌父,如果這些數(shù)據(jù)屬于不同的位置(索引位置),那么之間的插入是互不影響的毛萌。
This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.
7. 自增鎖(AUTO-INC lock)
自增鎖是一種特殊表鎖苟弛,這種鎖用在對有自增列的表進(jìn)行insert時(shí)。當(dāng)事務(wù)A向自增表插入數(shù)據(jù)表阁将,那么事務(wù)A將獲得該表上自增鎖膏秫,其他事務(wù)試圖想該表插入數(shù)據(jù)時(shí),必須等待事務(wù)釋放該表的自增鎖做盅。
總結(jié):
InnnoDB中鎖的使用缤削,與查詢是否使用索引(全表掃描or not)、索引類型(聚集索引or普通索引)吹榴、查詢范圍(單個(gè)or區(qū)間)都有關(guān)系亭敢。但是掌握了各個(gè)鎖的使用情況,就能根據(jù)具體的SQL判斷出使用鎖的類型腊尚。
參考:
- https://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
- MySQL技術(shù)內(nèi)幕-InnoDB存儲(chǔ)引擎