MYSQL InnoDB 加锁分析

前言

理解 InnoDB 行锁,分析一条 SQL语句会加什么样的行锁,会锁住哪些数据范围对业务 SQL 设计和分析线上死锁问题都会有很大帮助。本文的目的是对InnoDB的事务锁模块做个简单的介绍。

隔离级别

InnoDB 中的 锁和当前的隔离级别有很大关系,这里简单介绍一下数据库事物的四种隔离级别一级每种隔离级别的数据读取问题。

Read uncommitted(读未提交)
  • 可以读未提交记录。
Read committed(读已提交)
  • 读取已提交数据,会存在幻读。
Repeatable read(可重复读)
  • 可重复读。当前读的时候,部分语句会加范围锁,保证当前读的可重复。
Serializable
  • 可串行化。不存在快照读,所有读操作都会加锁。

MYSQL InnoDB 加锁分析

MYSQL InnoDB 行锁分析

  • LOCK_REC_NOT_GAP
  • LOCK_GAP
  • LOCK_ORDINARY(Next-Key Lock)
  • LOCK_S(共享锁
  • LOCK_X(排他锁)
  • LOCK_INSERT_INTENTION(插入意向锁)
  • LOCK_PREDICATE(一种Page级别的锁)
  • 隐式锁

LOCK_REC_NOT_GAP

  • 锁带上这个 FLAG 时,表示这个锁对象只是单纯的锁在记录上,不会锁记录之前的 GAP。在 RC 隔离级别下一般加的都是该类型的记录锁。

LOCK_GAP

  • 表示只锁住一段范围,不锁记录本身,通常表示两个索引记录之间,或者索引上的第一条记录之前,或者最后一条记录之后的锁。可以理解为一种区间锁,一般在 RR 隔离级别下会使用到 GAP 锁。
  • 可以通过切换到 RC 隔离级别,或者开启选项innodb_locks_unsafe_for_binlog 来避免 GAP 锁。这时候只有在检查外键约束或者 duplicate key 检查时才会使用到GAP LOCK。

LOCK_ORDINARY(Next-Key Lock)

  • 所谓的 NEXT-KEY 锁,包含记录本身及记录之前的 GAP。当前MySQL 默认情况下使用 RR 的隔离级别,而 NEXT-KEY LOCK 正是为了解决RR隔离级别下的幻读问题。所谓幻读就是一个事务内执行相同的查询,会看到不同的行记录。在RR隔离级别下这是不允许的。

LOCK_S(共享锁)

  • 共享锁的作用通常用于在事务中读取一条行记录后,不希望它被别的事务锁修改。但所有的读请求产生的 LOCK_S 锁是不冲突的。

LOCK_X(排他锁)

  • 排他锁的目的主要是避免对同一条记录的并发修改。通常对于UPDATE或者DELETE操作,或者类似 SELECT…FOR UPDATE 操作,都会对记录加排他锁。

LOCK_INSERT_INTENTION(插入意向锁)

  • 插入意向锁,当插入索引记录的时候用来判断是否有其他事务的范围锁冲突,如果有就需要等待。如果一个session执行 insert into t1 values(1, 2, 3), (2, 5, 7), (3, 10, 9); 此时另一个session执行insert into t1 values(11, 9, 0)。就会调用插入意向锁来检查是否需要等待。同时插入意向锁之间并不冲突,在一个 GAP 锁上可以有多个意向锁等待。

LOCK_PREDICATE

  • 从 MYSQL5.7 开始 MYSQL 整合了 boost.geometry 库以更好的支持空间数据类型,并支持在在 Spatial 数据类型的列上构建索引,在 InnoDB 内,这个索引和普通的索引有所不同,基于 R-TREE的结构,目前支持对 2D 数据的描述,暂不支持3D.

  • R-TREE 和 BTREE 不同,它能够描述多维空间,而多维数据并没有明确的数据顺序,因此无法在 RR 隔离级别下构建 NEXT-KEY 锁以避免幻读,因此 InnoDB 使用称为 Predicate Lock 的锁模式来加锁,会锁住一块查询用到的被称为 MBR(minimum boundingrectangle/box) 的数据区域。 因此这个锁不是锁到某个具体的记录之上的,可以理解为一种 Page 级别的锁。

隐式锁

  • InnoDB 通常对插入操作无需加锁,而是通过一种“隐式锁”的方式来解决冲突。聚集索引记录中存储了事务 id。如果另外有个 session 查询到了这条记录,会去判断该记录对应的事务 id 是否属于一个活跃的事务,并协助这个事务创建一个记录锁,然后将自己置于等待队列中。该设计的思路是基于大多数情况下新插入的记录不会立刻被别的线程并发修改,而创建锁的开销是比较昂贵的,涉及到全局资源的竞争。

Repeatable-Read 隔离级别加锁分析

  • InnoDB 默认的隔离级别是 RR,我们结合 RR 级别进行加锁场景分析。
  • 分析使用的SQL:delete from t1 where id = 10
id 主键
  • 只需要将主键上,id = 10 的记录加上X锁即可。

MYSQL InnoDB 加锁分析

id唯一索引
  • 若 id 列是 unique 列,其上有 unique 索引。那么 SQL 需要加两个 X 锁,一个对应于 id unique 索引上的 id = 10 的记录,另一把锁对应于聚簇索引上的 [name=’d’,id=10] 的记录。

MYSQL InnoDB 加锁分析

id非唯一索引+RR
  • 通过 id 索引定位到第一条满足查询条件的记录,加记录上的 X锁,加 GAP 上的 GAP 锁,然后加主键聚簇索引上的记录 X 锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录 [11,f],此时,不需要加记录 X 锁,但是仍旧需要加 GAP 锁,最后返回结束。

MYSQL InnoDB 加锁分析

id无索引+RR
  • 如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有 GAP,杜绝所有的并发 更新/删除/插入 操作。

MYSQL InnoDB 加锁分析

总结

  • 本文学习了 InnoDB 行锁相关源码,并对 RR 隔离级别下加锁进行了分析,对应知识点可以用于帮助分析 SQL 语句加锁情况。上面分析过程也可以发现,相同的 SQL 在不同索引状态下加锁的情况不一样,执行的效率也不一样。