前言
今天字节的面试问到了MySQL的锁,自己回答的含糊不清,所以重新学习一遍做一个梳理。这里讲的是MySQL 5.7在InnoDB引擎下的锁机制。
概述
总的来说,InnoDB共有七种类型的锁:
- 共享锁(S)/独占锁(X)
- 意图共享锁(IS)/意图独占锁(IX)
- 记录锁
- 间隙锁
- 临键锁
- 插入意图锁
- 自增锁
1.共享锁/独占锁
这两个锁都是行级锁
- 共享锁(S):允许一个事务去读一行
- 独占锁(X):允许一个事务更新或删除一行
互斥性:
- S锁定之间不互斥,即两个事务可以同时获取该行的S锁
- X锁定与其他任何锁互斥,即一个事务获得该行的X锁,其他事务对该行的X/S锁都不被马上许可,需要等待原本事务的完成
2.意图锁
InnoDB支持多间隔尺寸锁定,它允许记录锁和对整个表的锁共存。要实现多间隔级别尺寸的锁定,需要一种额外的锁,这种锁被称为意图锁。意图锁是表级锁。
先提前声明一个意图,并获取表级别的意图锁(意图共享锁/意图独占锁),如果获取成功,对该表的某些行加锁(S/X)。
意向锁协议:
- 在假设的事务可以获得对某假定行的S锁之前,他必须首先获得对包含改行的一个IS锁或者更强的锁。
- 在假设的事务可以获得对某假定行的X锁定之前,他必须首先获得对包含改行的表的一个IX锁定。
这些结果可以用一个兼容矩阵来表示:
客户端A获得对行的S锁:
接着客户端B尝试从该表删除该行,
删除要求一个X锁定,因为这个锁定不兼容客户端A持有的S锁定,所以X锁定不被允许,所以请求进入对行及客户端阻挡的锁定请求队列。(如上图,回车后,MySQL没有任何动作)
最后客户端A也试图删除该行:
因为客户端A需要一个X 锁定来删除该行,所以在这里发生死锁。尽管如此,锁定请求不被允许,因为客户端B已经有一个对X锁定的请求并且它正等待客户端A释放S锁定。因为客户端B之前对X 锁定的请求,被客户端A持有的S锁定也不能升级到X锁定。因此,InnoDB对客户端A产生一个错误,并且释放它的锁定。在那一点上,客户端B的锁定请求可以被许可,并且客户端B从表中删除行。
3.记录锁
记录锁是一个在索引行记录的锁。
例如:select * from tb1 where id = 1; 如果在id上的索引被用到,防止其他任何事务变动id=1的行。记录锁总是在索引行上加锁,即使一个表没有索引,这时InnoDB也会设置一个隐藏的聚集索引,这是InnoDB必须要有索引的一个原因。(还有其他原因是InnoDB采用B+树索引,会根据主索引建立一个B+树)。
当查询字段没有索引时,例如:update tb1 set age = 20 where name = ‘wule’;如果name字段不存在索引,那么就会锁住整个表。否则就锁住该行。
4.间隙锁
间隙锁封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者索引最后一天记录的范围。一般作用于范围查询:>,<,between…and…
select * from tb1 where id between 1 and 4;
# 这个SQL语句会封锁区间(1,4),防止其他事务在该区间插入数据
对于使用唯一索引来搜索一个的语句,不产生间隙锁定
间隙锁的主要目的是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降为读未提交(RC),间隙锁会自动失效。
5.临键锁
临键锁,是记录锁和间隙锁的组合,它的封锁既包含索引记录也包含索引区间。
默认情况,InnoDB使用next-key locks来锁定记录。但当查询的索引含有唯一属性的时候,next-key会进行优化,将其降级为记录锁。即锁住记录本身。
如果对tb1表中的name字段添加一个普通索引。然后执行:
# 事务A
select * from tb1 where name = 'wule' for update;
# 事务B
insert into tb1 values(0, 'xc');
因为name的索引是普通索引,即使命中了name=‘wule’,也会锁住该行及以前的记录。
临键锁的目的是为了避免幻读,如果把事务的隔离级别降为RC,临键锁会失效。
6.插入意图锁
插入意图锁是间隙锁的一种,它是专门针对insert操作的,多个事务在同一个索引和同一范围内插入数据,如果插入的位置不冲突,则不会彼此阻塞。
7.自增锁
自增锁是一种特殊的表级锁。事务插入到具有AUTO_INCREMENT列的表中,那么其他事务必须等待改事务执行完毕,以便该事务插入的是连续的主键值。
总结
按粒度划分为:
- 表锁:意图锁、自增锁
- 行锁:记录锁、间隙锁、临键锁、插入意图锁
间隙锁和临键锁只在RR以上的隔离级别生效,RC下会失效。
改事务执行完毕,以便该事务插入的是连续的主键值。