一、锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁
:
- 共享锁(S Lock),允许事务读一行数据。
- 排他锁(X Lock),允许事务删除或更新一行数据。
锁的兼容性:
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
必须指出的时S和X锁都是行锁,兼容指的是对同一记录的兼容性情况
二、锁的粒度
InnoDB支持多粒度锁定,允许表锁和行锁
同时存在,为了支持在不同粒度上进行加锁操作,InnoDB支持一种额外的加锁方式,称之为意向锁。
1. 意向锁是什么
行锁的目的是为了对某一行进行读取、更新、删除操作。
意向锁是表锁
,但它的目的确实为了表明某个事务正在或将要锁定某一行
,表明加锁的意图。
两种意向锁:
- 意向共享锁(IS):表明事务意图在表中某个单独行上设置共享锁。
- 意向排他锁(IX):表明事务意图在表中某个单独行上设置排他锁。
2. 意向锁为什么是表锁
当我们需要加一个排他锁(表锁)时,需要根据意向锁判读表中是否有数据行被锁定(行锁)。
- 如果意向锁是行锁,需要遍历每一行数据确认
- 如果意向锁是表锁,只需要判断一次就知道有没有数据行被锁定。
根本原因是表锁与行锁之间是排斥的:
如果有一个事务占据了某个表的行排他锁,那么另一个事务要获取这个表的表排他锁或表共享锁都应该受阻碍,如果没有意向锁的存在,那么就得一行行判断,效率低,因此用意向锁的方式给行锁弱升级(虚张声势)。
3. 意向锁如何支持表锁和行锁共存
由前面的解释知道,行锁和表锁本身是不共存的(都是共享锁可以)。
- 数据库支持表锁和行锁共存指的是同时支持这两类锁操作。
- 如果事务A对某一行上锁,那么其他事务就不能修改这一行,与事务B锁住了整个表就能修改表中任意一行冲突。所以在没有意向锁的时候行锁和表锁共存会存在很多问题。意向锁通过在申请行锁前先申请意向锁避免逐行检查行锁,提升性能。
4. 意向锁与其他锁的兼容性
- 意向锁之间彼此兼容
IX | IS | |
---|---|---|
IX | 兼容 | 兼容 |
IS | 兼容 | 兼容 |
- 意向锁与行锁不会发生冲突,只与表级X、S锁发生冲突
下表中的X、S均指表锁。行锁不会与意向表冲突(也不会比较,行锁之前会先比较意向锁,两个意向锁不冲突之后,两个行锁如果恰好是同一行再比较冲突与否)
IX | IS | X | S | |
---|---|---|---|---|
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
IS | 兼容 | 兼容 | 不兼容 | 兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 | 不兼容 | 兼容 |
三、锁的算法
InnoDB存储引擎由3种行锁的算法
- Record Lock(记录锁): 单个行记录上的锁
- Gap Lock(间隙锁):锁定一个范围,但不包含记录本身(解决幻读问题)
- Next-Key Lock(临键锁): Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
其实如果只是要解决幻读,间隙锁似乎就够用了(但实际上不够),因为间隙锁只能考虑到之间不存在的元素不会被其他事务修改得多出来,但是要避免原先存在的记录不被更改或删除甚至重复,则需要加上记录锁,也就是临键锁
间隙锁只在可重复读(RR)隔离级别下才有效。
- RR遵循两阶段锁协议,所有加锁资源都在事务提交或者回滚时才释放。
间隙锁之间不存在冲突关系:
- 与行锁冲突的是另外一个行锁;
- 与间隙锁冲突的是往这个间隙中插入一个记录这个操作。
使用相等条件为一个不存在的记录加锁,InnoDB也会采用间隙锁。
比如表中存在 c= 5, c= 10
事务A加共享锁查询c=7的记录(当前读),那么A会加间隙锁(5,10)。
之后B也查询c = 7的记录,也会加间隙锁(5,10),这两个间隙锁之间不会冲突。
- 默认间隙锁之间都是开区间;
- 间隙锁+行锁合成临键锁,为左开右闭区间。
间隙锁和临键锁如何解决幻读问题
- 防止间隙内有新数据被插入。
- 防止已存在的数据,更新成间隙内的数据。
- 加锁规则(必须在RR隔离级别下)
- 加锁基本单位是临键锁(左开右闭)
- 当查询的索引含有唯一属性(唯一索引,主键索引)时,Innodb存储引擎会对next-key lock进行优化,将其降为record lock,即仅锁住索引本身,而不是范围。
- 使用唯一索引查询:
– 并且只锁定一条记录时,innoDB会使用行锁
。
– 但是检索条件是范围检索,或者是唯一检索然而检索结果不存在(试图锁住不存在的数据)时,会产生Next-Key Lock
。 - 使用普通索引检索时,不管是何种查询,只要加锁,都会产生间隙锁。
– 同时使用唯一索引和普通索引时,由于数据行是优先根据普通索引排序,再根据唯一索引排序,所以也会产生间隙锁。
- 加临键锁的操作实际上分为两步,先加间隙锁,加锁成功后,再加行锁,此时可能会被阻塞
- 间隙锁带来的死锁问题
间隙锁带来的死锁例子
还会影响并发度。
sessionA | sessionB |
---|---|
beigin; select * from t where id =9 for update; | |
beigin; select * from t where id =9 for update; | |
insert into t values(9,9,9); (blocked) | |
insert into t values(9,9,9); (ERROR 1213(40001); Deadlock found) |
表内现在有数据(5,5,5)和(10,10,10)第一个数为id(主键)
- A执行select时id = 9数据不存在,加上间隙锁(5,10)
- B执行select时id = 9数据不存在,加上间隙锁(5,10),间隙锁加锁不冲突
- B试图插入(9,9,9),被A的间隙锁挡住了,进入等待
- A试图插入(9,9,9),被B的间隙锁挡住了,死锁
四、关于死锁
两个或以上事务在执行过程中,因争夺锁资源互相等待,若无外力作用,事务将无法推进。
如何解决死锁:
1. 超时机制
当等待时间超过设定的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。
2. 等待图进行死锁检测(InnoDB引擎)
等待图要求数据库保存以下两种信息:
- 锁的信息链表
- 事务等待链表
通过上述链表可以构造出一张图,若存在回路则代表存在死锁,在每个事务请求锁并发生等待时都会判断是否存在回路,通常来说InnoDB选择回滚undo 量最小的事务