前言
本文主要是对MySQL锁的实现进行分析和总结。中间有很长一段是关于各种查询情况下的锁分析,并且搭配了完整的脚本和图例。如果您有兴趣,可以按照文章中的步骤进行验证。其实总结的内容不多,耐心看完,一起消除MySQL锁原理的迷雾。
锁具分类
下面将从不同角度对InnoDB锁进行分类。每种锁定模式(lock mode)都有对应的英文代号。锁定周期是事务的开始和提交。
共享锁和独占锁
从锁属性来看,可以分为排它锁(排除锁)和共享锁(共享锁):
排他锁(X):修改一行记录时,防止其他人同时修改。
我们想象N个人在同一张纸上写文章来类比独占锁的概念。大家写的文章必须是独立的,否则就会散乱不堪,难以阅读。为了保证这一点,我们获得了一张独一无二的优惠券,大家可以一起抢。只有拿到这张凭证的人才能在纸上写文章。有的人一边写,有的人就得等待,等别人写完之后还回凭证,然后再去抢凭证。
总结就是,当多个线程(N个人)修改同一个数据(在同一张纸上写一篇文章)时,为了保证并发数据安全,需要线程抢锁(写文章的人的凭证)是可以修改的。没有抢到锁的线程需要等待持有锁的线程释放锁(返回凭证)然后再次竞争。
共享锁(S):读取一行记录时,防止他人修改。
基于以上想象,我们考虑另一个需求。现在有N个人想要阅读这些写好的文章。因为阅读文章和写文章是两件事,所以我们单独创建了无限量阅读文章的优惠券。所有想阅读的人只有拿到这张凭证后才能查看。
阅读文章不会改变纸上的内容,因此没有限制谁可以阅读该文章,您可以随心所欲地阅读。
但是为了保证大家看到的内容是完整的,我们就得限制一下,等别人写完了才可以看;
另一方面,人们在阅读时不能更改纸上的内容,否则阅读时看到的内容将是不完整的。
综上所述,持有共享锁的N个线程不需要互相等待,可以并行查询数据;当一个线程持有共享锁时,想要获取排它锁的线程需要等待所有共享锁被释放;当存在排他锁时,想要获取共享锁的线程需要等待。这个思想其实和JDK的读写锁(ReentrantReadWriteLock)是一样的。
以下排它锁和共享锁 并发 等待时,互斥意味着需要等待其他锁释放:
但是是否加锁其实是由业务线程本身决定的,就像上面文章的阅读者和写者决定是否遵守这个规则一样。不遵守规则就会导致数据混乱。
InnoDB默认对SQL的增删改查执行锁定行为。对于查询语句,需要在末尾添加关键字来实现排他锁(FOR UPDATE)和共享锁(LOCK IN SHARE MODE)。
表级锁和行级锁
MySQL锁按照锁粒度可以分为表级锁和行级锁。锁粒度越小,资源锁定的范围越小,并发度越高,性能越高。锁粒度越大,反之亦然。
让我们继续上面想象的场景。由于有锁,同时只能一个人在纸上写文章,白天一篇一篇写的产出肯定会很低。事实上,很多人想在纸上写出完全不同的位置,而不是把它们混在一起。为了提高效率,把纸撕成N张,分不同的部分写。如果写相同的部分,就需要争夺凭证,分别阅读。这就是锁粒度从粗(一张纸)到细(分为N份)、表锁和行锁的区别。
意向锁加锁属性可以分为意向排它锁(IX)和意向共享锁(IS)。功能可以参考这篇文章掘金-MySql InnoDB中意向锁的功能[1]。
自增锁是一种特殊的表级锁,用在自增字段上。
本文主要关注行级锁,因此这部分内容略过。
记录锁定(锁定rec但不锁定gap):锁定指定索引条件;
间隙锁定(间隙):将间隙锁定在较小索引值的方向。比如有两个索引值10和20,20的gap锁范围是10
临时锁:记录锁+间隙锁,即锁定某行记录的索引,以及该索引与下一个索引的间隙。
通过测试分析MySQL在各种情况下是如何加锁的
至此,我们通过命令行登录MySQL数据库,来分析在可重复读和读已提交隔离级别下,各种情况下MySQL是如何加锁的。看来这背后的内容还真不少。其实,看了几种情况找到加锁规则后,你会发现太简单了。现在我们开始测试前的准备工作。
# 登录数据库
mysql -u root -p
# 打开输出锁定信息开关
set global innodb_status_output_locks = 1;
# 查询锁定信息开关
显示类似'%innodb的变量_status_output_locks%';
检查开关开启前后的状态。