当多个事务或进程访问同一个资源时,为了保证数据的一致性就会用到锁机制,在MySQL中锁有多种不同的分类。
以操作粒度区分
行级锁、表级锁和页级锁
以操作类型区分
读锁、写锁
为了允许行锁和表锁的共存,实现多粒度的锁机制,InnoDB还有两种内部使用的意向锁,这两种意向锁都是表锁:
为什么意向锁是表级锁?
为了减少确认次数,提升性能:如果意向锁是行锁,需要遍历每一行去确认数据是否已经加锁;如果是表锁的话,只需要判断一次就知道有没有数据行被锁定;
意向锁是如何支持行级锁、表级锁共存的?
举例
以操作性能区分
乐观锁、悲观锁
行锁的实现原理
意向锁是InnoDB自动加的,不需要用户干预;对于 UPDATE 、DELETE 和 INSERT 语句,InnoDB会自动给涉及的数据集增加排他锁(X);对于普通的 SELECT 语句,InnoDB不会加任何锁;事务也可以通过以下语句显式的给记录集加共享锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE 和排它锁 SELECT * FROM table_name WHERE ... FOR UPDATE 。
在InnoDB中,支持行锁和表锁,行锁又分为共享锁和排它锁。InnoDB行锁是通过对索引数据页上的记录加锁实现的。由于InnoDB行锁的实现特点,导致只有通过索引条件检索并且执行计划中真正使用到索引时InnoDB才会使用行锁 ;并且不论使用主键索引、唯一索引、普通索引,InnoDB都会使用行锁来进行加锁,否则InnoDB将使用表锁。
由于InnoDB是针对索引加锁,而不是针对记录加锁,所以即使多个事务访问不同行的记录,但如果使用的是相同的索引,还是会出现锁冲突的情况,甚至出现死锁。
行锁的不同实现
行锁的主要实现有三种: Record Lock 、 Gap Lock 和 Next-Key Lock 。
RecordLock:记录锁,锁定单个行记录的锁,RC和RR隔离级别支持。
GapLock:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。范围锁,RR隔离级别支持。(加锁之后间隙范围内不允许插入数据,防止发生幻读)
Next-Key Lock:临键锁,它是记录锁和间隙锁的结合体,锁住数据的同时锁住数据前后范围。记录锁+范围锁,RR隔离级别支持。
insert 的加锁流程:
执行 insert 之后,如果没有任何冲突,在 show engine innodb status 命令中是看不到任何锁的,这是因为 insert 加的是隐式锁。什么是隐式锁?隐式锁的意思就是没有锁!
所以,根本就不存在先加插入意向锁,再加排他记录锁的说法,在执行 insert 语句时,什么锁都不会加。当其他事务执行 select ... lock in share mode 时触发了隐式锁的转换。
InnoDb 在插入记录时,是不加锁的。如果事务 A 插入记录且未提交,这时事务 B 尝试对这条记录加锁:事务 B 会先去判断记录上保存的事务 id 是否活跃,如果活跃的话,那么就帮助事务 A 去建立一个锁对象(排他记录锁),然后自身进入等待事务 A 状态,这就是所谓的隐式锁转换为显式锁。
结论:
执行 insert 语句,判断是否有和插入意向锁冲突的锁,如果有,加插入意向锁,进入锁等待;如果没有,直接写数据,不加任何锁;
执行 select ... lock in share mode 语句,判断记录上是否存在活跃的事务,如果存在,则为 insert 事务创建一个排他记录锁,并将自己加入到锁等待队列;
间隙锁的主要目的是为了防止幻读,其主要通过两个方面实现这个目的:
另外一方面,是为了满足其恢复和复制的需要。对于基于语句的日志格式的恢复和复制而言,由于MySQL的BINLONG是按照事务提交的先后顺序记录的,因此要正确恢复或者复制数据,就必须满足:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,根本原因还是不允许出现幻读。
锁规则
在mysql8.0.18及以上已经没有这个bug
锁结构
对不同记录加锁时,如果符合下边这些条件:
那么这些记录的锁就可以被放到一个锁结构中。
锁的兼容性
从图中可以看出,横向为事务A拥有的锁,竖向为事务B想要获取的锁;举例: 如果前一个事务A 持有 gap 锁 或者 next-key 锁的时候,后一个事务B如果想要持有 Insert Intention 锁的时候会不兼容,出现锁等待。
以 update t1 set name=‘XX’ where id=10 操作为例:
主键加锁
加锁行为:仅在id=10的主键索引记录上加X锁。
唯一键加锁
加锁行为:先在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁。
非唯一键加锁
加锁行为:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)(11,f)范围分别加Gap Lock。
无索引加锁
加锁行为:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎 锁机制是基于索引实现的记录锁定)。
查看事务、锁的语句:
输出结果解析:
数据准备:
锁举例
锁等待超时:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
死锁:1213 Deadlock found when trying to get lock
等值查询间隙锁
分析:
这是如果有 Session4 想要更新 id=8 的记录,是可以执行成功的,因为间隙锁之间互不冲突;
分析:
LOCK IN SHARE MODE; 只锁覆盖索引,FOR UPDATE; 会顺便锁上主键索引;
主键索引范围锁
select * from t where id=10 for update;
select * from t where id>=10 and id<11 for update;
对于以上两条SQL,加锁的范围不一致,第一条是id=10 的行锁,第二条是 (10, 15] 的 Next-key Lock。
分析:
如果 Session3 更新一个 (10, 15) 的值,则会阻塞;
非唯一索引范围锁
分析:
Session1 给索引c加上了 (5,10], (10,15] 两个 Next-key Lock ;由于是范围查询,不触发优化,不会退化成间隙锁
非唯一索引等值锁for Update
数据准备:
在表t中,a列有普通索引,所以可能锁定的范围有:
(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)
Session1 执行完成之后预期加锁范围为 (5, 8] 和 (8, 11],由于锁优化策略,退化成间隙锁,范围变成 (5, 8] 和 (8, 11) ,也就是 (5, 11) ,插入12和4不会阻塞很好理解。但是 5不在锁的范围内,还是被锁上了。
是因为如果索引值相同的话,会根据id进行排序加锁,所以最终的加锁范围是索引a的 (5, 4) 到 (11, 6) 的范围。
死锁模拟-场景1
AB BA操作问题
数据准备:
死锁模拟-场景2
S-lock 升级 X-lock
数据准备:
沿用简单场景1数据
分析:
死锁模拟-场景3
数据准备:
分析:
事务一在插入时由于跟事务二插入的记录唯一键冲突,所以对 a=10 这个唯一索引加 S 锁(Next-key)并处于锁等待,事务二再插入 a=9 这条记录,需要获取插入意向锁(lock_mode X locks gap before rec insert intention)和事务一持有的 Next-key 锁冲突,从而导致死锁。
死锁日志:
并不是在日志里看到 lock_mode X 就认为这是 Next-key 锁,因为还有一个例外:如果在 supremum record 上加锁,locks gap before rec 会省略掉,间隙锁会显示成 lock_mode X,插入意向锁就会显示成 lock_mode X insert intention。
INSERT 语句,会尝试获取lock mode S waiting 锁,这是为了检测唯一键是否重复,必须进行一次当前读,要加 S 锁。
INSERT 加锁分几个阶段:先检查唯一键约束,加 S 锁,再加插入意向锁,最后插入成功时升级为 X 锁。