Innodb锁介绍
Innodb 锁
通过前面两节事务、MVCC介绍都出现了锁的影子,下面我们来看下Innodb中锁的内容
操作系统以及各个编程语言中都存在关于锁相关内容的介绍,锁的算法以及基础的原理是一致的,不同点主要体现在以下几点
数据库锁 | 操作系统/编程语言 | |
---|---|---|
目标对象 | 事务 | 线程 |
受保护对象 | 数据库行记录 | 内存数据结构/临界资源/共享变量 |
持续时间 | 事务生命周期,时间较长 | 代码块,时间较短 |
类型 | 表锁/行锁/意向锁 | 读写锁、互斥量 |
通过上表可以直观的看出来数据库锁与操作系统、编程语言中的锁区别,Mysql由C语言开发,运行与各个宿主机操作系统之上,因此在运行中的mysql实例中同时存在上述两种类型的锁,今天的主角是数据库锁
Mysql中锁的分类
按照大的维度进行分类
- 表锁,锁住整表,通常用于数据的初始化,大批量的数据迁移,一般是离线操作或者业务低峰时期
- 行锁,基于数据行的锁
- 意向锁,一种介于表锁于行锁之间锁类型,下文会介绍
按照读写类型进行分类
- 读锁/共享锁,亦称之为S锁
- 写锁/排它锁,亦称之为X锁
上述三种类型的锁,每种又有相对应的读锁于写锁,读写锁的兼容可以参见下表
操作类型 | S | X |
---|---|---|
S | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 |
注意,此处的读写锁兼容不要与上文MVCC章节中介绍的读写操作搞混,表锁以及行锁的读写锁操作类型兼容性均与上表一致,意向锁的兼容类型比较特殊,下文会介绍
来看一个表锁的示例
step | Tx | Session1 | Tx | Session2 |
---|---|---|---|---|
1 | tx1 | begin; | tx2 | begin; |
2 | tx1 | lock tables account read; | tx2 | select id,balance,username from account where id = 1; |
3 | tx1 | select * from account where id =1; | tx2 | insert into account values(NULL,10,'footer',now(),now()); |
4 | tx1 | insert into account values(NULL,10,'foobar',now(),now()); | tx2 | -- waiting until tx1 unlock tables; |
5 | tx1 | ERROR 1099 (HY000): Table 'account' was locked with a READ lock and can't be updated | tx2 | |
6 | tx1 | unlock tables; | tx2 | commit; |
通过上述示例可以知道
- 表锁在S锁的模式下,其他事务无法修改,只能进行读取,无法进行任何写入操作,参见上述读写锁的兼容性描述
- 表锁在S锁的模式下,当前事务亦不能进行数据的修改操作,参见上述step5 session1的错误输出
表锁的X锁模式与上述类似,不同的是在加X锁的情况下,其他的事务读操作会被阻塞(X锁于其他任何类型的锁都不兼容),此处不在放具体的示例,感兴趣的同学可以执行下述的语句
lock tables account write;
... do something for write operation
unlock tables;
意向锁
上文提到意向锁是一种介于行锁于表锁之间的锁定,我们知道innodb底层是基于数据页(16kb)的,一张表物理上实际由N多个16KB的数据页构成,带来的一个问题就是锁定上粗细粒度的问题,表锁过于粗,行锁某写场景下过于细,意向锁的出现就是为了支持在不同粒度上进行加锁,意向锁也分为读/写锁,通常用IS/IX指代,
举两个例子
- 若需要对记录上X锁,则需要对记录所属的库/表/页/记录先上意向锁IX,若中间有任何一个环节造成IX锁等待,那么该环节需要等待更粗粒度的锁完成后,释放相关的意向锁资源方可继续进行
- 若需要对记录上S锁,则需要对记录所属的库/表/页/记录先上意向锁IS,若中间有任何一个环节造成IS锁等待,那么该环节需要等待更粗粒度的锁完成后,释放相关的意向锁资源方可继续进行
需要注意的是,innodb支持的意向锁是表级别的(参见官方文档:mysql锁),意向锁与其他类型锁之间的兼容下可参见下表
操作类型 | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
可以看到X锁仍然是不与其他任何类型的锁兼容
行锁
Innodb 支持三种类型的行锁
- Record锁,最简单的基于锁定行
- Gap锁,锁定区间
- Next-Key锁,锁定区间,包括行记录本身
下述建表语句数据用于演示行锁的几种case
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),(10,10,10),(20,20,20),(30,30,30),(40,40,40),(50,50,50),(60,60,60);
Record锁是使用的比较多也是最简单的一种行锁,诸如下述语句使用的就是行锁
select * from t where id = 10 lock in share mode;
select * from t where id = 10 for update;
上述SQL会基于id=10的行主键生成行锁数据
GAP锁定
select * from t where id >10 and id<15 for update;
看下下述示例:
step | Tx | Session1 | Tx | Session2 |
---|---|---|---|---|
1 | tx1 | begin; | tx2 | begin; |
2 | tx1 | select * from t where id >10 and id<15 for update; | tx2 | insert into t values(8,8,8); -- Query OK, 1 row affected (0.00 sec) |
3 | tx1 | tx2 | insert into t values(21,21,21); -- Query OK, 1 row affected (0.00 sec) | |
4 | tx1 | tx2 | insert into t values(18,18,18); -- waiting until tx1 commit or rollback | |
5 | tx1 | commit; | tx2 | commit; |
step 2 tx1 session1 执行select * from t where id >10 and id<15 for update;
范围查询,mysql表中数据想为0,10,20,30,40,50,60,针对此范围查询,生成的GAP锁定区间为(10,20),前后均为开区间
Next-Key 锁定
select * from t where c >10 and c<=20 for update;
如下辅助索引示例
step | Tx | Session1 | Tx | Session2 |
---|---|---|---|---|
1 | tx1 | begin; | tx2 | begin; |
2 | tx1 | select * from t where id >10 and id<=20 for update; | tx2 | insert into t values(8,8,8); -- Query OK, 1 row affected (0.00 sec) |
3 | tx1 | tx2 | insert into t values(21,21,21); --waiting until tx1 commit or rollback | |
4 | tx1 | tx2 | insert into t values(31,31,31); -- Query OK, 1 row affected (0.00 sec) | |
update t set d=d+1 where c=30;--waiting until tx1 commit or rollback | ||||
5 | tx1 | commit; | tx2 | commit; |
step2 tx1 session1 执行select * from t where id >10 and id<=20 for update;
辅助索引范围查询,针对此查询的锁定区间为辅助索引(10,20],(20,30],session2 中的三个insert语句以及update语句可以验证猜想
一点总结
实际上,innodb在默认的RR隔离级别下,默认使用next-key锁定,满足一定条件会退化为行锁或者gap锁定,行锁住要针对主键/唯一索引,gap锁住要针对范围查询,上述两个示例是两个最简单的示例,实际上的锁定规则远比此复杂,索引唯一性、查找类型(等值or范围)、全表扫描、事务隔离级别等均有一定程度的关系,详细的示例可参见下述链接 http://www.heartthinkdo.com/?p=3741#22
三种行锁的规则为:
(1)Next-Key是默认锁。左开右的闭理解:右闭是行锁(一行记录),左开-右开是间隙锁,所以某种程度上Next-KEY=行锁+间隙锁
(2)行锁场景
- 主键、唯一索引上等值查询,对主键B+树上元素。
- 普通索引需要回表(等值&范围), 主键B+树上满足条件的元素都是行锁。
(2)间隙锁场景,Innodb在RR级别解决幻读就是靠间隙锁实现
- 主键等值查询,只有在未找到元素时候,需要对向右第一个元素加间隙锁。
- 主键范围查询时,无论是否有结果,需要向右第一个元素加Next-Key。
- 二级索引等值查询时,无论是否命中结果,需要对向右第一个元素加间隙锁。