InnoDB 七种锁

InnoDB 的索引

InnoDB 的索引有两类,聚集索引(Clustered Index)和普通索引(Secondary Index)。

InnoDB 的每个表都会有一个聚集索引。

  • 如果定义了 PK,则 PK 就是聚集索引。
  • 如果表没有定义 PK,则第一个非空 unique 列是聚集索引。
  • 否则,InnoDB 会创建一个隐藏的 row-id 作为聚集索引。

索引的结构是 B+ 树。

  • 在索引结构中,非叶子节点存储 key,叶子节点存储 value。
  • 聚集索引,叶子节点存储行记录(row)。
  • 普通索引,叶子节点存储了 PK 的值。

行锁的实现方式

InnoDB 行锁是通过给索引上的索引项加锁来实现的(Oracle 是通过在数据块中对相应数据行加锁来实现的)。InnoDB 这种加锁方式意味着:只有通过索引数据检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁。

同样由于是在索引上加锁,所以可能虽然访问的是不同行,但是使用的是相同的索引键,同样也会冲突。

当表有多个索引时,不同的事务可以使用不同的索引锁定不同的行,如果不同的索引碰巧都落到了同一行样,依然会阻塞。

自增锁(Auto-inc Lock)

自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入 AUTO_INCREMENT 类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有的其他事务的插入则必须等待,以便插入的行是连续的主键值。

共享/排他锁(Shared and Exclusive Locks)

  • 事务拿到某一行记录的共享锁,才可以读取这一行。
  • 事务拿到某一行记录的排他锁,才可以修改或者删除这一行。

其兼容互斥表如下

- S X
S 兼容 互斥
X 互斥 互斥

共享/排他锁实现了读读的并行。但是没有实现读写的并行。

意向锁(Intention Lock)

InnoDB 支持多粒度锁(multiple granularity locking),它允许行级锁和表级锁共存。实际应用中,InnoDB 使用的是意向锁。

意向锁是指,未来的某个时刻,事务要加共享/排他锁了,先提前声明一个意向。

意向锁有这样一些特点:

  • 意向锁是一个表级别的锁。
  • 意向锁分为
    • 意向共享锁(IS),表示事务有意向对表中的某些行加共享锁。
    • 意向排他锁(IX),表示事务有意向对表中的某些行加排他锁。
  • 意向锁协议
  1. 事务要获得某些行的 S 锁,必须先获得表的 IS 锁。
  2. 事务要获得某些行的 X 锁,必须先获得表的 IX 锁。
  • 由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不互斥,可以并行的。
  • 意向锁与共享/排他锁互斥

    #

    • | S | X
      —- | —- | —-
      IS | 兼容 | 互斥
      IX | 互斥 | 互斥

插入意向锁(Insert Intention Locks)

对已有数据行的修改和删除,必须加强互斥锁 X 锁,但是对于数据的插入,却不需要这么强的锁,所以产生了插入意向锁。

插入意向锁,是间隙锁(Gap Locks)的一种,所以也是加在索引上,它是专门针对 insert 操作的。

它的处理逻辑是:
多个事务,在同一个索引内,同一个范围区间插入记录时,如果插入的位置不冲突,就不会阻塞彼此。

记录锁(Record Locks)

记录锁,它封锁索引记录。例如

SELECT * FROM t WHERE id = 1 FOR UPDATE

它会在 id = 1 的索引记录上加锁,以防止其他事务插入,更新,删除 id = 1 的这一行。

间隙锁(Gap Lock)

当我们使用范围条件而不是相等条件检索数据,并请求共享锁或者排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁。对于键值在条件范围内但是不存在的记录,叫做“间隙”。InnoDB 也会对这个间隙加锁。这种锁机制就是间隙锁,它封锁索引记录中的间隔

假如 SQL

SELECT * FROM t 
WHERE id > 10;
FOR UPDATE

比如这个数据库记录 id 大于 10 的只有 11,12。那么 InnoDB 不仅会对这两个数据记录加锁,还会对 id > 12 的这个间隙加锁。

这里如果不防止,那正好有另一个插入 id = 12 的事务执行成功,则同一个 SQL 两次查询会得到不同的的结果集,产生幻影数据。

间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致不可重复读。

如果把事务的隔离级别降级为读提交,间隙锁会自动失效。

临键锁(Next-Key Lock)

记录锁与间隙锁的组合。它的封锁范围,即包含索引记录,又包含索引区间。

更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。

如果一个会话占有了索引记录 R 的共享/排他锁,其他会话不能立刻在 R 之前的区间插入新的索引记录。

如果一个表的记录是
id | name
—- | —-
1 | ace
3 | bob
5 | clark
9 | dan

PK 上潜在的临键锁为
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]

临键锁的主要目的,也是为了避免幻读。
如果把事务隔离级别降低到提交读,临键锁也会失效。

参考
插入InnoDB自增列,居然是表锁?
InnoDB,select为啥会阻塞insert?
InnoDB并发插入,居然使用意向锁?
连mysql锁的机制都不了解,怎么做架构师