首先准备一张表
CREATE TABLE hero (
number INT,
name VARCHAR(100),
country VARCHAR(100),
PRIMARY KEY (number),
KEY idx_name (name)
) ENGINE=InnoDB CHARSET=utf-8;
INSERT INTO hero VALUES
(1, 'l刘备', '蜀'),
(3, 'z诸葛亮', '蜀'),
(8, 'c曹操', '魏'),
(15, 'x荀彧', '魏'),
(20, 's孙权', '吴');
表中当前两个索引如下:
record lock
- 即记录锁,其作用就是仅仅把一条记录锁上
- 有 S 锁和 X 锁之分,X 锁与 X 锁互斥,S 锁与 S 锁兼容,S 锁与 X 锁互斥
gap lock
- 解决幻读的一种方式,仅仅是为了锁住幻影记录
- 官方名称为: LOCK_GAP,作用是锁住不存在的幻影记录(非真实表记录)
- 有共享 GAP 锁和独占 GAP 锁之分,但具体作用都是一样的
- 如果对一条记录加上 GAP 锁之分,并不影响对该记录加 Record LOCK
如何锁(20, +∞)呢?借助数据页中的伪记录
- Infimum 记录,表示该页面中最小的记录
- Supermum 记录,表示该页面中最大的记录
给 number=20 所在数据页中的 Supermum 记录加上 GAP LOCK,则可以锁住 (20, +∞),如图
Next-key lock
- 等于 GAP lock + Record lock
Insert Intention lock
- 很没用的一种锁,就是为了表达当前事务期望插入数据,并不阻止其他事务获取同样的锁
图中,T1 已获取 GAP 锁,T2 / T3 期望做修改操作,只能等待 T1 事务结束,释放锁
隐式锁
即根据 trx_id,其他事务帮忙加的锁
InnoDB 加锁步骤
-
步骤1. 定位扫描区间的第一条记录;
-
步骤2. 如果扫描区间是从右到左扫描,那么需要给扫描区间最右边的记录的下一条记录添加一个gap锁(在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下);
-
步骤3. 对于Infimum记录是不加锁的,对于Supremum记录加next-key锁(在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下);
-
步骤4. 对于精确匹配的扫描区间来说,当扫描区间中的记录都被读完后,需对扫描区间后的第一条记录加一个gap锁即可,并且向server层返回可结束本扫描区间的查询的信息(在隔离级别不小于REPEATABLE READ并且也没有开启innodb_locks_unsafe_for_binlog系统变量的情况下);
-
步骤5. 事务的隔离级别不大于READ COMMITTED,开启innodb_locks_unsafe_for_binlog系统变量,唯一性搜索并且该记录的 delete_flag 不为1,对于 >= 主键的这种边界条件来说,当前记录恰好是开始边界记录,则对记录加正经记录锁,否则添加next-key锁;
-
步骤6. 判断 ICP 条件是否成立。如果当前记录是二级索引记录,并且已经不在扫描区间中,则向server层返回可结束本扫描区间的查询的信息;
-
步骤7. 如果对二级索引记录进行加锁,还需要对相应的聚簇索引记录加正经记录锁(使用覆盖索引,并且加S型锁的记录可跳过此步骤);
-
步骤8. 判断当前记录是否已不在扫描区间中,如果不在的话,则向server返回可结束本扫描区间的查询的信息;
-
步骤9. 如果server层收到可结束本扫描区间的查询的信息,则结束本扫描区间的查询,否则继续向InnoDB要下一条记录,InnoDB根据记录所在的链表获取到下一条记录后,从步骤3开始新一轮的轮回。