Innodb 鎖的分類
按照鎖的級別來分有表鎖和行數(shù),按照鎖的類型來分,有共享鎖和排它鎖
表鎖
S 共享鎖,X 排它鎖
這兩個表鎖不常用,在Innodb中表鎖都是Innodb自己維護的
IS 意向共享鎖,IX 意向排它鎖
當我們準備給一張表加上表鎖的時候,我們首先要去 判斷有沒其他的事務(wù)鎖定了其中了某些行?如果有的話,肯定不能加上表鎖。那么這個時候我們就要去掃描整張表才能確定能不能成功加上一個表鎖,如果數(shù)據(jù)量特別大,比如有上千萬的數(shù)據(jù)的時候,加表鎖的效率會很低。當我們在使用共享行鎖時,Innodb 會自動給我們加上IS,使用排他行鎖時自動加上IX ,用來表示改表中已經(jīng)存在那些鎖
IS 和 IX 這兩個鎖是兼容的
Auto-Inc Lock 自增鎖
Auto-Inc Lock 是一個特殊的表級鎖,用于自增列插入數(shù)據(jù)時使用。在插入一條數(shù)據(jù)的時候,需要在表上加個 Auto-Inc Lock,然后為自增列分配遞增的值,在語句插入結(jié)束之后,再釋放 Auto-Inc Lock
行鎖
- S 共享鎖 : 對表中的一行或多行加共享鎖,與排它鎖沖突
- X 排它鎖 : 對表中的一行或多行加排它鎖,與其他鎖沖突
什么情況下使用鎖
- select 默認不使用鎖
- select ... in share mode 使用共享鎖
- select ... for update 使用排他鎖
- insert, update, delete 都會默認加排他鎖
事務(wù)隔離級別為 Serializable 情況下,select 會默認使用 select ... in share mode
鎖的實現(xiàn)
行鎖的實現(xiàn)
行鎖真正鎖住的是索引,查詢條件使用到那個索引就會鎖住那個索引,因為Innodb 是聚簇索引,所以還會把主鍵索引給鎖上
行鎖的模式
1.記錄鎖(Record Locks)
單個行記錄上的鎖
2.間隙鎖(Gap Locks)
鎖定一段范圍內(nèi)的索引記錄。間隙鎖鎖住的是兩個索引之間的區(qū)間,
當我們查詢的記錄不存在,沒有命中任何一個 record,無論是用等值 查詢還是范圍查詢的時候,它使用的都是間隙鎖
間隙鎖會鎖住一個左開右開的區(qū)間
間隙鎖是為了解決幻讀問題,是為了防止其他事務(wù)往索引間隙中插入數(shù)據(jù),所以相同的間隙鎖之間是不沖突的,
間隙鎖只會阻塞插入操作
間隙鎖只存在RR隔離級別下
3.臨鍵鎖(Next-Key Locks)
當我們使用了范圍查詢,不僅僅命中了 Record 記錄,還包含了 Gap 間隙,在這種情況下我們使用的就是臨鍵鎖,它是 MySQL 里面默認的行鎖算法,相當于記錄鎖加上間隙鎖
臨鍵鎖會鎖住一個左開右閉的區(qū)間
4.插入意向鎖(Insert Intention Locks)
插入意向鎖是在插入之前,先判斷插入的間隙是否存在間隙鎖,如果存在則產(chǎn)生一個插入意向鎖,去等待間隙鎖的釋放
多個事務(wù)插入同一個間隙的不同位置,他們并不會沖突。假設(shè)存在索引記錄,其值分別為5和9。單獨的事務(wù)分別嘗試插入值6和7,在獲得插入行的排他鎖之前,每個事務(wù)都使用插入意圖鎖來鎖定5和9之間的間隙,但他們不會互相阻塞。
如何查看鎖信息
select * from performance_schema.data_locks;
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|
| 索引名 | 表鎖或行鎖 | 鎖的模式 | 鎖狀態(tài) | 鎖住的數(shù)據(jù) |
LOCK_MODE(鎖模式)
- IS : 意向共享鎖(表鎖)
- IX : 意向排他鎖(表鎖)
- X : 臨鍵鎖(排他鎖)
- X,GAP : 間隙鎖(排他鎖)
- X,REC_NOT_GAP : 記錄鎖(排他鎖)
實例分析
create table user
(
id bigint auto_increment
primary key,
name varchar(255) null comment '姓名',
number int null comment '編號',
age int null,
constraint user_number_uindex
unique (number)
);
create index user_age_index
on user (age);
INSERT INTO teeth.user (id, name, number) VALUES (1, 'A', 2);
INSERT INTO teeth.user (id, name, number) VALUES (3, 'B', 5);
INSERT INTO teeth.user (id, name, number) VALUES (10, 'C', 9);
INSERT INTO teeth.user (id, name, number) VALUES (11, 'D', 12);
記錄鎖(X,REC_NOT_GAP)
對 id 列為主鍵列或唯一索引列,進行刪除、修改、for update 都會產(chǎn)生記錄寫鎖(X,REC_NOT_GAP)
UPDATE user SET name = 'G' WHERE id = 1;
Delete From user WHERE id = 1;
SELECT * FROM user WHERE id = 1 FOR UPDATE;
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|
| null | TABLE | IX | GRANTED | null |
| PRIMARY | RECORD | "X,REC_NOT_GAP" | GRANTED | 1 |
需要注意的是:
id 列必須為唯一索引列或主鍵列,否則上述語句加的鎖就會退化成間隙鎖或臨鍵鎖
如果where條件未匹配到,也會退化成間隙鎖或臨鍵鎖
同時查詢語句必須為精準匹配(=),不能為 >、<、like等,否則也會退化成臨鍵鎖
如果 where 條件沒有使用到索引,那么會對所有的主鍵加上記錄鎖
間隙鎖(X,GAP)
select * from user where id = 5 for update ;
查看鎖信息:這里 "X,GAP" 就代表間隙鎖,會鎖住 (3,10) 區(qū)間
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|
| null | TABLE | IX | GRANTED | null |
| PRIMARY | RECORD | "X,GAP" | GRANTED | 10 |
臨鍵鎖(X)
select * from user where id <= 10 for update ;
查看鎖信息:會鎖住 (-∞,1] 和 (1,3],(3,10],這里的 ‘X’ 表示的是臨鍵鎖
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|
| null | TABLE | IX | GRANTED | null |
| PRIMARY | RECORD | X | GRANTED | 1 |
| PRIMARY | RECORD | X | GRANTED | 3 |
| PRIMARY | RECORD | X | GRANTED | 10 |
插入意向鎖(X,GAP,INSERT_INTENTION)
第一種情況
事務(wù)A,先獲?。?,10)的間隙鎖
begin;
select * from user where id = 5 for update ;
rollback;
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|
| null | TABLE | IX | GRANTED | null |
| PRIMARY | RECORD | "X,GAP" | GRANTED | 10 |
事務(wù)B,向(3,10)間隙中插入數(shù)據(jù)
begin;
insert into user(id, name, number, age) VALUE (6,'HHH',7,50);
rollback;
| TRANSACTION_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|---|
| 260407 | null | TABLE | IX | GRANTED | null |
| 260407 | PRIMARY | RECORD | "X,GAP,INSERT_INTENTION" | WAITING | 10 |
| 260406 | null | TABLE | IX | GRANTED | null |
| 260406 | PRIMARY | RECORD | "X,GAP" | GRANTED | 10 |
第二種情況
事務(wù)B,先向(3,10)間隙中插入數(shù)據(jù)
begin;
insert into user(id, name, number, age) VALUE (6,'HHH',7,50);
rollback;
| TRANSACTION_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|---|
| 260408 | null | TABLE | IX | GRANTED | null |
事務(wù)A,獲?。?,10)的間隙鎖
begin;
select * from user where id = 5 for update ;
rollback;
| TRANSACTION_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
|---|---|---|---|---|---|
| 260413 | null | TABLE | IX | GRANTED | null |
| 260413 | PRIMARY | RECORD | "X,GAP" | GRANTED | 6 |
| 260408 | null | TABLE | IX | GRANTED | null |
| 260408 | PRIMARY | RECORD | "X,REC_NOT_GAP" | GRANTED | 6 |
可以看到先插入的情況下,相當于 id = 6 的索引已經(jīng)存在了,并且添加了 id = 6 的記錄鎖,由于之前查詢的是 id = 5,所以之前的間隙鎖變成了(3,6)
死鎖
事務(wù)A
begin ;
select * from user where id = 5 for update ;
insert into user(id, name, number, age) VALUE (6,'@@@',7,50);
rollback ;
事務(wù)B
begin ;
select * from user where id = 5 for update ;
insert into user(id, name, number, age) VALUE (5,'@@@',7,50);
rollback ;
由于間隙鎖之間不是互斥的,插入意向鎖之間也不是互斥的,所以事務(wù)A、B 可以同時獲取到間隙鎖(3,10)
- 事務(wù)A執(zhí)行插入語句,生成插入意向鎖,等待事務(wù)B釋放間隙鎖(3,10)
- 事務(wù)B執(zhí)行插入語句,生成插入意向鎖,等待事務(wù)A釋放間隙鎖(3,10)
從而造成死鎖