上一篇文章中,我们介绍了 MySQL 中最基本的锁机制:
以及在此之上实现的全局锁与表级锁。
MySQL 锁机制(上) – 全局锁与表级锁
但事实上,Innodb 引擎实现了行级锁,与只支持表级锁的 MyISAM 相比,这显然能够有效减少锁冲突,这也是 Innodb 最终能够战胜 MyISAM 成为 MySQL 默认存储引擎的一个重要原因。
因此我们在使用中,最为频繁接触到就是行级锁,用好行级锁,减少锁冲突,将有效提升 MySQL 的执行性能,本文我们就来详细介绍一下 Innodb 中的各种行级锁。
按照锁定的范围不同,行级锁分为:
上述行级锁的加锁是 Innodb 自动进行的,我们可以通过某些 SQL 语句触发相应的加锁操作,但不能自由的实现加锁和解锁的动作。
和上篇文章中我们讲到的 MDL 锁一样,如果在事务中某些行或区间被加锁,那么只有到事务结束时(提交或回滚)才会自动进行解锁。
另外,这里提醒一句,innodb 通过 MVCC 实现了在可重复读事务隔离级别下不加锁实现快照读的机制,所以本文提到的所有行级锁,都不会影响到其它事务中的快照读。
那么,是在什么场景下进行加锁操作的呢?有以下场景:
针对上述所有操作,除了 select … lock in share mode 是加共享锁外,其他操作均为排它锁。
上面介绍到,记录锁就是对某行进行加锁,防止该行被其他操作修改或删除。
对于不存在的记录,Innodb 同样允许对其进行加锁,存储引擎首先创建一个隐藏的聚簇索引,然后将其记录为锁定状态。
例如下面的语句,当执行:
select * from test where dix_field = 2;
select * from test where dix_field in (2, 3, 4);
如果 idx_field 是主键或惟一键,就会锁定对应行记录的聚簇索引或隐藏的聚簇索引。
在读已提交隔离级别下,如果通过非主键或惟一键索引,会锁定查询过程中扫描到的每条记录,但在查询完成后,会自动释放未匹配的记录的锁。
记录锁锁定的是若干条行记录,间隙锁则锁的是若干个索引间的间隙,每个间隙都是两端开放的区间。
在一个数据表中,以主键、惟一键为间隔存在着很多个区间,这些区间如果被加锁,就被称为“间隙锁”。
间隙锁存在的目的是为了防止在事务执行过程中,另一个事务对间隙的插入,能够有效避免幻读的发生。
正是因为间隙锁的存在目的,所以多个事务可以同时对同一个间隙加锁,即使他们加的都是排它锁(事实上,考虑另一种常见情况,事务 1 持有间隙锁 (1, 3],事务 2 持有间隙锁 (3, 5),此时将记录 3 删除,那么事务 1 与事务 2 持有的间隙锁都将变成 (1, 5),如果强制间隙锁的互斥,那么这种情况下就会产生错误)
在读已提交与读未提交隔离级别下,Innodb 会自动禁用间隙锁。
下列场景下,innodb 会自动加间隙锁:
简单的来说,临键锁就是记录锁 + 间隙锁,也可以理解为特殊的间隙锁,他的区间是前开后闭的。
并发系统中不同线程出现对竞争资源的循环依赖并阻塞相互等待就会发生死锁。
例如事务 A 中,执行 update test set k = k + 1 where id = 1; 会锁定 id 为 1 的记录。
事务 B 中,执行 update test set k = k + 2 where id = 2; 会锁定 id 为 2 的记录。
此时,如果在事务 A 中执行 update test set k = k + 3 where id = 2; 同时在事务 B 中执行 update test set k = k + 4 where id = 1;
两个事务会分别阻塞等待另一个事务占用的排他锁,从而陷入死锁。
设置锁等待超时是最为简单粗暴的办法,innodb 提供了加锁阻塞超时时间的设置:innodb_lock_wait_timeout。
默认值是 50,即一个加锁请求在等待 50 秒后会自动返回加锁失败。
但这样存在几个问题:
该配置项的单位是秒数,因此他的最小粒度是 1 秒,对于有些系统,1 秒的超时显然太长,而另一些系统中,1 秒的超时又显得太短,难以区分是正常的锁等待还是发生了死锁,从而可能造成误伤。
innodb 提供了主动死锁检测机制,innodb 在锁冲突发生时,会扫描持有该锁或在竞争该锁的事务,判断他们之间是否有可能产生死锁,一旦发现当前事务的等待会产生死锁,那么就会立即返回错误。
可以通过 innodb_deadlock_detect 设置为 on 或 off 来开启或关闭主动死锁检测机制,默认是开启状态。
看上去主动死锁检测 + 业务重试可以解决所有的死锁问题了,但是这同样存在一定的问题。
由于整个主动死锁检测过程需要循环遍历所有持有或等待锁的事务两两间的持有锁情况,所以这个过程的时间复杂度是 O(n^2),在高并发的场景下,例如有 1000 个并发的线程同时更新一行,虽然他们之间并不会产生死锁,但主动死锁检测却要进行 100 万次对比,最终造成 CPU 利用率的飙高。
上述主动死锁检测引起性能问题的原因主要是单条记录加锁的并发度过高,但通常,我们不能靠降低系统的并发度来避免问题的发生,但我们可以通过横向或纵向拆分数据库中的字段来实现对并发加锁的优化。
例如,对于单纯用于递增记录的字段,我们可以拆分成多个字段,每次随机选取某个字段进行递增的记录。
这样虽然可以有效降低单个字段上的并发度,但依赖于实际的业务,如果业务场景同时存在增减操作,那么拆分成多个字段必须要考虑是否会将某个字段减到负数等问题,在很大程度上提升了业务逻辑的复杂度。
欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤。
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html。
https://dev.mysql.com/doc/refman/8.0/en/innodb-information-schema-understanding-innodb-locking.html。
https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html。