Innodb 锁的分类
按照锁的级别来分有表锁和行数,按照锁的类型来分,有共享锁和排它锁
表锁
S 共享锁,X 排它锁
这两个表锁不常用,在Innodb中表锁都是Innodb自己维护的
IS 意向共享锁,IX 意向排它锁
当我们准备给一张表加上表锁的时候,我们首先要去 判断有没其他的事务锁定了其中了某些行?如果有的话,肯定不能加上表锁。那么这个时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果数据量特别大,比如有上千万的数据的时候,加表锁的效率会很低。当我们在使用共享行锁时,Innodb 会自动给我们加上IS,使用排他行锁时自动加上IX ,用来表示改表中已经存在那些锁
IS 和 IX 这两个锁是兼容的
Auto-Inc Lock 自增锁
Auto-Inc Lock 是一个特殊的表级锁,用于自增列插入数据时使用。在插入一条数据的时候,需要在表上加个 Auto-Inc Lock,然后为自增列分配递增的值,在语句插入结束之后,再释放 Auto-Inc Lock
行锁
- S 共享锁 : 对表中的一行或多行加共享锁,与排它锁冲突
- X 排它锁 : 对表中的一行或多行加排它锁,与其他锁冲突
什么情况下使用锁
- select 默认不使用锁
- select ... in share mode 使用共享锁
- select ... for update 使用排他锁
- insert, update, delete 都会默认加排他锁
事务隔离级别为 Serializable 情况下,select 会默认使用 select ... in share mode
锁的实现
行锁的实现
行锁真正锁住的是索引,查询条件使用到那个索引就会锁住那个索引,因为Innodb 是聚簇索引,所以还会把主键索引给锁上
行锁的模式
1.记录锁(Record Locks)
单个行记录上的锁
2.间隙锁(Gap Locks)
锁定一段范围内的索引记录。间隙锁锁住的是两个索引之间的区间,
当我们查询的记录不存在,没有命中任何一个 record,无论是用等值 查询还是范围查询的时候,它使用的都是间隙锁
间隙锁会锁住一个左开右开的区间
间隙锁是为了解决幻读问题,是为了防止其他事务往索引间隙中插入数据,所以相同的间隙锁之间是不冲突的,
间隙锁只会阻塞插入操作
间隙锁只存在RR隔离级别下
3.临键锁(Next-Key Locks)
当我们使用了范围查询,不仅仅命中了 Record 记录,还包含了 Gap 间隙,在这种情况下我们使用的就是临键锁,它是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁
临键锁会锁住一个左开右闭的区间
4.插入意向锁(Insert Intention Locks)
插入意向锁是在插入之前,先判断插入的间隙是否存在间隙锁,如果存在则产生一个插入意向锁,去等待间隙锁的释放
多个事务插入同一个间隙的不同位置,他们并不会冲突。假设存在索引记录,其值分别为5和9。单独的事务分别尝试插入值6和7,在获得插入行的排他锁之前,每个事务都使用插入意图锁来锁定5和9之间的间隙,但他们不会互相阻塞。
如何查看锁信息
select * from performance_schema.data_locks;
INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
---|---|---|---|---|
索引名 | 表锁或行锁 | 锁的模式 | 锁状态 | 锁住的数据 |
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 都会产生记录写锁(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) 区间
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)
第一种情况
事务A,先获取(3,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 |
事务B,向(3,10)间隙中插入数据
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 |
第二种情况
事务B,先向(3,10)间隙中插入数据
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 |
事务A,获取(3,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 的索引已经存在了,并且添加了 id = 6 的记录锁,由于之前查询的是 id = 5,所以之前的间隙锁变成了(3,6)
死锁
事务A
begin ;
select * from user where id = 5 for update ;
insert into user(id, name, number, age) VALUE (6,'@@@',7,50);
rollback ;
事务B
begin ;
select * from user where id = 5 for update ;
insert into user(id, name, number, age) VALUE (5,'@@@',7,50);
rollback ;
由于间隙锁之间不是互斥的,插入意向锁之间也不是互斥的,所以事务A、B 可以同时获取到间隙锁(3,10)
- 事务A执行插入语句,生成插入意向锁,等待事务B释放间隙锁(3,10)
- 事务B执行插入语句,生成插入意向锁,等待事务A释放间隙锁(3,10)
从而造成死锁