疫情期间在家工做时,同事使用了 insert into on duplicate key update 语句进行插入去重,可是在测试过程当中发现了死锁现象:html
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
因为开发任务紧急,只是暂时规避了一下,可是对触发死锁的缘由和相关原理不甚了解,因而这几天一直在查阅相关资料,总结出一个系列文章供你们参考。本篇是上篇,主要介绍 MySQL 加锁原理和锁的不一样模式或类型的基本知识。后续会讲解常见语句的加锁状况和经过 MySQL 死锁日志分析死锁缘由。mysql
因为本篇文章涉及不少 MySQL 的基础知识,你们能够自行阅读我以前的 MySQL系列文章 《MySQL探秘》中的对应章节。sql
咱们首先来了解一下表锁和行锁:表锁是指对一整张表加锁,通常是 DDL 处理时使用;而行锁则是锁定某一行或者某几行,或者行与行之间的间隙。数据库
表锁由 MySQL Server 实现,行锁则是存储引擎实现,不一样的引擎实现的不一样。在 MySQL 的经常使用引擎中 InnoDB 支持行锁,而 MyISAM 则只能使用 MySQL Server 提供的表锁。并发
表锁由 MySQL Server 实现,通常在执行 DDL 语句时会对整个表进行加锁,好比说 ALTER TABLE 等操做。在执行 SQL 语句时,也能够明确指定对某个表进行加锁。性能
mysql> lock table user read(write); # 分为读锁和写锁 Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 100; # 成功 mysql> select * from role where id = 100; # 失败,未提早获取该 role的读表锁 mysql> update user set name = 'Tom' where id = 100; # 失败,未提早得到user的写表锁 mysql> unlock tables; # 显示释放表锁 Query OK, 0 rows affected (0.00 sec)
表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续须要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其余表,直到最后经过 unlock tables 释放全部表锁。测试
除了使用 unlock tables 显示释放锁以外,会话持有其余表锁时执行lock table 语句会释放会话以前持有的锁;会话持有其余表锁时执行 start transaction 或者 begin 开启事务时,也会释放以前持有的锁。优化
不一样存储引擎的行锁实现不一样,后续没有特别说明,则行锁特指 InnoDB 实现的行锁。spa
在了解 InnoDB 的加锁原理前,须要对其存储结构有必定的了解。InnoDB 是聚簇索引,也就是 B+树的叶节点既存储了主键索引也存储了数据行。而 InnoDB 的二级索引的叶节点存储的则是主键值,因此经过二级索引查询数据时,还须要拿对应的主键去聚簇索引中再次进行查询。关于 InnoDB 和 MyISAM 的索引的详细知识能够阅读《Mysql探索(一):B+Tree索引》一文。.net
下面以两条 SQL 的执行为例,讲解一下 InnoDB 对于单行数据的加锁原理。
update user set age = 10 where id = 49; update user set age = 10 where name = 'Tom';
第一条 SQL 使用主键索引来查询,则只须要在 id = 49 这个主键索引上加上写锁;第二条 SQL 则使用二级索引来查询,则首先在 name = Tom 这个索引上加写锁,而后因为使用 InnoDB 二级索引还需再次根据主键索引查询,因此还须要在 id = 49 这个主键索引上加写锁,如上图所示。
也就是说使用主键索引须要加一把锁,使用二级索引须要在二级索引和主键索引上各加一把锁。
根据索引对单行数据进行更新的加锁原理了解了,那若是更新操做涉及多个行呢,好比下面 SQL 的执行场景。
update user set age = 10 where id > 49;
上述 SQL 的执行过程以下图所示。MySQL Server 会根据 WHERE 条件读取第一条知足条件的记录,而后 InnoDB 引擎会将第一条记录返回并加锁,接着 MySQL Server 发起更新改行记录的 UPDATE 请求,更新这条记录。一条记录操做完成,再读取下一条记录,直至没有匹配的记录为止。
这种场景下的锁的释放较为复杂,有多种的优化方式,我对这块暂时尚未了解,还请知道的小伙伴在下方留言解释。
下面主要依次介绍 InnoDB 中锁的模式和类型,锁的类型是指锁的粒度或者锁具体加在什么地方;而锁模式描述的是锁的兼容性,也就是加的是什么锁,好比写锁或者读锁。
内容基原本自于 MySQL 的技术文档 innodb-lock 一章,感兴趣的同窗能够直接去阅读原文,原文地址为见文章末尾。
锁的模式有:读意向锁,写意向锁,读锁,写锁和自增锁(auto_inc),下面咱们依次来看。
读锁,又称共享锁(Share locks,简称 S 锁),加了读锁的记录,全部的事务均可以读取,可是不能修改,而且可同时有多个事务对记录加读锁。
写锁,又称排他锁(Exclusive locks,简称 X 锁),或独占锁,对记录加了排他锁以后,只有拥有该锁的事务能够读取和修改,其余事务都不能够读取和修改,而且同一时间只能有一个事务加写锁。
因为表锁和行锁虽然锁定范围不一样,可是会相互冲突。因此当你要加表锁时,势必要先遍历该表的全部记录,判断是否加有排他锁。这种遍历检查的方式显然是一种低效的方式,MySQL 引入了意向锁,来检测表锁和行锁的冲突。
意向锁也是表级锁,也可分为读意向锁(IS 锁)和写意向锁(IX 锁)。当事务要在记录上加上读锁或写锁时,要首先在表上加上意向锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就好了。
意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
AUTO_INC 锁又叫自增锁(通常简写成 AI 锁),是一种表锁,当表中有自增列(AUTO_INCREMENT)时出现。当插入表中有自增列时,数据库须要自动生成自增值,它会先为该表加 AUTO_INC 表锁,阻塞其余事务的插入操做,这样保证生成的自增值确定是惟一的。AUTO_INC 锁具备以下特色:
显然,AUTO_INC 表锁会致使并发插入的效率下降,为了提升插入的并发性,MySQL 从 5.1.22 版本开始,引入了一种可选的轻量级锁(mutex)机制来代替 AUTO_INC 锁,能够经过参数 innodb_autoinc_lock_mode 来灵活控制分配自增值时的并发策略。具体能够参考 MySQL 的 AUTO_INCREMENT Handling in InnoDB 一文,连接在文末。
下面是各个表锁之间的兼容矩阵。
总结起来有下面几点:
根据锁的粒度能够把锁细分为表锁和行锁,行锁根据场景的不一样又能够进一步细分,依次为 Next-Key Lock,Gap Lock 间隙锁,Record Lock 记录锁和插入意向 GAP 锁。
不一样的锁锁定的位置是不一样的,好比说记录锁只锁住对应的记录,而间隙锁锁住记录和记录之间的间隔,Next-Key Lock 则所属记录和记录以前的间隙。不一样类型锁的锁定范围大体以下图所示。
下面咱们来依次了解一下不一样的类型的锁。
记录锁是最简单的行锁,并无什么好说的。上边描述 InnoDB 加锁原理中的锁就是记录锁,只锁住 id = 49 或者 name = 'Tom' 这一条记录。
当 SQL 语句没法使用索引时,会进行全表扫描,这个时候 MySQL 会给整张表的全部数据行加记录锁,再由 MySQL Server 层进行过滤。可是,在 MySQL Server 层进行过滤的时候,若是发现不知足 WHERE 条件,会释放对应记录的锁。这样作,保证了最后只会持有知足条件记录上的锁,可是每条记录的加锁操做仍是不能省略的。
因此更新操做必需要根据索引进行操做,没有索引时,不只会消耗大量的锁资源,增长数据库的开销,还会极大的下降了数据库的并发性能。
仍是最开始更新用户年龄的例子,若是 id = 49 这条记录不存在,这个 SQL 语句还会加锁吗?答案是可能有,这取决于数据库的隔离级别。这种状况下,在 RC 隔离级别不会加任何锁,在 RR 隔离级别会在 id = 49 先后两个索引之间加上间隙锁。
间隙锁是一种加在两个索引之间的锁,或者加在第一个索引以前,或最后一个索引以后的间隙。这个间隙能够跨一个索引记录,多个索引记录,甚至是空的。使用间隙锁能够防止其余事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象。
值得注意的是,间隙锁和间隙锁之间是互不冲突的,间隙锁惟一的做用就是为了防止其余事务的插入,因此加间隙 S 锁和加间隙 X 锁没有任何区别。
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含
1五、1八、20 ,30,49,50 这几个值,可能的 Next-key 锁以下:
(-∞, 15],(15, 18],(18, 20],(20, 30],(30, 49],(49, 50],(50, +∞)
一般咱们都用这种左开右闭区间来表示 Next-key 锁,其中,圆括号表示不包含该记录,方括号表示包含该记录。前面四个都是 Next-key 锁,最后一个为间隙锁。
和间隙锁同样,在 RC 隔离级别下没有 Next-key 锁,只有 RR 隔离级别才有。仍是以前的例子,若是 id 不是主键,而是二级索引,且不是惟一索引,那么这个 SQL 在 RR 隔离级别下就会加以下的 Next-key 锁 (30, 49](49, 50)
此时若是插入一条 id = 31 的记录将会阻塞住。之因此要把 id = 49 先后的间隙都锁住,仍然是为了解决幻读问题,由于 id 是非惟一索引,因此 id = 49 可能会有多条记录,为了防止再插入一条 id = 49 的记录。
插入意向锁是一种特殊的间隙锁(简写成 II GAP)表示插入的意向,只有在 INSERT 的时候才会有这个锁。注意,这个锁虽然也叫意向锁,可是和上面介绍的表级意向锁是两个彻底不一样的概念,不要搞混了。
插入意向锁和插入意向锁之间互不冲突,因此能够在同一个间隙中有多个事务同时插入不一样索引的记录。譬如在上面的例子中,id = 30 和 id = 49 之间若是有两个事务要同时分别插入 id = 32 和 id = 33 是没问题的,虽然两个事务都会在 id = 30 和 id = 50 之间加上插入意向锁,可是不会冲突。
插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁惟一的做用就是防止其余事务插入记录形成幻读,正是因为在执行 INSERT 语句时须要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操做的执行。
不一样类型锁的兼容下以下图所示。
其中,第一行表示已有的锁,第一列表示要加的锁。插入意向锁较为特殊,因此咱们先对插入意向锁作个总结,以下:
其余类型的锁的规则较为简单:
下一篇文章咱们来看一下具体 SQL 的加锁分析和死锁日志分析,请小伙伴多多留意和关注,有问题的能够文章下方留言。