InnoDB中的锁

Posted by KANG's BLOG on Tuesday, March 22, 2022

一、锁的类型

Mysql可以按照粒度划分为表锁行锁

  • 表锁范围大,上锁慢,并发低
  • 行锁仅锁索引,上锁慢,并发高

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。

而行锁又分为排他锁共享锁

  • 共享锁(S):允许获得该锁的事务读取数据行(读锁),同时允许其他事务获得该数据行上的共享锁,并且阻止其他事务获得数据行上的排他锁。
  • 排他锁(X):允许获得该锁的事务更新或删除数据行(写锁),同时阻止其他事务取得该数据行上的共享锁和排他锁。

表锁和行锁同时存在就会带来一个问题,如果要申请表锁的时候如何确定当前表没有行锁呢?简单的方式当然是遍历查找,但这样效率太低,所以引入了“意向锁”(intention lock)。在添加行锁之前对表添加意向锁。

slideshare上对意向锁的一段表述:

IS and IX locks allow access by multiple clients. They won’t necessarily conflict until they try to get real locks on the same rows. But a table lock (ALTER TABLE, DROP TABLE, LOCK TABLES) blocks both IS and IX, and vice-versa.

翻译成中文:

IX(意向排他锁),IS(意向共享锁)是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突

除了上面提到的锁,还有一种,叫“间隙锁”。

二、间隙锁(Gap Locks)

什么是间隙锁

先来看看Mysql官网上的定义:

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.

翻译过来就是:间隙锁(Gap Lock)锁定的是索引记录之间的间隙、第一个索引之前的间隙或者最后一个索引之后的间隙

比如,当前数据库存在这样的一张person表,id是主键,age是普通索引,即二级索引:

id name age
1 张三 13
3 李四 14
4 王五 16

在事务A中有一项操作是查询id在1~3范围内的数据:

...
select * from person where id between 1 and 3
...

显而易见,应该返回张三李四这两条数据。

但如果在事务A尚未提交时,此时另一个事务B插入一条id为2的数据,并提交成功,那么当事务A结束后,会发现查询出来的数据(2条)少于数据库当前存在的数据数量(3条),这就产生了幻读的问题。

所以InnoDB引入间隙锁,来锁住1到3中所有的数据,这样不会有其他事务影响当前事务的范围操作。

这就是间隙锁的主要、唯一目的:阻止其他事务在间隙上插入记录

间隙锁的触发时机

  1. REPEATABLE READ或以上的隔离级别下
  2. locking reads,UPDATE和DELETE时,除了对唯一索引的唯一搜索外,都会获取gap锁或next-key锁。即锁住其扫描的范围。

为什么要锁住“第一个索引之前的间隙或者最后一个索引之后的间隙”呢?

再举个例子:

select * from person where age = '14' for update

这时如果另一个事务执行如下操作,会发现执行失败:

insert into person values(2, '赵六', 14)

这就引出另一个问题:数据间隙的分析时,优先通过二级索引排序,再通过二级索引中存储的主键排序

由于二级索引存放在B+树上,B+树的特点是不允许值重复,所以需要让主键共同参与二级索引值的构成来保证唯一性。

二级索引指向主键而不是数据地址,这样防止行移动或者数据页分裂时导致的二级索引维护工作。

所以,你的期望是锁住age为14的数据,但数据库不知道你要锁住的是14和哪个主键构成的二级索引,所以只能把id在1~3范围内的数据都锁住。

另外其间隙锁的区间是(1, 3],即大于1小于等于3。

Next-key 锁

相当于一个索引记录锁加上该记录之前的一个间隙锁。Next-key锁和MVCC共同作用下解决了幻读的问题。

三、Mysql数据上锁方式

1. 被动锁

对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁,即快照读

2. 主动锁

  • FOR UPDATE

    增加排他锁,即当前读,每次执行都会生成一个新的读视图

  • FOR SHARE

    增加共享锁