Innodb中的锁机制
- 什么是锁
- latch
- lock
- 行级锁与表级锁
- 锁的分类
- 共享锁和排他锁
- 共享锁
- 排他锁
- 意向锁
- 锁的算法实现
- Record Lock
- Gap Lock
- Next-Key Lock
- 加锁规则
- 举例说明
- 场景1:主键索引等值间歇锁
- 场景2:非唯一索引等值锁
什么是锁
Mysql中主要有两种锁分别是lock和latch,本文主要介绍的是lock,也是我们常说的对于事务的锁。
latch
latch 一般称为闩锁(轻量级的锁) ,其应用对象为线程,保护的是内存数据结构。在InnoDB存储引擎中,latch有可以分为mutex(互斥锁)和rwlock(读写锁)其目的用来保证并发线程操作临界资源的正确性,并且没有死锁检测的机制,仅通过加锁顺序保证无死锁发生。
lock
lock的对象是事务,用来锁定的是数据库中的UI想,如表、页、行。并且一般lock对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同),此外lock正如大多数数据库中一样,是有死锁机制的。
行级锁与表级锁
在 InnoDB 中是支持多粒度的锁共存的,比如表锁和行锁。与Oracle不同,mysql的行锁是通过索引加载的,即是行锁是加在索引响应的行上的,要是对应的SQL语句没有走索引,则会全表扫描,
行锁则无法实现,取而代之的是表锁。
表锁:不会出现死锁,发生锁冲突几率高,并发低。
行锁:会出现死锁,发生锁冲突几率低,并发高。
锁的分类
共享锁和排他锁
共享锁和排他锁(Shared and Exclusive Locks)是标准的实现行级别的锁。举例来说,当给 select 语句应用 lock in share mode 或者 for update,或者更新某条记录时,加的都是行级别的锁。
与行级别的共享锁和排他锁类似的,还有表级别的共享锁和排他锁。如 LOCK TABLES ... WRITE/READ
等命令,实现的就是表级锁。我们可以看到,共享锁和排他锁的粒度并不一定必须是行级别,也可以为表级别。
共享锁
共享锁也叫S锁/读锁, 作用是锁住当前事务select的数据行,其他事务可以读这些数据行,但不能写。
使用:在查询语句后面显式增加 LOCK IN SHARE MODE
SELECT ... LOCK IN SHARE MODE;
排他锁
排他锁也叫X锁/写锁,作用是锁住事务中使用了排他锁的数据行,其他事务对这些数据行,既不能读也不能写。
使用:
1、MySql 的 InnoDB 引擎会为insert、update、delete操作中涉及的数据自动加排他锁(根据where条件语句)
2、对于一般的select语句,InnoDB不会加任何锁,可加FOR UPDATE,显式地加排他锁
SELECT ... FOR UPDATE;
ps.加过排他锁的数据行在其他事务中不能修改数据,也不能通过for update和lock in share mode的方式查询数据;但可以直接通过普通select …from…查询数据(但查到的只是已提交过的数据),因为普通查询没有任何锁机制。
意向锁
意向锁是表级锁,也可以分为意向共享锁 intention shared lock (IS) 和意向排他锁 intention exclusive lock (IX) . 但有趣的是,IS 和 IX 之间并不互斥,也就是说可以同时给不同的事务加上 IS 和 IX.
意向锁的目的就是表明有事务正在或者将要锁住某个表中的行。想象这样一个场景,我们使用 select * from t where id=0 for update;
将 id=0 这行加上了写锁。假设同时,一个新的事务想要发起 LOCK TABLES ... WRITE
锁表的操作,这时如果没有意向锁的话,就需要去一行行检测是否所在表中的某行是否存在写锁,从而引发冲突,效率太低。相反有意向锁的话,在发起 lock in share mode 或者 for update 前就会自动加上意向锁,这样检测起来就方便多了。
在实际中,手动锁表的情况并不常见,所以意向锁并不常用。特别是之后 MySQL 引入了 MDL 锁,解决了 DML 和 DDL 冲突的问题,意向锁就更不被提起来了。
锁的算法实现
Record Lock
record lock ,就是常说的行锁。InnoDB 中,表都以索引的形式存在,每一个索引对应一颗 B+ 树,这里的行锁锁的就是 B+ 中的索引记录。之前提到的共享锁和排他锁,就是将锁加在这里。
Gap Lock
Gap Locks, 间隙锁锁住的是索引记录间的空隙,是为了解决幻读问题被引入的。有一点需要注意,间隙锁和间隙锁本身之间并不冲突,仅仅和插入这个操作发生冲突。
Next-Key Lock
Next-key lock 是行锁(Record)和间隙锁的并集。在 RR 级别下,InnoDB 使用 next-key 锁进行树搜索和索引扫描。记住这句话,加锁的基本单位是 next-key lock.
加锁规则
规则包括:两个“原则”、两个“优化”和一个“bug”。
原则1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
原则2:查找过程中访问到的对象才会加锁。
优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
对优化 2 的说明:
从等值查询的值开始,向右遍历到第一个不满足等值条件记录结束,然后将不满足条件记录的 next-key 退化为间隙锁。
等值查询和遍历有什么关系?
在分析加锁行为时,一定要从索引的数据结构开始。通过树搜索的方式定位索引记录时,用的是"等值查询",而遍历对应的是在记录上向前或向后扫描的过程。
举例说明
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
场景1:主键索引等值间歇锁
session A: update t set d=d+1 where id=7; (T1)
session B: insert into t values(8,8,8); (T2)
session C: update t set d=d+1 where id=10; (T3)
T1 < T2 < T3
其中,B被阻塞,C查询正常。
我们可以看到,id 列为主键索引,并且 id=7 的行并不存在。
对于 Session A 来说:
根据原则1,加锁的单位是 next-key, 因为 id=7 在 5 - 10 间,next-key 默认是左开右闭。所以范围是 (5,10].
根据优化2,但因为 id=7 是等值查询,到 id=10 结束。next-key 退化成间隙锁 (5,10).
对于 Session B 来说:
插入操作与间隙锁冲突,所以失败。
对于 Session C 来说:
根据原则1,next-key 加锁 (5,10].
根据优化1:给唯一索引加锁时,退化成行锁。范围变为:id=10 的行锁
Session C 和 Session A (5,10) 并不起冲突,所以成功。
这里可以看出,行锁和间隙锁都是有 next-key 锁满足一定后条件后转换的,加锁的默认单位是 next-key.
场景2:非唯一索引等值锁
session A: select id from t where c=5 lock in share mode;(T1)
session B: update t set d=d+1 where id=5; (T2)
session C: insert into t values(7,7,7); (T3)
T1 < T2 < T3
其中,B查询正常,C被阻塞。
c为非唯一索引,查询的字段仅有 id,lock in share mode 给满足条件的行加上读锁。
Session A:
c=5,等值查询且值存在。先加 next-key 锁,范围为 (0,5].
由于 c 是普通索引,因此仅访问 c=5 这一条记录不会停止,会继续向右遍历,到 10 结束。根据原则2,这时会给 id=10 加 next-key (5,10].
但 id=10 同时满足优化2,退化成间隙锁 (5,10).
根据原则2,该查询使用覆盖索引,可以直接得到 id 的值,主键索引未被访问到,不加锁。
Session B:
根据原则1 和优化1,给 id=10 的主键索引加行锁,并不冲突,修改成功。
Session C:
由于 Session A 已经对索引 c 中 (5,10) 的间隙加锁,与插入 c=7 冲突, 所以被阻塞。
可以看出,加锁其实是在索引上,并且只加在访问到的记录上,如果想要在 lock in share mode 下避免数据被更新,需要引入覆盖索引不能包含的字段。
假设将 Session A 的语句改成 select id from t where c=5 for update;, for update 表示可能当前事务要更新数据,所以也会给满足的条件的主键索引加锁。这时 Session B 就会被阻塞了。
通过这两个场景,我们已经可以了解到对于索引加锁的规则,还有其他的场景大家可以通过底下的参考资料学习。
参考资料