我在技术面试的时候,经常会遇到如下场景。
我:"你好,同学,可以说下MySQL有哪些锁吗?"
候选人:"有行锁和表锁,乐观锁和悲观锁。"
我:"还有其他的吗?"
候选人:"还有共享锁和独占锁。"
我:"那,还有其他的吗?"
候选人:"还有记录锁、间隙锁、临键锁。"
我:"还有其他的吗?"
候选人有些面带惊慌:"这。。。这。。。"
其实,关于MySQL的锁,这个同学真的只说了冰山一角而已,下面我们来盘点一下,MySQL是如何进行锁分类的,以及还有哪些不为人知的锁。
锁分类
如上图所示,是我在网上看了很多文章,总结出来的一个我认为最为合理的一个分类方式了。大家如果觉得有更为合理的分类方式,我们也可以一起聊聊。
通过近期的面试情况得知,上图中相对"不为人知"的锁有:全局锁、意向锁、自增锁、页锁、元数据锁、插入意向锁和Latch,我们来一一地进行讲解。
全局锁
全局锁就是对整个数据库进行加锁,命令为:
arduino
flush tables with read lock (FTWRL)
加锁后,数据库处于只读状态,以下三类语句将会被阻塞:
- DML语句中的insert、update、delete操作;
- 所有DDL语句;
- 更新类事务的提交语句;
释放全局锁的命令如下:
unlock tables
全局锁的使用场景为:对全库进行逻辑备份,就是把数据库中所有表中的数据都读取出来,进行备份。
当然,通过全局锁的方式进行全库逻辑备份,缺陷还是很大的:
- 对主库使用全局锁进行备份,大概率会导致业务停滞;
- 对从库使用全局锁进行备份,又会有主从延迟的问题;
因此,我们需要一个更好的方案。
在InnoDB存储引擎默认的可重复读(Repeatable Read)事务隔离级别下,通过官方自带的逻辑备份工具mysqldump,并将参数设置为--single-transaction的时候,会在导数据之前就会启动一个事务,来确保拿到一致性视图。
而由于MVCC的支持,这个过程中数据是可以正常更新的。
官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数--single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
意向锁
为了便于理解,我们先来举个电商场景的例子。
事务A:某购物网站有用户下单,购买了一件id为10商品,此时需要进行库存扣减,SQL如下:
bash
update product set quantity = quantity -1 where id = 10;
那么,这条SQL会在id为10这条记录上,加上一个行锁。同时,数据库还会自动给事务A加上product表的意向(独占)锁。
事务B:恰好此时,该购物网站又到货了一批商品,需要把数据库中商品表的库存数,在现有的基础上全部加10。
SQL如下:
ini
update product set quantity = quantity +10;
这条SQL由于没有where条件,所以妥妥的会被加上表锁。
而需要给一个表加上表锁的时候,需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加锁成功。
如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则直接判断一次就知道表中是否有数据行被锁定了。
所以,意向锁的目的是为了提高加表锁的效率。
BTW:意向锁间是相互兼容的。
自增锁
我们通常会把数据库表中的主键设置成自增的,这是通过对主键字段声明 AUTO_INCREMENT 属性来实现的,之后在插入数据时,可以不指定主键的值,数据库会自动给主键赋上递增的值。
而自增锁正是实现此场景下的自增约束,当一个事务要插入新数据并获取下一个自增值时,它首先会获取一个自增锁。
一旦事务获得了自增锁,它就可以安全地执行插入操作,并确保每次插入都会得到一个唯一且连续(btw:是否连续,也要看参数设置)的自增值。其他事务在等待自增锁被释放之前,无法获取下一个自增值,从而避免了冲突和重复。
在InnoDB中,每个含有自增列的表都有一个自增长计数器(自动增长计数器仅被存储在主内存中,而不是存在磁盘上)。当对含有自增长计数器的表进行插入时,首先会执行
sql
select max(auto_inc_col) from t for update;
来得到计数器的值,然后再将这个值加1赋予自增长列,我们将这种方式称之为自增锁。
自增锁是一种特殊的表锁,它在完成对自增长值插入的SQL语句后立即释放,所以性能会比事务完成后释放锁要高。由于是表级别的锁,所以在并发环境下其依然存在性能问题。
从MySQL 5.1.22开始,InnoDB中提供了一种轻量级的内存mutex锁来实现自增长机制,每次分配自增长ID时,就通过估算插入的数量,然后更新mutex,下一个线程过来时从新mutex开始继续计算,这样就能避免传统模式非要等待每个都插入之后才能获取下一个,把锁降级到只在分配id的时候进行锁定。
同时InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,进而提高自增长值插入的性能。
插入类型
类型 | 说明 |
---|---|
Simple inserts | 插入的记录行数是确定的,比如:insert into values,replace但是不包括: insert ... on duplicate key update |
Bulk inserts | 插入的记录行数不能马上确定的,比如: insert ... select, replace ... select |
Mixed-mode inserts | 部分auto increment值给定或者不给定,如:insert into t1 (c1,c2) values (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d')或者: insert ... on duplicate key update |
innodb_autoinc_lock_mode参数
参数值 | 说明 |
---|---|
0(Traditional) | 此模式下,所有插入语句会获取一个表级自增锁,这个表级的自增锁在插入语句结束之后立即释放,而无需等到事务结束。它可以保证在一个insert里面的多行记录连续递增,也能保证多个insert并发情况下自增值是连续的。 |
1(Consecutive) | 此模式下,Simple inserts和Mixed-mode inserts采用mutex锁方式,Bulk inserts依旧采用表级自增锁方式。当然如果一个事务里已经持有表级的自增锁,那么后续的简单插入也需要等待这个表级的自增锁释放。 |
2(Interleaved) | 此模式下,所有插入语句都不会有表级自增锁,多个语句可以同时执行,所以在高并发插入场景下性能会比较好。 |
MySQL 8.0之前版本的默认值为1(Consecutive),而在MySQL 8.0版本,其默认值改为了2(Interleaved)。
空洞:即自增值是不连续的。
另外,在 0,1, 2 三种任何模式下,如果事务回滚,那么里面获得自增值的SQL回滚,但产生的自增值会一起丢失,不可能重新分配给其它insert语句,这也会产生空洞。
在Bulk inserts情景下,innodb_autoinc_lock_mode为0或1时,因为表级自增锁会持续到语句结束,同一时间只有一个语句在表上执行,所以自增值是连续的(其它事务需要等待),不会有空洞。
innodb_autoinc_lock_mode为 2 时,两个Bulk inserts之间可能会有空洞,因为每条语句事先无法预知精确的数量而导致分配过多的id,可能有空洞。
在混合插入场景下 ,innodb_autoinc_lock_mode为1或2时,如下SQL,mutex锁会按行分配4个id,但实际只用到2个,因此也会出现空洞。
sql
INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
结语
锁相关的内容比较抽象,想要讲得很明白并不容易,本期先讲这么多,下一期再讲讲页锁、全局锁、插入意向锁和Latch。