暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

MySQL之锁详解(四):InnoDB的Insert Intention锁和AUTO-INC锁

GrowthDBA 2022-11-21
1109
距离上篇文章已几月有余,由于近期事情较多,公众号就被耽搁了,在这里给大家道个歉...
这是锁系列的第5篇文章,今天我们来继续学习InnoDB存储引擎的Insert Intention Lock(插入意向锁)和AUTO-INC Lock(自增锁),Let's GO!~

Insert Intention Lock(插入意向锁)




01

什么是插入意向锁

WHAT

我们先来看下MySQL官档中对Insert Intention Lock(插入意向锁)的描述(https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-insert-intention-locks)。

翻译一下这段话。大概意思是说:Insert Intention Lock(插入意向锁)是一种Gap锁(间隙锁遗忘了的同学可移步至MySQL之锁详解(三):InnoDB的Record锁、Gap锁和Next-Key锁),发生在Insert插入操作数据行之前。多个事务以同样的方式在同一个索引同一个范围区间插入记录时,如果插入位置不冲突,不会彼此阻塞。假设有值为4和7的索引记录。分别尝试插入值5和6的独立事务,每个事务在获得插入行上的排他锁之前,都会用Insert intention Lock(插入意向锁)锁定4和7之间的间隙,但不会阻塞彼此,因为行是不冲突的。

英文翻译过来还是有点绕,简单总结一下:Insert Intention Lock(插入意向锁)是一种Gap锁(间隙锁),多个事务中执行相同格式的插入语句在同一个索引同一个范围区间插入记录时,只要插入的记录互相不冲突,Insert Intention Lock(插入意向锁)间不会互相阻塞

02

插入意向锁是如何加锁的

HOW

为了方便说明,先做一下准备工作,我们就拿官档的例子来看下。
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> SELECT @@tx_isolation;

mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);

mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;

复制

我们先将当前的会话设置成了RR事务隔离级别,然后创建了一张child表,并插入id为90,102两行数据,将id>100的数据加上了X锁(独占锁)。接下来开启第二个会话:
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> SELECT @@tx_isolation;

mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);

复制

第二个会话,我们同样设置了RR事务隔离级别,然后插入id=101的数据,发现被阻塞了。看下加锁情况:

mysql> SHOW ENGINE INNODB STATUS\G
复制

通过锁信息我们可知。因第一个会话id>100 for update加了范围X锁(排他锁),第二个会话的事务是一个Insert语句,锁的类型是lock_mode X locks gap ... insert Intention(排他的间隙插入意向锁),又因Insert的值正好处在第一个事务的加锁范围内,所以第二个事务的发生了锁等待。所以,第二个会话会被阻塞。

当我们将第一个会话回滚(即释放了会话1的锁):

mysql> ROLLBACK;
复制

第二个会话被阻塞的SQL就可以正常提交了。然后,将第二个会话的事务也提交。重新开启两个会话、开启事务、同时插入不同的数据:

# 会话1:
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (111);
mysql> INSERT INTO child (id) VALUES (113);

# 会话2:
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (112);
mysql> INSERT INTO child (id) VALUES (114);

复制

发现两个会话的数据都可以正常插入,再来看一下此时锁的情况:
mysql> SHOW ENGINE INNODB STATUS\G

复制

你会发现,此时是没有锁的。至此得到结论:多个事务中执行相同格式的插入语句在同一个索引同一个范围区间插入记录时,只要插入的记录互相不冲突,就不会被阻塞,且不显示相关Insert Intention Lock(插入意向锁)信息。

继续,我们让两个会话同时插入相同的数据,看看会发生什么:

# 会话1:
mysql> INSERTINTOchild (id) VALUES (115);

# 会话2:
mysql> INSERTINTOchild (id) VALUES (115);

复制

发现会话2被阻塞了。正如官档中对Insert Intention Lock(插入意向锁)的描述:多个事务中执行相同格式的插入语句在同一个索引同一个范围区间插入记录时,只要插入的记录互相不冲突,插入操作就不会被阻塞,反之亦然。



隐式锁和显式锁




01

隐式锁

implicit-lock

通过上面官档给出的栗子🌰,我们证明了Insert Intention Lock(插入意向锁)的存在。但是在开启两个会话、同时开启事务、同时Insert插入不同数据的时候,除了IX(table-level)意向排他锁之外,是看不到Insert Intention Lock(插入意向锁)或者说任何锁信息的身影。这就有点奇怪了,既然Insert Intention Lock(插入意向锁)是存在的,为什么有时候又不显示锁信息呢?

带着上面的疑问,我们来看一个栗子🌰,同样使用官档中的child表,我们重新开启一个会话:

mysql> SET tx_isolation='REPEATABLE-READ';
mysql> SELECT @@tx_isolation;

mysql> BEGIN;
mysql> INSER TINTO child (id) VALUES (16);

mysql> SHOW ENGINE INNODB STATUS\G

复制

可以看到,数据正常插入了,但是事务未提交(事务处于活跃状态),同时看不到任何锁信息。然后我们开启另一个会话,执行下面的SQL:
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> SELECT @@tx_isolation;

mysql> BEGIN;
mysql> SELECT * FROM child WHERE id = 16 FOR UPDATE;

mysql> SHOW ENGINE INNODB STATUS\G

复制

这时发现,会话2的SELECT ... WHERE id=16 for update的操作被阻塞了,再来看下锁的情况:

通过锁信息,我们可以得到一些信息:
  • 第一部分:因为会话1执行Insert时是没有任何锁信息的,但是会话2的SELECT ... WHERE id=16 for update;语句正在等待id=16这行记录的锁,是不是感觉怪怪的(会话1本来没有锁,为什么会话2会被阻塞)?
  • 第二部分:阻塞会话2的这把锁被显示出来了,lock_mode X locks rec but not gap,这个锁看起来有点莫名其妙是不是?
其实,InnoDB存储引擎在这里做了优化,这个锁叫隐式锁(implicit-lock),INSERT INTO child (id) VALUES (16);这条记录不需要加锁,就知道上面有锁,因为这条记录对应的事务还没有提交,还处于活跃会话列表ReadView里面,代表上面肯定有锁。所以,InnoDB还会做内存上面的一些优化。第一点:就是我们之前学习过的锁是基于位图管理的;第二点:锁又分为显示锁(explicit-lock)和隐式锁(implicit-lock)。这也就说明了我们会话1在执行Insert操作时,没有看到锁信息的原因。

02

显式锁

explicit-lock

对于INSERT操作来说,一开始就是不创建锁的对象(其实加的是隐式锁),只有当发生等待的时候才会转换为显示的锁。

栗子🌰中id=16这条记录,在活跃事务列表(ReadView)中,说明没有提交,上面有锁。这时候再去创建锁的对象(延迟去创建锁的对象),在延迟过程中,没有对这条记录加锁的话,就不用创建锁的对象了,这样就可以节省内存。但是查看锁信息的时候还是没有出现insert intention lock(插入意向锁)的信息,而是显示lock_mode X locks rec but not gap的信息。这一点和文章刚开始显式的lock_mode X locks gap ... insert Intention(排他的间隙插入意向锁)信息有点差异,大家可以再回过头去对比一下。

为了便于大家理解,我再举一个栗子🌰,给大家看一下:

# Time1,事务1
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> BEGIN;

mysql> SELECT * FROM child WHERE id <= 100 FOR UPDATE;

# Time2,事务2
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> BEGIN;

mysql> INSERT INTO child (id) VALUES (95);

# Time3,事务1
mysql> COMMIT;

# Time4,事务2
mysql> INSERT INTO child (id) VALUES (95);成功插入

复制

Time2时,插入95会被阻塞,看下锁信息:

锁类型是lock_mode X locks gap before rec insert intention,阻塞了当前的记录。
Time3时,将事务1提交,Time4事务2成功插入。此时再看一下锁信息:

综上,我们来分析一下:
  • 首先会话1加的(93,100]的Next-Key Lock;
  • 然后会话2加的是:Gap + Insert Intention Lock(间隙插入意向锁);
  • 当会话1事务提交,锁释放,会话2就持有了这把锁,这把锁比较特殊,(95,100)的Insert Intention的Gap锁。
然后,我们开启会话3:
# Time5,事务3
mysql> SET tx_isolation='REPEATABLE-READ';
mysql> BEGIN;

mysql> INSERT INTO child (id) VALUES (96);成功插入

复制

可以看到,Time5事务3成功插入(即使会话2持有(95,100)的Insert Intention的Gap锁)。

03

插入意向锁小结

SUMMARY

Insertion Intention Lock(插入意向锁),就是为了提升并发插入能力的。

但是上述的Gap锁(95,100),应该是不能插入96的,但是这个锁比较特殊,是Gap Insert Intention Llock,插入是允许的。意义在于:提升了插入的性能,如果没有Insertion Intention Lock,那(95,100)就是Gap锁,如果持有的话,96就不能插入,性能就不好了。

Transaction2插入95,锁100的意义:首先Transaction1是(93,100]的Next-Key Lock,所以Transaction2插入95的时候就会被阻塞(因为95要在100上面加一个Gap锁,要知道自己能不能插入:一条记录能不能插入,就要看它插入记录后面的记录有没有锁,锁是否为Gap的,如果是,不能插。如果是Record锁,可以插入。),所以要插入95的时候,一定要在100上面加一个Gap锁。如果只加一个Gap锁,当Transaction1提交了,(93,100]锁释放了,100这上面的Gap锁锁住的就是(95,100)的Gap,如果插入96就不允许了。但是我加的是Gap Insert Intention Lock,就可以插入,这就是Gap Insertion Intention Lock存在的意义:用来判断我当前的事务能不能被插入,但是不会阻塞后面的插入操作。插入意向锁和插入意向锁之间是兼容的,只阻塞我当前线程的插入,不阻塞其他会话的插入




AUTO-INC Lock(自增锁)




01

什么是自增锁

WHAT

我们还是先来看下MySQL官档中对AUTO-INC Lock(自增锁)的描述(https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html)。

翻译总结一下:AUTO-INC锁(自增锁)是一种特殊的(table-level)表级锁,专门针对具有AUTO_INCREMENT属性(MySQL之字段属性)的字段数据插入时获得。同一张表,如果一个事务正在向表中插入值,那么任何其他事务都必须等待自己对该表进行插入,以便第一个事务插入的行接收连续的主键值。即事务1正在插入数据,事务2也插入数据,此时事务2会被阻塞,直到事务1释放AUTO-INC Lock(自增锁),其目的就是为了保证事务1插入的记录是连续的。

02

插入类型

TYPE

在讨论AUTO-INC Lock(自增锁)之前,需要对MySQL的插入类型进行简单的分类:
插入类型‍‍‍说明‍‍‍
Insert-likeInsert-like指所有的插入语句,如INSERT、REPLACE、INSERT ... SELECT、LOAD DATA等
Simple Inserts(简单插入)Simple Insert指能在插入前就确定插入行数的语句。这些语句包含INSERT、REPLACE等。需要注意的是:Simple Inserts不包含INSERT ... ON DUPLICATE KEY UPDATE这类SQL
Bulk Inserts(批量插入)打开表Bulk Inserts指在插入前不能确定得到插入行数的语句,如INSERT ... SELECT,REPLACE ... SELECT,LOAD DATA
Mixed-mode Inserts(混合插入)Mixed-mode Inserts指插入中有一部分的值是自增长的,有一部分是确定的。比如INSERT INTO table(id,name) VALUES(1,'zhangsan'),(2,'lisi'),(3,'wangwu');也可以是指INSERT ... ON DUPLICATE KEY UPDATE这类SQL语句

03

自增锁的模式

MODE

从MySQL 5.1.22版本开始,InnoDB存储引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。从这个版本开始,InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,共有3个可选项,即0,1,2。

innodb_autoinc_lock_mode‍‍‍说明‍‍‍
0MySQL 5.1.22版本之前的自增长的实现方式,即通过表锁AUTO-INC Lock方式
1该参数的默认值。对于“Simple Inserts”,该值会用互斥量(mutex)去对内存中的计数器进行累加的操作。对于“Bulk Inserts”,还是使用传统表锁AUTO-INC Lock方式。在这种配置下,如果不考虑回滚操作,对于自增值列的增长还是连续的,并且在这种方式下,Binlog格式为statement(SBR)的复制操作还是能很好的工作。需要注意:如果已经使用AUTO-INC Lock方式去产生自增长的值,而这时需要再进行“Simple Inserts”操作时,还是需要等待AUTO-INC Lock的释放
2在这个模式下,对于所有的“Insert-like”自增长的值的产生都是通过互斥量,而不是AUTO-INC Lock的方式。显然,这是最佳性能模式,但同时,也会带来一定问题。因为并发插入的问题,在每次插入时,自增长的值可能不是连续的。此外,最重要的是,基于statement(SBR)格式的复制会出现问题,因此,使用这个模式的复制,必须使用基于ROW(RBR)。这样才能保证最大的并发性能及主从一致
小提示
需要特别注意的是InnoDB存储引擎自增长的实现和MyISAM不同,MyISAM存储引擎是表锁设计,自增长不用考虑并发插入的问题。在InnoDB存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列。如果不是第一个列,MySQL会抛出异常,而MyISAM引擎没有这个问题。
mysql> CREATE TABLE a (
-> col1 INT AUTO_INCREMENT,
-> col2 INT,
-> KEY(col2,col1)
-> ) ENGINE = InnoDB;
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key
mysql>
mysql> CREATE TABLE b (
-> col1 INT AUTO_INCREMENT,
-> col2 INT,
-> KEY(col2,col1)
-> ) ENGINE = MyISAM;
Query OK, 0 rows affected (0.02 sec)

复制
innodb_autoinc_lock_mode有三种模式,不同模式下,分配自增值时可能存在不连续的情况,大家感兴趣的话可以下来自己做实验,就不再过多赘述了。



小结




今天我们学习了插入意向锁(Insert Intention Lock)、自增锁(AUTO-INC Lock)、隐式锁(Implicit Lock)、显示锁(Explicit Lock),下面做一个简单的总结 :
  • Insert Intention Lock(插入意向锁)是一种隐式的Gap锁,只有当发生锁冲突时,才会将隐式锁转为显示锁;
  • 多个事务中执行相同格式的插入语句在同一个索引同一个范围区间插入记录时,只要插入的记录互相不冲突,就不会被阻塞,因为隐式锁的原因,不会显示相关锁信息;
  • Insertion Intention Lock(插入意向锁),就是为了提升并发插入能力的。用来判断我当前的事务能不能被插入,但是不会阻塞后面的插入操作。插入意向锁和插入意向锁之间是兼容的,只阻塞我当前线程的插入,不阻塞其他会话的插入;
  • AUTO-INC锁(自增锁)是一种特殊的(table-level)表级锁,专门适用于具有AUTO_INCREMENT属性字段数据插入操作时;
  • AUTO-INC Lock(自增锁)保证了同一张表,同一个事务种插入自增数据的连续性,在此期间,任何其他事务都必须等待自己操作完成才可继续操作;
  • 参数innodb_autoinc_lock_mode用来控制自增长的模式,可选参数1,2,3,设置为不同值时,可能存在自增值不连续的情况。
今天的内容就到这里吧,站在巨人的肩膀上,每天进步一点点!~



参考资料

  • 姜承尧《MySQL技术内幕:InnoDB存储引擎 第2版》

  • https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html



扫描二维码关注

获取更多精彩

GrowthDBA


end


文章转载自GrowthDBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论