MySQL锁之行表锁,共享排他锁,悲观乐观锁,记录间隙意向锁

1 MySQL锁

1.1 Mysql锁分类

在这里插入图片描述

Mysql中锁的分类按照不同类型的划分可以分成不同的锁:

按照 锁的粒度 划分可以分成:

  • 行锁
  • 表锁
  • 页锁

按照 使用方式 划分可以分为:

  • 共享锁
  • 排它锁

按照 思想 的划分:

  • 乐观锁
  • 悲观锁

1.2 行锁表锁页锁和存储引擎锁机制

1.2.1 行锁表锁页锁

1.2.1.1 行锁

描述:行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。
行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁

特点:开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

1.2.1.2 表级锁

描述:表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持

最常使用的MyISAMInnoDB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)

特点:开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度也最低

1.2.1.3 页级锁

描述:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。
因此,采取了折中的页级锁,一次锁定相邻的一组记录。BDB支持页级锁

特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

1.2.2 存储引擎锁机制

MyISAMMEMORY采用表级锁(table-level locking)
BDB采用页面锁(page-level locking)表级锁,默认为页面锁
InnoDB支持行级锁(row-level locking)表级锁,默认为行级锁

InnoDB存储引擎有几种锁算法 :

  • Record Lock : 单个行记录上的锁(锁数据,不锁Gap)
  • Gap Lock : 间隙锁,锁定一个范围,不包括记录本身;
  • Next-Key Lock : 锁定一个范围,包括记录本身
    其实 Next-KeyLocks=Gap锁+ Recordlock锁

如果查询条件的是唯一索引,或者主键时,Next-Key Lock会降为Record Lock。如果是普通索引,将对下一个键值加上gap lock,其实就是对下一个键值的范围为加锁。gap lock间隙锁,就是为了解决幻读问题而设计出来的

1.3 共享锁排他锁

不论是行级锁还是表级锁,都存在共享锁(Share Lock,S锁)和排他锁(Exclusive Lock,X锁)

1.3.1 共享锁 Shared Locks(S锁)

描述:共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁不能加排他锁。获取共享锁的事务只能读数据,不能修改数据。
使用用法:

SELECT … LOCK IN SHARE MODE;

在查询语句后面增加LOCK IN SHARE MODEMySQL 就会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。

其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

1.3.2 排他锁 Exclusive Locks(X锁)

描述:排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。

使用用法:

SELECT … FOR UPDATE;

在查询语句后面增加FOR UPDATEMySQL 就会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的. 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以

兼容性 S X
S 兼容 不兼容
X 不兼容 不兼容

1.3.3 select for update分析

1.3.3.1 select for update加的是表锁还是行锁

这道面试题,一般需要分两种数据库隔离级别(RR可重复读和RC已提交读),还需要分查询条件是唯一索引、主键、一般索引、无索引等几种情况

  • 在RC隔离级别下:
    • 如果查询条件是唯一索引,会加IX意向排他锁(表级别的锁,不影响插入)、两把X排他锁(行锁,分别对应唯一索引和主键索引)
    • 如果查询条件是主键,会加IX意向排他锁(表级别的锁,不影响插入)、一把对应主键的X排他锁(行锁,会锁住主键索引那一行)。
    • 如果查询条件是普通索引,如果查询命中记录,会加IX意向排他锁(表锁)、两把X排他锁(行锁,分别对应普通索引的X锁,对应主键的X锁)
      如果没有命中数据库表的记录,只加了一把IX意向排他锁(表锁,不影响插入)
    • 如果查询条件是无索引,会加两把锁,IX意向排他锁(表锁)、一把X排他锁(行锁,对应主键的X锁)。
      查询条件是无索引,为什么不锁表呢,MySQL会走聚簇(主键)索引进行全表扫描过滤。每条记录都会加上X锁。但是,为了效率考虑,MySQL在这方面进行了改进,在扫描过程中,若记录不满足过滤条件,会进行解锁操作
  • 在RR隔离级别
    • 如果查询条件是唯一索引,命中数据库表记录时,一共会加三把锁:一把IX意向排他锁 (表锁,不影响插入),一把对应主键的X排他锁(行锁),一把对应唯一索引的X排他锁 (行锁)。
    • 如果查询条件是主键,会加IX意向排他锁(表级别的锁,不影响插入)、一把对应主键的X排他锁(行锁,会锁住主键索引那一行)。
    • 如果查询条件是普通索引,命中查询记录的话,除了会加X锁(行锁),IX锁(表锁,不影响插入),还会加Gap 锁(间隙锁,会影响插入)。
    • 如果查询条件是无索引,会加一个IX锁(表锁,不影响插入),每一行实际记录行的X锁,还有对应于supremum pseudo-record的虚拟全表行锁。这种场景,通俗点讲,其实就是锁表了。

1.3.3.2 如何使用数据库分布式锁

一般可以使用select ... for update来实现数据库的分布式锁。
它的优点是:简单,使用方便,不需要引入Redis、zookeeper等中间件。
缺点是:不适合高并发的场景,db操作性能较差。

1.3.3.3 一条SQL加锁情况分析

一条SQL加锁,可以分9种情况进行:

  • 组合一:id 列是主键,RC 隔离级别
  • 组合二:id 列是二级唯一索引,RC 隔离级别
  • 组合三:id 列是二级非唯一索引,RC 隔离级别
  • 组合四:id 列上没有索引,RC 隔离级别
  • 组合五:id 列是主键,RR 隔离级别
  • 组合六:id 列是二级唯一索引,RR 隔离级别
  • 组合七:id 列是二级非唯一索引,RR 隔离级别
  • 组合八:id 列上没有索引,RR 隔离级别
  • 组合九:Serializable 隔离级别

1.3.4 lock tables和unlock tables

1.3.4.1 定义

在MySQL中提供了锁定表(lock tables)和解锁表(unlock tables)的语法功能,ORACLE与SQL Server数据库当中没有这种语法

锁定表的语法:

LOCK TABLES
tbl_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}
[, tbl_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}] ...

LOCAL修饰符表示可以允许在其他会话中对在当前会话中获取了READ锁的的表执行插入。但是当保持锁时,若使用Server外的会话来操纵数据库则不能使用READ LOCAL。另外,对于InnoDB表,READ LOCALREAD相同。也就是说READ LOAL仅仅是MyISAM类型表才有的功能

解锁表的语法:

UNLOCK TABLES

LOCK TABLES为当前会话锁定表。 UNLOCK TABLES释放被当前会话持有的任何锁

1.3.4.2 lock table读锁定

如果一个线程获得在一个表上的read锁,那么该线程和所有其他线程只能从表中读数据,不能进行任何写操作。
下边我们测试下,测试表为user表。
不同的线程,可以通过开多个命令行MySQL客户端来实现,当用不同客户端可以通过命令SELECT CONNECTION_ID()来查看会话连接

示例序号
线程A(命令行窗口A)
线程B(命令行窗口B)

示例序号1
mysql> lock tables user read;
Query OK, 0 rows affected (0.00 sec)
mysql>
对user表加读锁定。
示例序号2
mysql> select * from user;
+——+———–+
| id | name |
+——+———–+
| 22 | abc |
| 223 | dabc |
| 2232 | dddabc |
| 45 | asdsagd |
| 23 | ddddddddd |
+——+———–+
5 rows in set (0.00 sec)
mysql>
自己的读操作未被阻塞
mysql> select * from user;
+——+———–+
| id | name |
+——+———–+
| 22 | abc |
| 223 | dabc |
| 2232 | dddabc |
| 45 | asdsagd |
| 23 | ddddddddd |
+——+———–+
5 rows in set (0.00 sec)
mysql>
其他线程的读也未被阻塞

示例序号3
mysql> insert into user values(12,’test’);
ERROR 1099 (HY000): Table ‘user’ was locked with a READ lock and can’t be updated
mysql>
发现本线程的写操作被阻塞
mysql> insert into user values(22,’2test’);
发现没有任何反应,一直等待中,说明没有得到写锁定,一直处于等待中。

示例序号4
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
mysql>
释放读锁定。
mysql> insert into user values(22,’ddd’);
Query OK, 1 row affected (1 min 27.25 sec)
mysql>
在线程A释放读锁后,线程B获得了资源,刚才等待的写操作执行了。

示例序号5
mysql> lock tables user read local;
Query OK, 0 rows affected (0.00 sec)
mysql>
获得读锁定的时候增加local选项。
mysql> insert into user values(2,’b');
Query OK, 1 row affected (0.00 sec)
mysql>
发现其他线程的insert未被阻塞。

mysql> update user set name = ‘aaaaaaaaaaaaaaaaaaaaa’ where id = 1;
但是其他线程的update操作被阻塞了。

注意:user表必须为Myisam表,以上测试才能全部OK,如果user表为innodb表,则lock tables user read local命令可能没有效果,也就是说,如果user表为innodb表,第5 将不会被阻塞,这是因为INNODB表是事务型的,对于事务表,例如InnoDB和BDB,–single-transaction是一个更好的选项,因为它不根本需要锁定表

1.3.4.3 lock table写锁定

如果一个线程在一个表上得到一个 WRITE 锁,那么只有拥有这个锁的线程可以从表中读取和写表。其它的线程被阻塞。
写锁定的命令:lock tables user write,user表为Myisam类型的表。
参考如下测试:

示例序号
线程A(命令行窗口A)
线程B(命令行窗口B)

示例序号1
mysql> lock tables user write;
Query OK, 0 rows affected (0.00 sec)
对user表加写锁定。

示例序号2
mysql> select * from user;
+—-+———————–+
| id | name |
+—-+———————–+
| 1 | aaaaaaaaaaaaaaaaaaaaa |
| 2 | b |
+—-+———————–+
2 rows in set (0.00 sec)
自己可以继续进行读操作
mysql> select * from user;
其他线程读操作被阻塞。

示例序号3
mysql> unlock tables ;
Query OK, 0 rows affected (0.00 sec)
释放锁定。

示例序号4
mysql> select * from user;
+—-+———————–+
| id | name |
+—-+———————–+
| 1 | aaaaaaaaaaaaaaaaaaaaa |
| 2 | b |
+—-+———————–+
2 rows in set (32.56 sec)
其他线程获得资源,可以读数据了。

1.4 悲观锁与乐观锁

1.4.1 乐观锁

乐观锁(Optimistic Lock):假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。

乐观锁, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用version版本号/时间戳等机制,一般配合CAS算法实现

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

1.4.2 悲观锁

悲观锁(Pessimistic Lock):假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
select…for update就是MySQL悲观锁的应用。

1.4.3 数据库悲观锁和乐观锁的原理和应用场景

悲观锁,先获取锁,再进行业务操作,一般就是利用类似 SELECT … FOR UPDATE 这样的语句,对数据加锁,避免其他事务意外修改数据。

当数据库执行SELECT … FOR UPDATE时会获取被select中的数据行的行锁,select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

乐观锁,先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过。Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断

1.5 锁模式

1.5.1 记录锁

记录锁(Record Lock):是最简单的行锁,仅仅锁住一行。
如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE,如果C1字段是主键或者是唯一索引的话,这个SQL会加一个记录锁

记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。它会阻塞其他事务对这行记录的插入、更新、删除。
一般我们看死锁日志时,都是找关键词,比如lock_mode X locks rec but not gap,就表示一个X型的记录锁。记录锁的关键词就是rec but not gap

1.5.2 间隙锁

间隙锁(Gap Lock),锁定一个范围,不包括记录本身
为了解决幻读问题,InnoDB引入了间隙锁。间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。它锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
比如lock_mode X locks gap before rec表示X型gap锁。

1.5.3 临键锁

临键锁(Next-Key Lock):是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。
说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如(5,10]。所以其实 Next-KeyLocks=Gap锁+ Recordlock锁
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。

1.5.4 意向锁

意向锁Intention Locks,意向锁相互兼容,意向锁是一种不与行级锁冲突的表级锁(table-level locking)
注意:意向锁,是一个表级别的锁哈
指未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向
意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
例子:事务A修改user表的记录r,会给记录r上一把行级的排他锁(X),同时会给user表上一把意向排他锁(IX),这时事务B要给user表上一个表级的排他锁就会被阻塞。意向锁通过这种方式实现了行锁和表锁共存且满足事务隔离性的要求

为什么需要意向锁呢? 或者换个通俗的说法,为什么要加共享锁或排他锁时的时候,需要提前声明个意向锁呢呢?

因为InnoDB是支持表锁和行锁共存的,如果一个事务A获取到某一行的排他锁,并未提交,这时候事务B请求获取同一个表的表共享锁。因为共享锁和排他锁是互斥的,因此事务B想对这个表加共享锁时,需要保证没有其他事务持有这个表的表排他锁,同时还要保证没有其他事务持有表中任意一行的排他锁。
然后问题来了,要保证没有其他事务持有表中任意一行的排他锁的话,去遍历每一行?这样显然是一个效率很差的做法。为了解决这个问题,InnoDb的设计者提出了意向锁。

意向锁分为两类:

  • 意向共享锁(IS锁):它预示着,事务有意向对表中的某些行加共享S锁,事务在请求S锁前,要先获得IS锁
  • 意向排他锁(IX锁):它预示着,事务有意向对表中的某些行加排它X锁,事务在请求X锁前,要先获得IX锁

意向锁仅仅表明意向的锁,意向锁之间不会互斥,是可以并行的,整体兼容性如下:

兼容性 IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

1.5.5 插入意向锁

插入意向锁(Insert Intention Locks)
对已有数据行的修改与删除,必须加强互斥锁X锁,那对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?
插入意向锁,是插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号。它解决的问题:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此

  • 插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
  • 在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
    假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方因为插入行不冲突。
  • 插入意向锁不会阻止任何锁,对于插入的记录会持有一个记录锁

锁模式兼容矩阵(横向是已持有锁,纵向是正在请求的锁):

兼容性 间隙锁 插入意向锁 记录锁 临键锁
间隙锁 兼容 兼容 兼容 兼容
插入意向锁 冲突 兼容 兼容 冲突
记录锁 兼容 兼容 冲突 冲突
临键锁 兼容 兼容 冲突 冲突

相信介绍下间隙锁:
间隙锁是(RR: 可重复读级别下)一个在索引记录之间的间隙上的锁,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间
在这里插入图片描述
当我们用范围条件而不是相等条件索引数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项枷锁;对于键值在条件范围内但并不存在的记录,叫做间隙(GAP)

1.5.7 自增锁

自增锁是一种特殊的表级别锁。它是专门针对AUTO_INCREMENT类型的列,对于这种列,如果表中新增数据时就会去持有自增锁。简言之,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

假设有表:

mysql> create table t0 (id int NOT NULL AUTO_INCREMENT,name varchar(16),primary key ( id));

mysql> show variables like '%innodb_autoinc_lock_mode%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 1     |
+--------------------------+-------+
1 row in set, 1 warning (0.01 sec)

参数innodb_autoinc_lock_mode,设置为1的时候,相当于将这种auto_inc lock弱化为了一个更轻量级的互斥自增长机制去实现,官方称之为mutex。
innodb_autoinc_lock_mode还可以设置为0或者2

  • 0:表示传统锁模式,使用表级AUTO_INC锁。
    这个参数的值被设置为 0 时,表示采用之前 MySQL 5.0 版本的策略,即语句执行结束后才释放锁;一个事务的INSERT-LIKE语句在语句执行结束后释放AUTO_INC表级锁,而不是在事务结束后释放。
    传统模式他可以保证数据一致性,但是如果有多个事务并发的执行 INSERT 操作,AUTO-INC的存在会使得 MySQL 的性能略有降落,因为同时只能执行一条 INSERT 语句

  • 1:连续锁模式或间断模式,连续锁模式对于Simple inserts不会使用表级锁,而是使用一个轻量级锁来生成自增值,因为InnoDB可以提前知道插入多少行数据。自增值生成阶段使用轻量级互斥锁来生成所有的值,而不是一直加锁直到插入完成。对于bulk inserts类语句使用AUTO_INC表级锁直到语句完成。
    普通 insert 语句,自增锁在申请之后就马上释放;类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
    该模式也可以保证数据一致性,但是如果有多个事务并发的执行 INSERT 批量操作时,就会进行锁等待状态。如果我们业务插入数据量很大时,这个时候MySQL的性能就会大大下降
    该值是MySQL8.0之前默认值

  • 2:交错锁模式或穿插模式,所有的INSERT-LIKE语句都不使用表级锁,而是使用轻量级互斥锁。所有的申请自增主键的动作都是申请后就释放锁
    该模式没有进行任何的上锁设置。在一定情况下是保证了MySQL的性能,但是无法保证数据的一致性。如果我们在穿插模式下进行主从复制时,如果binlog格式不是row格式,主从复制就会出现不一致。
    该值是MySQL8.0默认值且binlog_format=row.,这样有利于我们在 insert … select 这种批量插入数据的场景时,既能提升并发性,又不会出现数据一致性问题

INSERT-LIKE:指所有的插入语句,包括:INSERT、REPLACE、INSERT…SELECT、REPLACE…SELECT,LOAD DATA等。
Simple inserts:指在插入前就能确定插入行数的语句,包括:INSERT、REPLACE,不包含INSERT…ON DUPLICATE KEY UPDATE这类语句。
Bulk inserts: 指在插入前不能确定行数的语句,包括:INSERT … SELECT/REPLACE … SELECT/LOAD DATA。

自增ID用完会怎么样?

当主键自增 ID 达到上限后,再新增下一条数据时,它的 ID 不会变(还是最大的值),只是此时再添加数据时,因为主键约束的原因,ID 是不允许重复的,所以就会报错提示主键冲突。

row_id用完会怎么样?

如果表没有设置主键,InnoDB 会自动创建一个全局隐藏的 row_id,其长度为 6 个字节,当 row_id 达到上限后,它的执行流程和主键 ID 不同,它是再次归零,然后重新递增,如果出现相同的 row_id,后面的数据会覆盖之前的数据。

1.5.8 伪记录X锁

这个锁是因为线上使用INSERT ... ON DUPLICATE KEY UPDATE,然后发生死锁了,才发现这个锁。
supremum伪记录的X锁InnoDB存储引擎中的一种特殊锁机制。supremum伪记录本身不是一个可以被加锁的对象,它是一个特殊的标记,表示索引页的上界

  • supremum伪记录
    supremum伪记录InnoDB为了管理B+树索引而引入的一个特殊记录。它相当于比索引中所有值都大,但却不存在于索引中的一个虚拟记录。简单来说,它可以被看作是在索引结构中的最后一个记录之后的位置标记。当在InnoDB表中查询超过当前最大索引值的数据时,会涉及到对supremum伪记录的锁定。
  • supremum伪记录的X锁:
    当某个事务试图插入一个值超过当前索引最大值的新记录时,InnoDB会先对supremum伪记录加上X锁。这是因为新记录的插入可能会影响到索引的结构,尤其是当新记录成为新的最大值时。
    supremum伪记录X锁的目的是为了确保在插入操作完成之前,没有其他事务可以插入或更新超过当前最大值的记录,从而保持索引结构的一致性和完整性。
  • 锁的获取
    InnoDB的锁机制中,supremum伪记录本身并不直接持有X锁(排他锁)或S锁(共享锁),因为 它不是一个真正的数据记录 。然而,当事务试图向表中插入新记录时,它可能会与supremum伪记录相关的间隙(gap)或页进行交互。

1.5.9 操作

用下面这张数据库表在这里插入图片描述
发现事务 B 的 update 不会阻塞,而事务 C 的 update 会阻塞,都是对 id = 10 这条记录进行 update, 为什么一个会阻塞,一个不会阻塞
在这里插入图片描述
首先,我们先来分析下,事务 A 这条 SQL 加了什么锁。

// 事务 A 
select * from t_person where id < 10 for update;

事务 A 加了这三个行级锁:

  • 在 id 为 1 的主键索引上,加了 X 型的 next-key 锁,范围是 (-∞,1]。意味着,其他事务无法对 id = 1 的记录进行删除和更新操作,同时无法插入 id 小于 1 的新记录。
  • 在 id 为 5 的主键索引上,加了 X 型的 next-key 锁,范围是 (1, 5]。意味着,其他事务无法对 id = 5 的记录进行删除和更新操作,同时无法插入 id 为 2、3、4 的新记录。
  • 在 id 为 10 的主键索引上,加了 X 型的间隙锁,范围是 (5, 10)。意味着,其他事务无法插入 id 为 6、7、8、9 的新纪录。

事务 B 的 update 语句为什么不会阻塞?
事务 B 的 update 语句是对 id = 10 的行记录的 name 字段进行更新。

// 事务 B
update t_person set name = "小林" where id = 10;

事务 B 会在 id = 10 的主键索引上加 X 型记录锁,仅锁住这一行。因为当我们用唯一索引进行等值查询的时候,查询的记录是「存在」的,在索引树上定位到这一条记录后,该记录的索引中的 next-key 锁会退化成记录锁

事务 A 并没有对 id = 10 的主键索引上加 X 型记录锁,而是对 id = 10 的主键索引上加 X 型间隙锁间隙锁记录锁之间是没有互斥关系的,所以事务 B 的 update 语句不会阻塞。

事务 C 的 update 语句为什么会阻塞?
事务 C 的 update 语句是将 id = 10 的行记录的 id 更新为 2。

// 事务 C
update t_person set id = 2 where id = 10;

这条 update 很特殊,特殊之处在于更新了主键索引。你以为它只是一个更新操作,实际上它在背后执行了两个操作,也就是先删除 id = 10 的记录,然后再插入 id = 2 的新纪录:

  • 操作 1:delete from t_person where id = 10;
  • 操作 2:insert into t_person (2, 陈某, 30, 广州市海珠区);

为什么当 update 语句更新了索引值,会被拆分成删除和插入操作?
要回答这个问题,我们先要清楚 B+ 树的特点。
Innodb(MySQL 存储引擎)在实现索引的时候,采用的数据结构是 B+ 树。B+ 树是基于二分查找树演变过来的,所以 B+ 树在存储索引的时候,是按顺序存储的,因为这样才能利用二分查找快速检索到索引。
现在有一颗这样的 B+ 树,可以看到叶子节点的索引值是从小到大的顺序。
在这里插入图片描述

假设这时候需要将索引值为 25 更新为 3,如果直接索引值为 25 的位置上,将值改为 3 的话。
在这里插入图片描述
这时候你就会发现这棵 B+ 树不满足顺序性,所以更新索引的值,不能只是修改一个索引值就完事,而是还要保证更新后的索引值能继续满足 B+ 树的顺序性。
解决的方法就是:先删除索引值为 25 的节点,再插入索引值为 3 的节点,这样,这颗 B+树才能满足顺序性
在这里插入图片描述

事务 C 的 update 语句具体阻塞在哪个操作?
现在我们知道,事务 C 的 update 特殊语句背后执行了两个操作,分别是删除和插入操作,那具体是阻塞在哪个 操作?

操作 1 是删除 id = 10 的记录,事务 C 是会在 id = 10 的主键索引上加 X 型记录锁,而事务 A 并没有对 id = 10 的主键索引上加 X 型记录锁,而是对 id = 10 的主键索引上加 X 型间隙锁间隙锁记录锁之间是没有互斥关系的,所以操作 1 不会阻塞。
根据排除法,既然 操作 1 不会阻塞,那事务 C 的 update 语句阻塞的原因就是因为 操作 2发生了阻塞。

为什么操作2会发生阻塞呢?
要知道,插入操作什么时候会发生阻塞:插入语句在插入一条新记录之前,需要先定位到该记录在 B+树的位置,如果插入的位置的下一条记录的索引上有间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是插入语句会被阻塞

操作 2插入的是 id = 2 的新记录,在主键索引的 B+树定位到插入的位置如下图。
在这里插入图片描述
插入位置的下一条记录是 id = 5 的记录,而事务 A 在 id 为 5 的主键索引上已经加了 X 型的 next-key 锁,这里面包含了间隙锁。所以操作 2的插入操作会发生阻塞,这就是事务 C 的 update 语句阻塞的原因。

从这我们也可以知道间隙锁的作用就是阻止其他事务在间隙锁的范围内插入新记录,从而避免可重复读隔离级别下幻读的现象

我们也可以通过 select * from performance_schema.data_locks; 这条语句,查看事务 C 在加什么锁的时候导致阻塞。
在这里插入图片描述
从上面的输出信息,可以看到事务 C 在加插入意向锁的时候,发生了阻塞。

插入意向锁是插入操作才会有的锁,而事务 C 只是执行 update 语句,却出现了插入意向锁,从这里也可以证明,事务 C 这条特殊的 update 语句运行的时候,被拆分成了两个操作,一个是删除,另一个是插入。

总结:如果 update 语句更新的是普通字段的值,就会对发生更新的记录加 X 型记录锁。但是,如果 update 语句更新的是索引的值,那么在运行的时候会被拆分成删除插入操作,这时候分析锁的时候,要从这两个操作的角度去分析
此处参考链接:https://mp.weixin.qq.com/s/cVC-RDiNNR-ZhxCE4OeJ9Q

1.6 全局锁

1.6.1 定义

全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份,这个命令可以使整个库处于只读状态,使用该命令之后,数据更新语句,数据定义语句,更新类事务的提交语句等操作都会被阻塞。

1.6.2 使用全局锁会导致的问题

如果在主库备份,在备份期间不能更新,业务停止,所以更新业务会处于等待状态
如果在从库备份,在备份期间不能执行主库同步的binlog,导致主从延迟

参考连接:

posted @ 2021-04-15 11:46  上善若泪  阅读(152)  评论(0编辑  收藏  举报