MYSQL 5.7 InnoDB引擎 锁机制

全文主要内容

  1. MYSQL InnoDB引擎的锁类型以及特点
  2. 不同SQL语句的加锁情况
  3. 锁之间的兼容性关系
  4. 死锁发现
  5. 死锁分析
  6. 减少死锁发生以及死锁处理的解决方案

锁集合

Record Lock

A record lock is a lock on an index record.

这个就比较容易理解,就是记录锁。锁加在索引上,如果表没有设置索引。那么会加在Innodb的隐藏的聚集索引上。

在InnoDB的monitor中经常以这样的字眼出现,

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 10078 lock_mode X locks rec but not gap Record lock

Rec but not gap 代表的就是 记录锁了

Gap Lock

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。

间隙锁就是一种锁在记录与记录之间间隙的一种范围的锁,它是锁一定的范围。经常用于RR隔离级别下的防止幻读的解决方案。

如果索引是唯一索引,并且完全命中的话。也不会发生间隙锁,因为记录可以唯一确定。如果是多字段组合的唯一索引,然后where条件中只有部分字段命中索引,那么还是会发生GAP lock的。

总结一下,我认为发生间隙锁的核心是

是否不加间隙锁会导致出现多次读写的结果记录不一致

后面遇到具体的不同SQL语句加锁情况的时候,我也会再次提及这个核心条件。

关闭间隙锁的方式:

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the innodb_locks_unsafe_for_binlog system variable (which is now deprecated). In this case, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.

大概的意思就是说使用了RC的隔离级别就不会发生间隙锁了,因为RC允许幻读的存在,同时这个时候间隙锁只会发生在外键检查和主键重复检查的冲突中。

还有一个比较容易忽略的点,关于间隙锁的在扫描遇到不满足条件的记录的时候是否会释放。这个问题 不同的隔离级别所表现的形式也是不一样的。在MYSQL官网的手册中有说提到这个问题,实际上这个问题我之前也没有仔细去看。直到我看到了一个微信公众号的文章,也是关于锁的分组实验。我才发现了这个现象的存在。

原文如下:

There are also other effects of using the READ COMMITTED isolation level or enabling innodb_locks_unsafe_for_binlog. Record locks for nonmatching rows are released after MySQL has evaluated the WHERE condition. For UPDATE statements, InnoDB does a “semi-consistent” read, such that it returns the latest committed version to MySQL so that MySQL can determine whether the row matches the WHERE condition of the UPDATE.

在RC的隔离级别下,锁会在不匹配记录的情况下被释放。如果是当你要扫描的记录已经被锁住的情况下,那么你就会被阻塞。无论这个记录是否满足你的过滤条件,你可以理解成 MYSQL要判断你的记录符不符合要求的前提是拿到这条记录,但是这个时候记录已经被上锁无法获取,所以你只能阻塞住。

而在UPDATE的场景下,MYSQL做了一些优化,一种叫做"semi-consistent"的读操作,可以绕过阻塞。它会返回这条记录的最新版本给到MYSQL,让它去判断是否是满足条件的记录,如果是命中过滤条件的记录则会再次发起读操作。这个时候就要不是获取锁,要不就是被阻塞。

Next Key Lock

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

就是记录锁和间隙锁的组合。

一般是RR隔离级别中用来搜索和扫描索引的一种加锁方式。目的就是防止幻读

By default, InnoDB operates in REPEATABLE READ transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows

看到这里是不是对在面试过程中,经常被问到为什么MYSQL InnoDB的RR隔离级别就能完全保证事务的隔离性要求。而不像其他的数据库系统,可能需要 “串行化”的隔离级别才能达到?

原因就是Next Key Lock算法

Intention Lock

InnoDB supports multiple granularity locking which permits coexistence of row locks and table locks. For example, a statement such as LOCK TABLES ... WRITE takes an exclusive lock (an X lock) on the specified table. To make locking at multiple granularity levels practical, InnoDB uses intention locks. Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table.

MYSQL的InnoDB引擎为了实现多粒度锁使用的一种解决方案,就是“意向锁”。通过意向锁,事务会表达它准备对数据记录执行的操作动作。

一共有两种类型的意向锁,一种是IX intention exclusive lock,一种是IS intention share lock。

意向锁是表级锁的类型,只与表级的操作可能发生冲突。不会与任何行级操作发生冲突,同时意向锁之间也是兼容不冲突的

表级锁类型之间的兼容性关系表,行代表已经持有的锁类型,列代表即将要请求的锁类型。conflict代表冲突,compatible代表兼容。

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

Insert Intention Lock

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.

也是意向锁,只不过这个是叫做“插入意向锁”。但是它和刚刚的意向锁有一些不同,它是一种轻量级的Gap锁。主要目的是事务表达“插入”意向。

插入意向锁会和Gap锁互斥冲突,来解决幻读的问题。

比如事务1,在非唯一索引上执行了select * from table where column > 100 for update。此时column上加了Gap锁,而如果这个时候事务2,想要插入一条记录到Gap间隙中去,就会发生insert intention waiting的日志在InnoDB monitor 的output里面。

Shared And Exclusive Lock

InnoDB implements standard row-level locking where there are two types of locks, shared (S) locks and exclusive (X) locks.

这是InnoDB实现了两种类型标准的行级锁,一种是共享锁,一种是排它锁。这两个类型应该就是锁的两大基础类型。无论你是record lock 还是 gap lock 都有S或者X之分。

  • A shared (S) lock permits the transaction that holds the lock to read a row.
  • An exclusive (X) lock permits the transaction that holds the lock to update or delete a row.

共享锁允许事务共享,读取一行数据。排它锁允许事务取更新或者删除一行数据。

S和X锁的兼容性关系表格

行代表的是当前事务持有的锁类型,列代表的是事务想要请求的锁类型

S X
S 兼容 冲突
X 冲突 冲突

Auto_Inc Lock

An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.

自增锁是一种特殊的表级锁,它是为了保证不同并发事务之间能够正确的获取连续的自增ID值。所以加了表级的锁。

为了提供并发插入的性能,它实际上有三种锁模式,在不同的insert语句的操作下这些不同的锁模式会带来不一样的并发效果。

毕竟如果是表锁的话,我们平时项目如果高并发插入,肯定性能就会降低很多。可是平时也没什么感觉,这其中的奥秘就在于自增锁的几种模式。了解和熟悉之后,应该会明白很多。

MYSQL手册中先介绍了几种不同的插入statement的概念

INSERT-like statement

和插入相关的语句集合,特指下面全部的这些插入语句类型。

Simple inserts

这种的意思就是简单插入,插入的记录数可以在执行的时候提前知道。常见的比如 insert replace 等 直接插入给定数据的语句。

不包括 INSERT ... ON DUPLICATE KEY UPDATE的语句

Bulk inserts

批量插入,针对的是那些不能提前知道数据量的插入语句。比如INSERT ... SELECT, REPLACE ... SELECT, and LOAD DATA statements。

不包括语句中已经为每一行的主键列都指定了值的批量插入语句

比如这样的插入语句,t1表的主键列是id,然后t2表显示的给主键列进行了赋值。

insert into t1
(id,name)
select id,name from t2;
Mixed-mode inserts

混合插入,针对的是 使用了“simple insert”的插入,但是插入的过程中有的行显示的赋值了主键列,有的没有赋值。

比如:

INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');

还有一种是之前"simple insert"不包括的INSERT ... ON DUPLICATE KEY UPDATE。这种类型最坏的情况就是,插入后面跟一个更新,导致你分配的自增长值可能会被使用,也可能没有用处。

看到这样就真的很难受了,搞那么多花样。

然后介绍了三种不同的插入模式,通过变量innodb_autoinc_lock_mode进行控制

  • innodb_autoinc_lock_mode = 0` (“traditional” lock mode)

传统的插入模式,这种自增的插入模式就是会在statement执行的时候加上一个auto_inc的表级锁,能够很好的保证并发插入的主键自增的连续性。它是每一个statement生成一次自增值,然后在语句运行结束后就会释放锁,而不是等到事务的结束。并且在主从复制(statement based replication)的过程中,也能很好的保证从数据和主数据的主键自增的一致性。不过因此也会在并发事务插入的场景下,带来并发和伸缩能力的限制。

  • innodb_autoinc_lock_mode = 1 (“consecutive” lock mode)

这个是默认的加锁模式,它可以很好的平衡主键值的连续性与并发能力的限制。在"simple insert"的场景下,consecutive模式会使用轻量级的mutex互斥信号量来分配连续的自增主键值,它是在语句的开始阶段一次性将整个statement的自增值分配好,而不使用AUTO-INC的表级锁,达到了提高并发性能的目的。而在"bulk insert"的场景下还是保持和传统的锁模式一致,使用AUTO-INC表锁。如果是别的事务已经获取了AUTO-INC锁,那么当前事务无论是否"simple insert"都需要等待锁的释放。

优点:能够达到和traditional模式一样的自增连续性,同时保证了性能。但是有一种情况例外:mixed-mode inserts

  • innodb_autoinc_lock_mode = 2 (“interleaved” lock mode)

这种模式的锁是性能最好的,它是完全基于互斥信号量的方式来自增主键。并发的插入可能都不会连续。在基于语句的主从复制场景下,以及数据恢复下都不是安全的。因为自增的主键值可能完全不一样了。

不同的自增锁模式带来的可能结果,可以参考MYSQL手册。InnoDB AUTO_INCREMENT Lock Mode Usage Implications

不同SQL语句的加锁情况

加锁这个部分推荐一个大佬的PDF,有很好的图文讲解。是我从0到1理解加锁最清晰的就是这个资料了。读完之后 再去看手册的加锁会更加熟悉

MySQL 加锁处理分析.pdf

select ... from

is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE。select 基本查询会使用快照读,不会涉及到任何的加锁,除非使用了"串行化"的隔离级别,但是基本我们也不会使用这个隔离级别。所以可以简单的认为普通的读,无锁的干扰。

select.. for update 和 select ... lock in share mode

这两种读都会设置next key lock 到所有扫描的记录上,除非是使用了唯一索引,并且精确匹配时才使用record lock。for update 使用的是exclusive Lock 然后 share Mode 使用的是 shared lock。

update .... Where... 和 delete from ... where...

这两种也是使用了exclusive的next key lock 除非是唯一索引 并且精确匹配则使用record lock。

insert 和insert ... duplicate key on update

insert的话需要先获取一个insert intention lock 之后再获取一个record lock 即可。插入之间不同的记录不会阻塞。如果发生了duplicate key error 的主键冲突错误,会在主键索引上加 shared lock。这里也可能会导致死锁的发生。而duplicate key on update 和普通的插入基本一样,只不过在发生主键冲突时,它会使用一个exclusive lock。如果是主键冲突 就是 exclusive record lock 如果是 唯一索引冲突,那就是exclusive的next key lock了。

Replace

和insert是一样的加锁情况

锁之间的兼容性关系表

shared和exclusive:行代表持有的锁,列代表要请求的锁

S X
S 兼容 冲突
X 冲突 冲突

intention shared Lock 和 intention exclusive Lock:行代表持有的锁,列代表要请求的锁

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

死锁发现

死锁检测的开启

死锁的检测是通过 innodb_deadlock_detect variable 来控制的。如果不开启,则MYSQL使用 innodb_lock_wait_timeout 去回滚事务,防止死锁的发生。

查看最后一次死锁的情况,可以使用 SHOW ENGINE INNODB STATUS语句。如果死锁发生太频繁,开启 innodb_print_all_deadlocks 看到全部的死锁日志。

最后一次死锁的日志(SHOW ENGINE INNODB STATUS)类似如下:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-07-28 23:00:10 0x7f4725da9700
*** (1) TRANSACTION:
TRANSACTION 4810711, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 44, OS thread handle 139943553947392, query id 255173 172.18.0.1 root update
insert into bank_account_tab_00000000
    (account_number, `name`, gender,create_time)
    values
      
      ('5683761150', 'harris_NEW_DDDD', 'F', '2021-07-28 23:00:10.556'), ('5683799390', 'sad', 'M', '2021-07-28 23:00:10.556')
     
    on duplicate key update `name` = values(`name`),gender=values(gender)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2103 page no 11 n bits 344 index PRIMARY of table `harris`.`bank_account_tab_00000000` trx id 4810711 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) TRANSACTION:
TRANSACTION 4810710, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
7 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 42, OS thread handle 139943554488064, query id 255171 172.18.0.1 root update
insert into bank_account_tab_00000000
    (account_number, `name`, gender,create_time)
    values
      
      ('5685979070', 'KKKK', 'M', '2021-07-28 23:00:10.542'), ('5686017310', 'KKKK', 'M', '2021-07-28 23:00:10.542')
     
    on duplicate key update `name` = values(`name`),gender=values(gender)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2103 page no 11 n bits 344 index PRIMARY of table `harris`.`bank_account_tab_00000000` trx id 4810710 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2103 page no 11 n bits 344 index PRIMARY of table `harris`.`bank_account_tab_00000000` trx id 4810710 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

死锁日志分析

这里我拿了我自己遇到过的一次死锁的情况作为例子来分析,为什么会发生死锁。这个死锁的日志是我开启了innodb_print_all_deadlocks,然后存储到MYSQL的error日志中的一次死锁记录。通过show engine InnoDB status 也可以。

日志的开头,标记着一次死锁的发现。deadlock detected 代表 MYSQL 发现了一次死锁,并把detail information 记录在下面。

首先是transaction 1,黄色的部分transaction 1 正在等待的锁。锁的space page 以及是什么索引都标记了,是space id 为2111 页数为11的一个主键索引的记录锁。而事务id 5536910 lock mode 为 X的一个插入意向锁正在等待。我们大概可以知道这个事务打算要插入数据了,所以会先请求插入意向锁。锁的位置在 supremum,这个是MYSQL的一个特殊标记。标记着最大的一个记录Supremum Records

接着我们看transaction2,同样黄色的部分,有两块。一个是transaction 持有的锁 一个是它正在请求的锁。持有的锁是一个排他记录锁,请求也是请求这个记录的插入意向锁。

综上 我们可以得出结论,transaction1 在等待transaction2的supremum记录锁,transaction持有这个锁,同时也在请求这个supremum的锁。造成了死锁,循环等待的条件。

最后的红色部分 MYSQL 选择了回滚transaction1。

死锁日志

造成这次死锁的原因是:我的猜想是,两个语句都是在批量插入,事务2 先插入了一条记录,此时它会获取supremum的锁,这个时候事务1 也过来插入则要等待锁的释放。事务2的第二条语句开始插入也进入等待阶段。造成了事务2自己无法释放锁。

所以建议是尽量不要使用insert on duplicate key 因为它是性能比较差的一种语句。插入发生了主键错误时,会有next key lock的锁。

减少死锁发生的MYSQL手册推荐实践

  1. 发生了死锁,可以通过 SHOW ENGINE INNODB STATUS 去发现最近的一次死锁情况。不要慌
  2. 保持事务执行时间短,同时执行的语句小。这样可以减少事务的发生碰撞
  3. 尽快提交事务,也是为了减少事务碰撞
  4. 如果使用了当前读(locking read) 可以将隔离级别设置成RC

资源推荐

  1. 推荐何登成的GitHub中的database部分,何老师是数据库的专家。这个database部分,包含了死锁、索引以及各种内部原理的分析。并且锁这一块我自己看过,图文并茂,对锁的初步理解非常有帮助。GitHub链接
  2. 《高性能MYSQL》第三版 动物书 比较出名的讲解MYSQL的系统的书。
  3. MYSQL 的官方手册,对大部分的细节都有详细的描述。这篇文章的内容,都在这个章节InnoDB Locking and Transaction Model

你可能感兴趣的:(MYSQL 5.7 InnoDB引擎 锁机制)