获取锁的流程

  1. 锁包含**行级锁**或是**表级锁**,当程序要进行读或写操作时,需要**先获取到对应的锁**,才能继续进行操作。
  2. 数据行在**一开始时没有任何锁**与其绑定
  3. 如果想要对数据行进行操作,需要先生成一个锁,锁中的主要内容有 事务的id,以及该锁是否在等待。
  4. 如果此时数据行没有锁与其绑定,则该锁会与数据行绑定,并且 标志位为没有在等待。
  5. 此时如果有其他想来操作这个数据行,则也需要生成一个自己的锁,因为该数据行此时已经绑定了一把锁,因此这个锁也会绑定在数据行上,但是此时该锁的状态是等待中,不能对数据行进行操作。
  6. 只有当第一个锁完成释放后,后面的锁才会进行竞争。

按照数据操作类型划分 读写锁

SELECT操作

  1. **读锁** 称之为 **S锁**,可以通过SELECT … **IN SHARE MODE** 进行添加,**多个S锁之间不会阻塞**
  2. **写锁** 称之为 **X锁**,可以通过SELECT… **FOR UPDATE** 进行添加,**S锁和X锁之间会阻塞**

DELETE操作

  1. 对于**删除一条记录**的时候,会**先获取到这个记录的X锁**,再进行操作
  2. 当删除**范围之间**的记录时,会先获取到**区间内所有记录的X锁**,并且会对记录之前加上**Next-key**

INSERT操作

  • 插入不需要提前获取锁,只需要在插入后增加一个**隐性锁**,保证该条记录不被其他事务所访问。
  • 隐性锁的本质也是临键锁,

UPDATE操作

  1. 如果**修改不涉及主键**,并且修改前后的**数据类型长度**没有发生改变,则获取到该行的**X锁**后,进行修改。
  2. 如果修改**涉及主键**,或者是修改前后的**数据类型发生**改变,则需要获取**X锁**,将该行记录的**删除标志位进行标记**,然后再**重新插入一条**新的记录。

按照锁的维度划分 表锁、页锁、行锁

表锁

表级别读锁(X锁)和写锁(S锁)
  1. LOCK TABLES t READ 获取表级别读锁
  2. LOCK TABLES t WRITE 获取表级别写锁
意向锁
  1. 当需要**给表添加读锁或写锁时**,因为读锁和写锁冲突的关系,**需要检查表中的所有页、所有行是否持有读锁和写锁**,这是一个比较耗时的操作。
  2. 因此为了改善这种情况,引入**意向锁**。在事务对其中的**行列进行操作**之前,需要先获取到**读写的意向锁**
  3. **意向锁****表级的读写锁**之间是**不兼容的****表级读锁****读意向锁**兼容,其余的都不兼容。如果事务A获取了意向锁,事务B想要对表进行加锁,此时会被阻塞。
  4. **意向锁****行级锁****兼容的**,事务A在操作某个数据行前先获取了意向锁,此时事务B想要对某个数据行进行操作,也会先获取意向锁,此时不会被阻塞。事务之间可以同时持有属于自己的表的读写意向锁。
自增锁

每当有一个 **auto increment**的字段需要获取值时,都需要先获取到这个自增锁。

  1. innodb_autoinc_lock_mode = 0(“传统”锁定模式) 每个INSERT语句会获取锁
  2. innodb_autoinc_lock_mode = 1(“传统”锁定模式) 批量只获取一个锁
  3. innodb_autoinc_lock_mode = 2(“交错”锁定模式) 可以同时进行 **仅保证总体增长**
元数据锁
  1. 在访问表时,需要加上元数据锁,读锁之间不阻塞,而读写锁之间冲突

行锁

mysql对表查询不加锁_意向锁

记录锁

仅锁一条记录
在聚簇索引中,只锁一条记录 SELECT * FROM xx WHERE id = 1 FOR UPDATE

间隙锁
间隙锁和临键锁是可重复读隔离级别中用于解决幻读问题的方法
因此在读已提交隔离级别中 只存在记录锁
锁范围,两边开区间
例子:1. id=5这条记录在列表中不存在,因此会对(3,8)的记录进行加锁
SELECT * FROM xx WHERE id = 5 FOR UPDATE
2. 指定范围
SELECT * FROM xx WHERE id > 3 AND id < 8 FOR UPDATE
3. 对于末尾的空间可以显示指定进行加锁
SELECT * FROM xx WHERE id > 20 FOR UPDATE
在加上间隙锁后,如果此时有一条id=4的记录想要插入,则会被阻塞。
阻塞的原理是会在插入时,会生成**插入意向锁**,本质也是一个**间隙锁**,此时会阻塞等待。临键锁
特殊的间隙锁,包含记录本身与间隙
SELECT * FROM xx WHERE id > 3 AND id <= 8 FOR UPDATE

死锁

mysql对表查询不加锁_加锁_02

死锁出现的条件

  1. 两个事务都**持有对方所需要的锁**
  2. 锁不会被**强占**
  3. **环路等待**
  4. 锁只能被一个事务所拥有

死锁如何解决

  1. **等待超时时间** 设定属性 **innodb_lock_wait_timeout** 如果事务等待获取锁的时间过长,则主动释放锁,但是对于在线服务来说,如果设置过长的时间,则会造成用户体验差,设置时间过短,则容易误伤到正常的锁。
  2. **主动判断逻辑是否有环路等待**

但是每个阻塞等待的锁都会主动判断是否存在死锁,因此不开启

  1. **通过合理的代码避免死锁**
  • 使用**读已提交**隔离级别代替**可重复读****没有临键锁和间隙锁**可以减少很多的死锁情况发生
  • 合理的**使用索引**,通过索引能够大幅减少需要加锁的行数,同时也减少了SQL的运行时间,让锁更快释放
  • 在一个事务内同时要执行**update和insert**操作,在不影响顺序的前提下,可以**先执行insert**操作,**后执行update**操作。因为update操作获取的**锁需要在事务结束后才会释放**,后进行update操作可以使得update操作持有锁的时间更短。

查看死锁

innodb_status_output_locks

加锁的规则

加锁规则

  1. 默认加锁加入的锁都是**临键锁**,临键锁是**前开后闭区间**。从左边的节点开始(不包含该节点),右边则会继续查询,直到找到第一个不等于该值的才会停止。
  2. 查询过程**只有访问到的对象**会进行加锁,其他索引上加的锁,如果有**回表**操作,最终也**会给主键索引加上锁**,如果普通索引的内容不需要进行回表操作,则不需要给主键索引加锁。
  3. **唯一索引**上的**等值查询**,临键锁会退化成记录锁。
  4. 普通索引上进行的查询,如果一直向右遍历找到的记录不相等,临键锁会退化成间隙锁。

加锁案例

在读已提交隔离级别下 只存在记录锁 不存在间隙锁和临键锁
  1. **主键索引**的情况下,只会对**条件记录中****主键索引**记录进行加锁
  2. **唯一索引**的情况下,会对**二级索引的记录****主键索引**进行加锁
  3. **普通索引**的情况下,会对**扫描出的所有记录**和这些记录对应的**主键索引**进行加锁
  4. 在没有索引的情况下,因为会进行**全表扫描**,因此会对所有记录进行加锁,但是MySQL有进行优化,在加锁后会判断是否符合条件,检查到不符合条件后**会进行解锁**
在可重复读隔离级别下 存在间隙锁和临键锁
  1. 在主键索引、唯一索引情况下与**读已提交**相同。
  2. 在普通索引的情况下,会对该条记录加上X锁,同时会对左右两边加上间隙锁
  3. 如果查询的条件没有索引的情况下,会进行全表扫描,会对所有记录加上X锁,并且给每个加上间隙锁
CREATE TABLE t5 ( id int(11) NOT NULL, c int(11) DEFAULT NULL, d int(11) DEFAULT NULL, PRIMARY KEY (id), KEY c (c)) ENGINE=InnoDB;
insert into t5 values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
当唯一索引等值查询,会查询前后的数值。在对id为6的记录加锁时,首先添加临键锁,即锁住(5,10】,但是因为6的下一个位置是10,与6不相同,因此临键锁退化成间隙锁(5,10)。
UPDATE T5 SET D = D + 1 WHERE ID = 6
在普通索引等值查询时,查询前后的数值。在事务A对id为5的记录加锁时,首先添加临键锁(0,5】。因为是普通索引因此会继续往下寻找,寻找到第一个数值,第一个数值是10,因此会给继续加临键锁(5,10】。但因为10不匹配,因此临键锁退化成为间隙锁(5,10)。因为事务A中没有进行回表,因为只有查询到的对象才会进行加锁,所以主键索引中没有锁,因此事务B不会被阻塞。而事务C中在插入id为6的记录时,因为有间隙锁(5,10)的存在,事务C会被阻塞。

mysql对表查询不加锁_网络_03

当唯一索引进行范围查询时,事务A首先根据id>=10,首先加入临键锁,即添加临键锁(5,10】,而因为是唯一索引进行优化,则最终临键锁退化成记录锁id=10。因为条件还有范围查询,因此会继续向右查询,直到查询到15,此时会添加临键锁(10,15】。因此在事务B中,插入id为12的记录时因为间隙锁的原因会被阻塞,而插入id为6的记录时,不会被阻塞。在事务C中插入id为15的记录时因为有临键锁的存在,此时事务C的更新语句也会被阻塞。

mysql对表查询不加锁_mysql对表查询不加锁_04

当普通索引进行范围查询时,事务A首先根据id>=10,首先添加临键锁(5,10】。因为条件中还有范围查询,找到下一条记录是15,此时会添加临键锁(10,15】。因此在事务B中对id=6的插入因为临键锁而阻塞。事务C中的对id=15的插入也会被阻塞。

mysql对表查询不加锁_加锁_05

唯一索引范围查询BUG,此时当id=15时首先会加入临键锁(10,15】,此时他应当是小于不应该继续往后查询,但因为有BUG,此时他继续往后查询,添加了临键锁(15,20】。因此事务B和事务C的SQL语句都会被阻塞。

mysql对表查询不加锁_网络_06

普通索引等值查询时,根据c=10,首先会加入临键锁(5,10】,然后会继续往后查找,直到找到第一个不等于该值的数据行,因此则是到(c=15,id=15),此时会添加临键锁(10,15】。因为该查询是等值查询,因此临键锁会退化成间隙锁(10,15)。因此事务B对id=13的插入会因为间隙锁而阻塞,而事务C对id=15的加锁不会被阻塞。

mysql对表查询不加锁_网络_07


mysql对表查询不加锁_mysql对表查询不加锁_08

LIMIT可以减少加锁的范围,首先根据id=10,先添加临键锁(5,10】,然后会继续往后查询,当查询到(c=10,id=28),此时已经满足到LIMIT 2的限制,因此不会继续往后查询,后面的也就不会进行加锁。因此事务B中对id=13的操作不会被阻塞。

mysql对表查询不加锁_加锁_09