前一章提到了解决幻读的问题是加锁的方式,那就聊一聊MySQL锁相关的知识点。锁在面试中问的概率不是很大,了解一下相关的概念就行。
怎么用乐观锁解决并发问题?面试可能会被问的问题。
其中比较难理解的点是兼容性锁这块内容。核心需要要了解的是锁的几种模式。
Inorb 支持多粒度锁(multiple granularity locking)它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁。
5.5.3引入meta data lock锁,用于保证表中元素据的信息
会话A开启事务后,会自动获得一个MDL锁,B就不可以执行任何DDL语句的操作
引入MDL锁主要是为了解决两个问题:
事务隔离问题:比如在可重复隔离级别下,会话A在2次查询期间,会话B对表结构做了修改,两次查询结果就会不一致,无法满足可重复读的要求。
数据复制问题:比如会话A执行了多条更新语句期间,另外一个会话B做了表结构变更并且先提交,就会导致slave在重做时,先重做alter,再重做update时就会出现复制错误的现象。
共享锁: LOCK IN SHARE MODE
允许多个事务对同一个行进行加锁,因为他是共享锁
在除自己之外的其他事务持有锁时,UPDATE,DELETE操作会被阻塞
排他锁:FOR UPDATE
对读取的行记录加上写锁,其他事务的锁都会被阻塞
其实这里有一个很重要的概念:锁住的是索引,而非记录本身。(下文会举例说明这句话的意思)
当一条SQL查询条件中没有使用到索引而加锁,就会退化成表锁。 (重点, 先记住这个概念吧)
(至于MySQL是怎么做到的目前还没找到详细的原理,后面补上吧 -_-)
LOCK TABLES table READ 读锁锁表,会阻塞其他事务修改表数据。
LOCK TABLES table WRITE 写锁锁表,会阻塞其他事务读和写。
意向共享锁:在给一个数据行加共享锁前必须获取该表的IS锁
意向排他锁:在给一个数据行加排他锁前必须获取该表的IX锁
表级锁类型兼容性 (重点:表级)
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
为什么有意向锁?为什么要强调各个锁的兼容性?
InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。 (重点理解)
意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求
意向锁的作用是什么?
事务 A 获取了某一行的排他锁,并未提交:
SELECT * FROM user WHERE id = 1 FOR UPDATE;
此时 user 表存在两把锁:
这个时候事务 B 想要获取 user 表的共享锁:
LOCK TABLES user READ;
此时事务 B 发现事务 A 持有 user 表的意向排他锁,就可以知道事务 A 肯定持有该表中某些数据行的排他锁,那么事务 B 对 user 表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁。提高了效率。
意向锁是InnoDB自动加的,不需用户干预。所以我们对于意向锁的研究不用太过深入,面试中被问的概率也是很低的。
Record Lock:单条索引记录上加锁 。
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
间隙锁基于非唯一索引,它锁定一段范围内的索引记录
SELECT with FOR UPDATE or LOCK IN SHARE MODE,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB 也会对这个“间隙”加锁。
UPDATE和DELETE时,除了对唯一索引的搜索外都会获取gap锁或next-key锁。
在不通过索引条件查询时,InnoDB 会锁定表中的所有记录。所以,如果考虑性能,WHERE语句中的条件查询的字段都应该加上索引。
InnoDB通过索引来实现行锁,而不是通过锁住记录。因此,当操作的两条不同记录拥有相同的索引时,也会因为行锁被锁而发生等待。
要理解Gap Lock(间隙锁),需要看一个例子。
还是之前的表user(id PK, age KEY, name), age上加了一个普通索引,现在有数据如下:
那么该表中 age 列潜在的
Gap lock 有:(10,25), (25,36), (36,48)
Next-Key Lock有:(-∞, 10], (10, 25] ,(25, 36], (36, 48], (45, +∞)
Suppose that an index contains the values 10, 11, 13, and 20. The possible next-key locks for this index cover the following intervals, where a round bracket denotes exclusion of the interval endpoint and a square bracket denotes inclusion of the endpoint:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
(官方文档)
根据非唯一索引列锁住某条记录:
SELECT * FROM user WHERE age = 25 FOR UPDATE;
a. INSERT INTO user VALUES(100, 9, ‘李七’); 【不会阻塞】
b. INSERT INTO user VALUES(100, 10, ‘周九’); 【阻塞】
c. INSERT INTO user VALUES(100, 35, ‘吴十’); 【阻塞 】
d. INSERT INTO user VALUES(100, 36, ‘郑天’); 【不会阻塞 】
SQL最终的锁住的区间是什么?
If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.(官方文档)
WHERE age = 25 FOR UPDATE: 这个SQL锁住的区间是(10,36)
之前一直陷入了一个误区,上面说的Record Lock 、 Gap lock 、Next-Key Lock仅仅是个概念,真正加锁的方式是组合起来的。
首先:SQL获取了 (10, 25] 这个Next-Key Lock,因为插入age = 35被阻塞,说明还会加上下一个Gap lock,25的下一个Gap lock是(25,36)。所以最终锁定的是(10,36)这个区间。
但是发现插入age = 10的数据会阻塞,而插入36这个数据却不会阻塞,这是什么?
锁住的是索引,而非记录本身
锁住的是索引,而非记录本身
锁住的是索引,而非记录本身
重要的事说三遍
索引是有序存储的,age = 10这条数据的插入是会放到已经存在age = 10的索引之后,但是已经锁定了区间(10,36)不能插入,所以会被阻塞;而插入age = 36这条数据会放到原有age = 36索引的后面,自然不会被阻塞。 (没理解建议多看几遍)
如果改成 WHERE age = 24 FOR UPDATE: 这个SQL锁住的区间是(10,24]
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。
该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。
假设有索引记录,其值分别为4和7,两个不同的事务分别试图插入值值5和6,每个事务在获取插入行上排他锁前,都会获取4和7之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)。
插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待。
实际操作一下:
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; -- 事务A
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
mysql> START TRANSACTION; -- 事务B
mysql> INSERT INTO child (id) VALUES (101); -- 阻塞
--这是官方给的例子。
--结论:插入意向锁与Gap锁是不兼容的,所以,事务B的插入操作会被阻塞。(云里雾里,先不用深究。)
死锁是指由于每个事务都持有对方需要的锁而无法进行其他事务的情况。因为这两个事务都在等待资源变得可用,所以两个都不会释放它持有的锁。
MySQL本身处理死锁的方式
MySQL默认开启了死锁检测功能,InnoDB自动检测事务的死锁和回滚事务。
InnoDB尝试选择要回滚的小事务,其中事务的大小由插入,更新或删除的行数确定。
在高并发系统上,当多个线程等待相同的锁时,死锁检测会导致速度变慢。有时,禁用死锁检测并在innodb_lock_wait_timeout 发生死锁时依靠设置进行事务回滚可能会更有效 。
可以使用innodb_deadlock_detect 配置选项禁用死锁检测 。
怎么避免出现死锁
SHOW ENGINE INNODB STATUS命令以确定最近死锁的原因
保持事务小巧且持续时间短,以使事务不易发生冲突。
如果您使用锁定读取(SELECT … FOR UPDATE或 SELECT … FOR SHARE),请尝试使用较低的隔离级别,例如 READ COMMITTED。
修改事务中的多个表或同一表中的不同行集时,每次都要以一致的顺序执行这些操作。然后,事务形成定义良好的队列,并且不会死锁。
使用更少的锁定。允许 SELECT从一个旧的快照返回数据,不要添加条款FOR UPDATE或FOR SHARE给它。
死锁的详细案例
死锁文档
这个面试中也基本不会被问到,感兴趣的可以继续看看。
又要先了解几个名词:
简单插入
可以预先确定要插入的行数的语句(最初处理语句时)。这包括没有嵌套子查询的单行和多行INSERT以及REPLACE语句,但不包括INSERT … ON DUPLICATE KEY UPDATE。
批量插入
预先不知道要插入的行数(以及所需的自动增量值的数量)的语句。这包括 INSERT … SELECT, REPLACE … SELECT和LOAD DATA声明,但不是简单的 INSERT。在处理每一行时,InnoDB为AUTO_INCREMENT列分配一个新值。
混合模式插入
指定一些(但不是全部)新增行的自动增量值的“简单插入”语句。
另一种类型的“混合模式插入”是INSERT … ON DUPLICATE KEY UPDATE,在最坏的情况下实际上是INSERT 后跟了一个UPDATE操作,其中AUTO_INCREMENT列在UPDATE阶段期间可能使用或不使用分配值 。
对于AUTO-INC又有三种不同的配置参数,由innodb_autoinc_lock_mode 控制 。
traditional: innodb_autoinc_lock_mode = 0
在这种锁定模式下,所有“ INSERT ”的语句都将获得特殊的表级AUTO-INC 锁定,以将其插入具有 AUTO_INCREMENT列的表中。此锁定通常保持在语句的末尾(而不是事务的末尾),以确保为给定的INSERT 语句序列以可预测和可重复的顺序分配自动递增值,并确保自动递增值任何给定语句分配的都是连续的。
consecutive: innodb_autoinc_lock_mode = 1(默认模式)
批量插入使用特殊的AUTO-INC 表级锁并将其保持到语句结束
这适用于所有 INSERT … SELECT, REPLACE … SELECT和LOAD DATA语句
简单插入一次性插入值的个数可以立马得到确定,所以MySQL可以一次生成几个连续的值,用于这个INSERT语句;总的来说这个对复制也是安全的 (它保证了基于语句复制的安全)
这个模式的好处是auto_inc锁不要一直保持到语句的结束,只要语句得到了相应的值后就可以提前释放锁
interleaved: innodb_autoinc_lock_mode = 2
由于这个模式下已经没有了auto_inc锁,所以这个模式下的性能是最好的,而且自动增量值保证是唯一的;但是它也有一个问题,就是对于同一个语句来说它所得到的auto_incremant值可能不是连续的。
InnoDB AUTO_INCREMENT计数器初始化
如果AUTO_INCREMENT为InnoDB表指定列,则InnoDB数据字典中的表句柄 包含一个称为自动增量计数器的特殊计数器,该计数器用于为该列分配新值。该计数器仅存储在主存储器中,而不存储在磁盘上。
在MySQL 8.0中,此行为已更改。每次更改时,当前最大自动增量计数器值都会写入redo log,并保存到每个检查点的引擎专用系统表中。这些更改使当前的最大自动增量计数器值在服务器重新启动后保持不变。
在正常关闭后重新启动服务器时, InnoDB使用存储在数据字典系统表中的当前最大自动增量值初始化内存中自动增量计数器。
后面在写吧
锁的基本概念都差不多是这些,关于意向锁简单了解一下即可,乐观锁和悲观锁重点学习。
查看锁相关的SQL语句
select * from information_schema.innodb_locks\G;
select * from information_schema.innodb_trx\G;
select * from information_schema.innodb_lock_waits\G;
show engine innodb status\G;