MySQL是怎么提高并发度的?InnoDB为什么会设计这些锁?一篇文章串通MySQL各种锁!

SpringBoot+Vue+MySQL 高校学科竞赛平台 完整可运行版(源码+文档) 本项目基于SpringBoot+Vue+MySQL构建高校学科竞赛管理平台,覆盖学生、教师、领队教师、管理员四大角色,实现竞赛发布、报名、刷题、评分、公示等全流程闭环管理。系统架构清晰,技术主流,配套完整源码、数据库脚本、万字论文、开题报告、答辩PPT及部署文档,支持一键运行,适用于毕业设计、课程设计及院系二次开发,真正实现“开箱即用”。 阅读详情

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/0)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂

MySQL中的锁,按照锁的粒度分,分为一下三类:

  1. 全局锁:锁定数据库中的所有表
  2. 表级锁:每次操作锁住整张表
  3. 行级锁:每次操作锁住对应的行数据

相信不管是在工作中,还是再学习中,都会涉及到 MySQL 的锁,

本篇文章重点讨论 MySQL 的表级锁和行级锁

1 全局锁

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的 DML 的写语句,DDL 语句,已经更新操作的事务提交语句都将被阻塞

其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性

基本操作:

使用全局锁:

FLUSH TABLES WITH READ LOCK;

释放全局锁:

UNLOCK TABLES;

备份数据库内容:

mysqldump -h -u -p... 数据库名 [表名1] [表名2] > 备份的位置  -- 不建议直接这么使用
-- 示例
mysqldump -h localhost -u root -p 123456 learn_mysql emp dept > G:/emp_copy.sql

不加锁的一致性备份,加参数: --single-transaction

mysqldump --single-transaction  -h localhost -u root -p 123456 emp > G:/emp_copy.sql

演示图:

数据库中加全局锁,是一个比较重的操作,存在以下问题:

  1. 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
  2. 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟

解决方法:

在 InnoDB 引擎中,在备份时加上参数--single-transaction参数来完成不加锁的一致性数据备份(只适用于支持「可重复读隔离级别的事务」的存储引擎)

mysqldump --single-transaction  -h localhost -u root -p 123456 emp > G:/emp_copy.sql

原理补充:通过加上这个参数,确保了在备份开始时创建一个一致性的快照,通过启动一个新的事务来实现这一点。(该事务的隔离级别是Repeatable Read级别),从而实现在该事务读取下一直读取的是创建时的数据,而不影响其他事务的读写操作。如果不是 RR 级别,那么会出现下面的问题,例如我们需要备份两个表,一个是余额表,一个是购买成功的订单表。当备份余额表时,用户购买了一个东西。当备份订单表时,用户就好像时不花钱买了一个东西,从而造成了数据不一致。

2 表级锁

每次操作锁住整张表,锁定粒度大,发生锁的冲突的概率最高,并发度最低。所以全局锁的场景对于 JAVA 开发人员来说很少会遇到,更多的是表级锁和行级锁。那讲到这里,肯定理解为什么会有表级锁和行级锁(都是为了降低锁的粒度,提高并发度)

下面我们继续讨论表级锁,表级锁分为表级读写锁,元数据锁,意向锁,还有 InnoDB 引擎的 AUTO-INC 锁

其中表级读锁和表级写锁可以用户手动加锁,元数据锁和意向锁用户无法自动加锁

2.1 表级读锁 / 表级写锁

对于表锁,分为两类:

  1. 表共享读锁(read lock),阻塞其他客户端的写,不会阻塞读
LOCK TABLES 表名 READ;
  1. 表独占写锁(write lock),阻塞其他客户端的 写和读
LOCK TABLES 表名 WRITE;

读锁不会阻塞其他客户端的读,但是会阻塞写。写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。

表锁不支持重入:无法在同一会话中多次获得相同的表锁

表锁不支持从写锁降级为读锁:必须先释放写锁,然后单独获取读锁

表锁不支持从读锁升级为写锁:必须先释放读锁,然后单独获取写锁

-- 表级别的共享锁,也就是读锁
-- 允许当前会话读取被锁定的表,但阻止其他会话对这些表进行写操作
LOCK TABLES emp READ;

-- 表级锁的独占锁,也是写锁
-- 允许当前会话对表进行读写操作,但阻止其他会话对这些表进行任何操作(读或写)
LOCK TABLES emp WRITE;

释放所有锁(会话退出,也会释放所有锁):

UNLOCK TABLES;

2.2 元数据锁(MDL)

元数据锁(Metadata Lock )是 server 层的锁,表级锁,每执行一条 DML、DDL 语句时都会申请 MDL 锁,DML 操作需要 metadata 的读锁,DDL 操作需要 metadata 写锁,metadata 加锁过程是系统自动控制,无法直接干预

MDL 加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL 锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免 DML 与 DDL 冲突,保证读写的正确性

  1. 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
  2. 对一张表做结构变更操作的时候,加的是 MDL 写锁;

对应SQL

锁类型

说明

适用场景

lock tables xxx read lock tables xxx write

SHARED_READ_ONLY SHARED_NO_READ_WRITE

LOCK TABLES 是一种显式的表级锁,可以指定为读锁或写锁

① select ② select ... lock in share mode

SHARED_READ

与SHARED_READ、SHARED_WRITE兼容,与 EXCLUSIVE 互斥

SHARED_READ 锁适用于查询操作,允许多个事务并发读取数据,但不允许写入

① insert ② update③ delete ④ select ...for update

SHARED_WRITE

与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥

SHARED_WRITE锁适用于修改数据的操作,允许多个事务并发写入数据,但不允许其他事务对表结构进行修改

alter table ...

EXCLYSIVE

与其他的MDL都互斥

EXCLUSIVE 锁用于表结构变更的操作,它是独占的,不允许其他事务操作该表的元数据

查看元数据锁:

SELECT object_type,object_schema,object_name,lock_type,lock_duration FROM performance_schema.metadata_locks;

LOCK_MODE

LOCK_DATA

锁范围

X,REC_NOT_GAP

15

15 那条数据的行锁

X,GAP

15

15 那条数据之前的间隙,不包含 15

X

15

15 那条数据的间隙,包含 15

在没有元数据之前,MySQL 会存在一个 bug:会话 A 执行了多条更新语句期间,另外一个会话 B 做了表结构变更并且先提交,就会导致 slave 在重做时,先重做 alter,再重做 update 时就会出现复制错误的现象

又比如:当前的事务级别为 RR,session1有一个事务正在查询数据,此时另一个会话 session2 进行了 DDL 语句,修改了表的结构,那么在 session1 的一个事务中,即使在 RR 级别下,两次查询结果也会不一致,无法满足可重复读的要求。5.1 之前对于元数据的保护是语句级别的(语句执行完毕,无论事务是否回滚还是提交,对于其他会话都是可见的),5.5 之后对于元数据的保护才是事务级别的,MDL主要解决两个问题:

  1. 事务隔离问题:元数据的保护范围扩展到事务级别,解决 RR 级别的一个事务中可能出现两次查询不一致问题
  2. 数据复制问题:比如会话A执行了多条更新语句期间,另外一个会话B做了表结构变更并且先提交,就会导致slave在重做时,先重做alter,再重做update时就会出现复制错误的现象

2.3 意向锁(IS/IX)

为了避免 DML 在执行时,加的行锁与表锁的冲突,在 InnoDB 中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突

  1. 如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢
  2. 那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁。那么在想要添加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录

意向锁的目的是为了快速判断表里是否有记录被加锁

意向锁之间是互相兼容的

IS 锁

IX 锁

IS 锁

兼容

兼容

IX 锁

兼容

兼容

意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)

表级别

IS 锁

IX 锁

S 锁

兼容

互斥

X 锁

互斥

互斥

加锁方式:

  1. IS : 意向共享锁(先在表上加上意向共享锁,然后对读取的记录加共享锁)
SELECT ... LOCK IN SHARE MODE;
  1. IX : 意向独占锁(先表上加上意向独占锁,然后对读取的记录加独占锁)
INSERT / UPDATE / DELETE / (SELECT ... FOR UPDATE)
-- 示例
START TRANSACTION;
-- 查找某账户的余额并加锁,以便后续更新
SELECT salary FROM emp WHERE id = 1 FOR UPDATE;
-- 执行操作,如修改余额
UPDATE emp SET salary = salary - 1000 WHERE id = 1;
COMMIT;

查看意向锁:

SELECT object_schema, object_name, index_name, lock_type, lock_mode, lock_data FROM performance_schema.data_locks;

LOCK_MODE

LOCK_DATA

锁范围

X,REC_NOT_GAP

15

15 那条数据的行锁

X,GAP

15

15 那条数据之前的间隙,不包含 15

X

15

15 那条数据的间隙,包含 15

2.4 AUTO-INC 锁

表中的主键值都会自动增加,这是通过对主键字段加上 AUTO_INCREMENT 属性实现的。之前如果想插入新记录,可以不指定主键的值,数据库会自动生成主键值增加的值,这主要是通过 AUTO_INCREMENT 锁实现的。不仅仅是自增主键,<font style="color:#000000;">AUTO_INCREMENT</font>的列都会涉及到自增锁,毕竟非主键也可以设置自增长

AUTO_INCREMENT 锁是特异的锁,不是再一个事务提交后才释放,而是执行完插入语句后就会立即释放。

在插入数据时,会加一个表级别的 AUTO_INCREMENT 锁,然后释放 AUTO_INCREMENT 锁。那么,一个事务持有 AUTO_INCREMENT 锁的过程中,其他事务的请求向表格进行插入操作,依然会影响插入的主键值的自增进程。但是,AUTO_INCREMENT 锁再大量数据插入时,影响插入性能,因为另一个事务的插入会被阻塞。因此,在 MySQL 5.1.22 版本以后,InnoDB 存储引擎提供了一种轻量级的锁来实现自增

就像是正在插入数据时,会涉及到 AUTO_INCREMENT 锁的修改,但后续这些字段值一个自增值的生成,就不需要等待整个插入语句执行完后释放锁。

InnoDB 存储引擎提供了一个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择不同的 AUTO_INCREMENT 锁,最常用的锁

  1. 当 innodb_autoinc_lock_mode = 0 时,默认采用 AUTO_INCREMENT 锁,语句执行结束后释放锁;
  2. 当 innodb_autoinc_lock_mode = 1 时,采用轻量级锁,并且需要等待其他执行完后释放;
  3. 当 innodb_autoinc_lock_mode = 2 时,采用混合模式 普通 insert 语句,自增锁在申请之后就马上释放; 类似 insert...select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

当 innodb_autoinc_lock_mode=2 是性能最高的方式,但是当搭配 binlog 的日志格式是 statement 一起使用的时候,在「主从复制的场景」中会发生数据不一致的问题

session A 往表 t 中插入了 4 行数据,然后创建了一个相同结构的表 t2,然后往 t2 中插入数据。

如果 innodb_autoinc_lock_mode = 2,意味着「申请自增主键后就释放锁,不必等待整个插入语句执行完」

那么就可能出现这样的情况:

  • session B 先插入了两条记录 (1,1), (2,2);
  • 然后,session A 来时申请 id=3,插入了 (3,5);
  • 然后,session B 继续执行,插入两条记录 (4,3), (5,4)

可以看到,sessionB 的 insert 语句,生成的 id 不连续\ 当「主库」发生了这种情况,binlog 面对 t2 表的更新只会记录这两个 session 的 insert 语句,如果 binlog_format=statement,记录的语句就是原始语句。记录的顺序要么先记sessionA的insert语句,要么先记 session B的 insert 语句\ 但不论是哪一种,这个binlog拿去「从库」执行,这时从库是按「顺序」执行语句的,只有当执行完一条 SQL 语句后,才会执行下一条 SQL。因此,在从库上「不会」发生像主库那样两个 session「同时」执行向表 t2 中插入数据的场景。所以,在备库上执行了 sessionB 的 insert 语句,生成的结果里面,id 都是连续的。这时,主从库就发生了数据不一致。\ 要解决这问题,binlog 日志格式要设置为 row,这样在 binlog 里面记录的是主库分配的自增值,到备库执行的时候,主库的自增值是什么,从库的自增值就是什么。 所以,当 innodb_autoinc_ock_mode=2 时,并且binlog_format=row,既能提升并发性,又不会出现数据一致性问题。

3 行级锁

行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。

  1. 记录锁(Record Lock): 锁定单个行记录的锁,防止其他事务对此行进行 update 和 delete ,在RC、RR隔离级别下都支持,InnoDB 的数据是基于索引组织的(聚集索引),记录锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁
  2. 间隙锁(Gap Lock): 锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 insert,产生幻读,在RR隔离级别下都支持
  3. 临键锁(Next-Key Lock): 行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap,在RR隔离级别下支持

InnoDB 有三种行锁的特点:

  1. Record Lock(记录锁):锁住某一行记录
  2. Gap Lock(间隙锁):锁住一段左开右开的区间
  3. Next-key Lock(临键锁):锁住一段左开右闭的区间

加锁规则的两条核心:

  1. 查找过程中访问到的对象才会加锁(注意访问三个是 B+ 树的访问规则)
  2. 加锁的基本单位是 Next-key Lock,根据规则逐步退化

通过使用 select * from performance_schema.data_locks; 和操作实践,可以看出 LOCK_MODE 和 LOCK_DATE 的关系:

LOCK_MODE

LOCK_DATA

锁范围

X,REC_NOT_GAP

15

15 那条数据的行锁

X,GAP

15

15 那条数据之前的间隙,不包含 15

X

15

15 那条数据的间隙,包含 15

  1. LOCK_MODE = X 是前开后闭区间;
  2. X,GAP 是前开后开区间(间隙锁);
  3. X,REC_NOT_GAP 行锁。

3.1 Record Lock(行锁)

Record Lock 称为记录锁,锁住的是一条记录,而且记录锁是有 S 锁和 X 锁之分

InnoDB实现了以下两种类型的行锁:

  1. 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁
  2. 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁

S(共享锁)

X(排他锁)

S(共享锁)

兼容

冲突

X(排他锁)

冲突

冲突

行锁类型:

SQL

行锁类型

说明

insert,update,delete ...

排他锁

自动加锁

select

不加任何锁

select ... lock in share mode

共享锁

需要手动select之后加上lock in share mode

select ... for update

排他锁

需要手动在select之后for update

默认情况下,InnoDB在 REPEATABLE READ 事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读

  1. 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为记录锁
  2. InnoDB的记录锁是针对于索引加的锁,不通过索引条件检索数据,那么 InnoDB 将对表中的所有记录加锁,此时 就会升级为表锁

3.2 Gap Lock(间隙锁)

Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。假设,表中有一个范围 id 为 (3,5) 间隙锁,那么其他事务就无法插入 id=4 这条记录了,这样就有效的防止幻读现象的发生

Gap Lock 只能防插入,不能防已有行的更新 / 删除,间隙锁的引入就是为了解决幻读问题的

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的

3.3 Next-Key Lock(临键锁)

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

假设,表中有一个范围id为 (3,5] 的 Next-Key Lock,那么其他事务即不能插入 id=4 记录,也不能修改 id = 5 这条记录

所以,Next-Key Lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。

Next-Key Lock 是包含间隙锁 + 记录锁的,如果一个事务获取了 X 型的 Next-Key Lock,那么另外一个事务在获取相同范围的 X 型的 Next-Key Lock 时,是会被阻塞的。比如,一个事务持有了范围为 (1,10] 的 X 型的 Next-Key Lock,那么另外一个事务在获取相同范围的 X 型的 Next-Key Lock 时,就会被阻塞。虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的

默认情况下,InnODB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。

  1. 索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁
  2. 索引上的等值查询(非唯一索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
  3. 索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止
  4. 索引上的范围查询(非唯一索引),会访问到不满足条件的第一个值为止,next-key lock 不能退化为 gap lock

4 MySQL中一条SQL是如何加锁的?

从两阶段锁说起,在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,这样锁的时间就尽可能的短

事务隔离界别为 RR,MySQL 是如何通过 Multi-Version Concurrency Control(MVCC) 和间隙锁来解决幻读的问题?

InnoDB 有三种行锁的特点:

  1. Record Lock(记录锁):锁住某一行记录
  2. Gap Lock(间隙锁):锁住一段左开右开的区间
  3. Next-key Lock(临键锁):锁住一段左开右闭的区间

加锁规则的两条核心:

  1. 查找过程中访问到的对象才会加锁(注意是基于 B+ 树的访问规则)
  2. 加锁的基本单位是 Next-key Lock,根据规则逐步退化

假设有这么一张 test 表,id 为主键(唯一索引),a 是普通索引(非唯一索引),b 都是普通的列,其上没有任何索引

id(唯一索引)

a(非唯一索引)

b

10

4

A

15

8

B

20

16

C

25

32

D

30

64

E

CREATE TABLE `test`(
    `id` int UNIQUE KEY COMMENT '唯一索引',
    `a` int COMMENT '非唯一索引',
    `b` varchar(1) COMMENT '普通列',
    INDEX `not_unique`(`a`)
);

INSERT INTO test VALUES(10, 4, 'A'), (15, 8, 'B'), (20, 16, 'C'), (25, 32, 'D'), (30, 64, 'E'), (35, 128, 'F'); 

唯一索引+唯一条件 时,锁是可退化的,锁只作用于那条记录,不加后间隙锁;

非唯一索引 或 非唯一条件 时,InnoDB 会锁定从第一个匹配到最后一个匹配,再到它后面的第一个非匹配记录之间的整个区间(记录锁 + 前后两端的间隙锁)

4.1 唯一索引等值查询

当我们用唯一索引进行等值查询时,根据查询记录是否存在,加锁的规则会有所不同:

  1. 当查询的记录是存在的,Next-Key Lock 会退化为 Record Key
  2. 当查询记录不存在时,Next-key Lock 会退化为 Gap Lock
  1. 记录存在
select * from user where id = 25 for update;

结合加锁的两条核心:查找过程中访问到的对象才会加锁 + 加锁的基本单位是 Next-key Lock(左开右闭),我们可以分析出,这条语句的加锁范围是 (20, 25]。 不过,由于这个唯一索引等值查询的记录 id = 25 是存在的,因此,Next-key Lock 会退化成 Record Lock,因此最终的加锁范围是 id = 25 这一行

  1. 记录不存在
select * from user where id = 22 for update;

结合加锁的两条核心:查找过程中访问到的对象才会加锁 + 加锁的基本单位是 Next-key Lock(左开右闭),我们可以分析出,这条语句的加锁范围是 (20, 25],由于这个唯一索引等值查询的记录 id = 22 是不存在的,因此,Next-key Lock 会退化成 Gap Lock,因此最终在主键 id 上的加锁范围是 Gap Lock (20, 25)

4.2 唯一索引范围查询

当我们用唯一索引进行范围查询时,根据查询记录是否存在,加锁的规则会有所不同:

  1. 当范围的边界是存在的,Next-Key Lock 会退化为 Record Key
  2. 当范围的边界不存在时,Next-Key Lock 会退化为 Gap Lock
  3. 唯一索引的范围查询需要一直向右遍历到第一个不满足条件的记录,添加 Next-Key 只后再进行退化 下边界不存在时,8.0.17 版本会加下一个前开后闭范围的锁。而 8.0.18 版本及以后,进行了优化,主键时判断不等,不会锁住后闭的区间,加一个前开后开的范围的锁 下边界存在时,8.0.17 会锁住下一个 next-key 的前开后开区间,而 8.0.18 及以后版本,将不会添加下一个间隙锁
select * from user where id >= 20 and id < 22 for update;

查询条件的前半部分 id >= 20,因此,这条语句最开始要找的第一行是 id = 20,然后向右遍历,结合加锁的两个核心,需要加上 Next-key Lock (15,20]。又由于 id 是唯一索引,且 id = 20 的这行记录是存在的,因此会退化成记录锁,也就是只会对 id = 20 这一行加锁。

再来看语句查询条件的后半部分 id < 22,由于是范围查找,就会继续往后找第一个不满足条件的记录,也就是会找到 id = 25 这一行停下来,然后加 Next-key Lock (20, 25],重点来了,但由于 id = 25 不满足 id < 22,因此会退化成间隙锁,加锁范围变为 (20, 25)

所以,上述语句在主键 id 上的最终的加锁范围是 Record Lock id = 20 以及 Gap Lock (20, 25)

select * from user where id >= 20 and id <= 25 for update;

id = 20 的记录锁和上面的情况一样。然后会加一个(20,25] 的邻键锁,因为 21,22,23,24 都在范围内且这些数据不存在,所以不会退化为记录锁,所以退化为间隙锁,但是 id=25 是存在的所以退化为记录锁

(20,25] next-key lock ➔ (20,25) gap lock + id=25 record lock

所以加锁范围就是(20,25]

在 8.0.18 版本之前还会继续往后遍历找到第一个不满足条件的记录,然后添加(25,30)的间隙锁。但是在 8.0.18 版本之后,就不会添加(25,30)的间隙锁了。找到了边界条件就不再往后遍历了。

所以,上述语句在主键 id 上的最终的加锁范围是 Record Lock id = 20 以及 Gap Lock (20, 25)

4.3 非唯一索引等值查询

当我们用非唯一索引进行等值查询时,根据查询记录是否存在,加锁的规则会有所不同:

  1. 当查询的记录是存在的,除了会加 Next-Key Lock 之外,还会添加一个额外的 Gap Lock(规则是向后遍历到第一个不符合条件的值才会停止),查找记录的左区间加 Next-key Lock,右区间加 Gap lock
  2. 当查询记录不存在时,Next-key Lock 会退化为 Gap Lock
  1. 记录存在
select * from user where a = 16 for update;

这条语句首先会对普通索引 a 加上 Next-key Lock,范围是 (8,16]。

又因为是非唯一索引等值查询,且查询的记录 a= 16 是存在的,所以还会加上间隙锁,规则是向下遍历到第一个不符合条件的值才能停止,因此间隙锁的范围是 (16,32)。

所以,上述语句在普通索引 a 上的最终加锁范围是 Next-key Lock (8,16] 以及 Gap Lock (16,32)。

  1. 记录不存在
select * from user where a = 18 for update;

结合加锁的两条核心,这条语句首先会对普通索引 a 加上 Next-key Lock,范围是 (16,32]。

但是由于查询的记录 a = 18 是不存在的,因此 Next-key Lock 会退化为间隙锁,即最终在普通索引 a 上的加锁范围是 (16,32)

4.4 非唯一索引范围查询

当我们用非唯一索引进行范围查询时,根据查询记录是否存在,加锁的规则会有所不同:

  1. 当范围的下界是存在的,Next-Key Lock 也不会退化,所以始终加的是 Next-Key Lock
  2. 唯一索引的范围查询需要一直向右遍历到第一个不满足条件的记录,即使不满足条件,也不会像唯一索引的范围查询进行锁的退化,所以最终加的都是 Next-Key Lock
select * from user where a >= 16 and a < 18 for update;

先来看语句查询条件的前半部分 a >= 16,因此,这条语句最开始要找的第一行是 a = 16,结合加锁的两个核心,需要加上 Next-key Lock (8,16]。虽然非唯一索引 a = 16 的这行记录是存在的,但此时并不会像 唯一索引范围查询 那样退化成记录锁

再来看语句查询条件的后半部分 a < 18,由于是范围查找,就会继续往后找第一个不满足条件的记录,也就是会找到 id = 32 这一行停下来,然后加 Next-key Lock (16, 32]。虽然 id = 32 不满足 id < 18,但此时并不会向 唯一索引范围查询 那样退化成间隙锁


进一步理解锁的最小单位是 Next-Key Lock

所以,上述语句在普通索引 a 上的最终的加锁范围是 Next-key Lock (8, 16] 和 (16, 32],也就是 (8, 32]

select * from user where a >= 16 and a <= 32 order by a desc for update;

在这语句中,我们需要按照索引 a 进行降序排序,又是如何加锁的呢?

  1. 由于是 order by a desc,所以第一个要定位的是所以 a 上"最右边的数据",也就是 a = 32 的行,所以会添加一个(16,32] 的锁,等值查询之后,还需要向后遍历,找到第一个不符合条件的值,找到了 a = 64 的值,所以会添加一个(32,64] 的锁
  2. 然后继续像左遍历,扫描到 16 时,添加(8,16],然后继续向左寻找第一个不符合条件的值,找到了 a = 8,所以会添加(4,8] 的锁

讲到这里是不是就很清除加锁的过程了?就是遍历过程中遇到一个记录,就给这个记录添加一个 Next-Key Lock,然后再根据不同的情况决定是否需要退化 Next-Key Lock

4.5 limit 语句加锁

对于非唯一索引而言,如果语句明确加了 limit 的限制,相当于告诉了 InnoDB 此次只需要变量多少需要的记录。相当要告诉 InnoDB 不再需要遍历到第一个不满足条件的记录。例如 t 表中的数据有

c(非唯一索引)

id(唯一索引)

0

0

5

5

10

10

10

30

15

15

20

20

25

25

DELETE FROM t WHERE c = 10 limit 2;

如果不加 limit 2,加的锁将是(c=5,id=5) 到 (c=15,id=15) 的开区间

但是添加了 limit 2,InnoDB 再遍历到第二个满足条件的数据时(10,30),就不会继续往下遍历了

所以加的锁是(c=5,id=5) 到(c=10,id=30) 这个前开后闭区间

所以在删除数据的时候尽量加 limit,这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围

4.6 死锁的例子

c(非唯一索引)

id(唯一索引)

0

0

5

5

10

10

10

30

15

15

20

20

25

25

Session A 和 Session B 对上面进行的操作如下

  1. Session A 的 select 语句获得的锁是(5,10] 和(10,15]
  2. Session B 更新时也要在索引 C 上加 Next-Key Lock(5,10],但是 10 已经有了记录锁,所以此时会进入阻塞
  3. 然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁,InnoDB 让 session B 回滚

所以到现在加锁的过程就很明朗了,session B 的“加 next-key lock(5,10] ”操作,实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 c=10 的行锁,这时候才被锁住的。

也就是说,我们在分析加锁规则的时候可以用 next-key lock 来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的

4.7 为什么加锁逻辑是这样的?

首先你要明白的是间隙锁的引入是为了解决幻读问题的,其次幻读特指其他事务在我查询的范围中新插入了数据,明白了这一点,你就能抓住重心,如何才能锁住不造成幻读的范围

对于 唯一索引的等值查询 来说,我能确保该值只可能出现一个,所以我只需要添加 Record Lock 就能保证其他事务想要修改我,或者想要变为我,都是加锁失败的。

对于 唯一索引的范围查询 来说,我只需要将查询的范围区间覆盖,这样就阻塞了其他事务在这个范围中插入数据。如果区间的临界值时存在的,那么还能进一步缩减区间。如果区间的临界值不存在,我们就不应该缩减区间。例如表中唯一索引包含的数据有 id = 20 和 id = 25,查询范围是 [20, 22) ,那我们应该锁住的是 [20, 25)这个区间,而不是锁住 [20, 22) 这个区间,我们是基于 B+ 树 的扫描过程进行加锁的,不是想当然的进行加锁的。

对于 非唯一索引的等值查询,因为值是可以重复的。所以当一个值是存在的时候,我们需要锁住当前值,以及他的前后区间,防止前后区间的数据被修改成为当前值。此外,如果一个值是不存在的,那我们只需要锁住这个值可能出现的区间位置即可,比如 有非唯一索引 a = 16,a = 32,当想要锁住 a = 18 时,发现记录不存在,那么我们就只需要锁住它可能出现的范围 (16, 32) 即可。

对于 非唯一索引的范围查询,因为值是可以重复的,所以不管区间的临界值时是否存在,我们都需要锁住这个记录,以及它前后的区间。但是这个地方我们不能像 唯一索引的范围查询 一样进行区间的缩减了。比如 上面的例子中,如果将 (8, 32] Next-Key Lock 退化为 (8, 32) Gap Lock,那么就可能会出现 a = 32 值,被修改到了 (8, 32) 区间内的值!为什么会出现这种情况?因为如果是 (8, 32) Gap Lock,会因为 Gap Lock 是兼容的,而导致其他 Gap Lock 也加锁成果。比如我们修改 a = 32 时,根据 非唯一索引等值 的加锁流程我们会添加 (16, 32], (32, 64) 这两把锁。所以是能加锁成功的,也就代表了,我们是能够将 a = 32 修改为 (8, 32) 中间的值,也就会继续出现幻读的现象。所以对于 非唯一索引的范围查询 ,我们不能缩减区间,根本原因就是 Gap-Key 是兼容的,Gap Lock 只能防插入,不能防已有行的更新/删除

最后我们将了 limit 情况,得出的结论是删除数据的时候尽量加 limit,这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围

从个体到群体:Cartool实战静息态EEG微状态聚类全解析 本文详细解析了使用Cartool软件进行静息态EEG微状态聚类的完整实战流程。从个体水平聚类到群体模板提取,再到指标量化,文章深入探讨了k-means算法的应用、GFP峰值数据的使用技巧以及关键参数设置,为脑电信号处理研究者提供了从数据准备到结果解读的全方位指南。 阅读详情

相关推荐

AWR射频仿真基本操作(一)

这里只讲述一个基本的操作: 软件是AWR 14版本

钢丝 2万+

面试必备——MySQL解决幻读的底层实现原理

本篇的理论知识偏多会略显枯燥,但这也是解决电商项目高并发的关键理论知识笔者旨在将MVCC以及锁这些知识串通起来,使得更加系统,同时也理顺自己混乱的思绪。建议先阅读笔者写的InnoDB锁和事务模型之锁MySQL锁之InnoDB锁实战面试必备——MySQL的MVCC实现原理,简单阅读了解个大概即可由于写写并发操作的场景下会涉及间隙锁,因此在这里简要介绍间隙锁,下文也会提到它。InnoDB支持3种行锁如下,详情可以见InnoDB锁和事务模型之锁行锁(Record Lock):锁是直接加在索引。

Android_la的博客 561

NXP(imx8qxp)系列DDR校准以及android镜像烧录

文章目录一、 不得不吐槽以下nxp的这个代码镜像烧写的过程,做得太开源了,导致镜像根本烧录不进去,实属令人头痛。 一、

永不秃头的程序员的博客 5991

MySQL(InnoDB)一棵 B+ 树能存多少条记录?从页、记录到树高度的完整链路解析

本文从页(page)与记录(row)的物理字节组成出发,串通 InnoDB 的页结构(File header / Page header / infimum & supremum / slot array)、记录头与内部隐含字段(DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID),推导出 页内能容纳多少条记录 的通用公式,并给出典型例子与树高估算方法。文中给出可复用的估算步骤与实战建议,帮助你在容量规划与性能诊断时做出准确判断。

往者之不谏 来者犹可追 955

焦距与视差的关系及其对深度估计的影响

焦距在立体视觉中的作用至关重要,它直接影响视差的大小,从而影响深度估计的精度。焦距越长,视差差异越大,物体的深度估计就越精确。这一关系使得焦距长的相机在一些需要高精度深度估计的应用场景中更加优越。了解焦距与视差的关系,对于优化相机配置和提升深度估计精度具有重要意义。在实践中,选择合适的焦距对于获得精确的深度信息至关重要,特别是在自动驾驶、虚拟现实、三维重建等领域,精确的深度估计能够提供更好的用户体验和技术支持。

计算机科学与技术专业学生的成长之路 1671

手把手教你学Simulink——基于全桥 LLC 谐振变换器的双向 DC-DC 建模与仿真

本文详细介绍了基于Simulink的双向全桥LLC谐振变换器建模与仿真方法。内容涵盖:1)LLC谐振变换器背景与双向化挑战;2)系统架构设计及关键数学推导(FHA增益分析、双向控制逻辑);3)Simulink建模步骤(功率级搭建、PWM生成、同步整流实现);4)仿真结果分析(正向/反向工作验证、ZVS特性);5)工程实践建议(增益微调、代码生成与HIL测试)。通过手把手教学,帮助读者掌握950V↔48V双向LLC变换器的完整设计流程,实现高效率能量双向流动的同时保持软开关特性,适用于储能系统、车载充电等应用

MHD0815的博客 179

从零打通 MySQL → DataX → Doris:Windows 11 + Docker 本地环境搭建全记录

本文记录了在Windows 11+Docker环境下搭建MySQL→DataX→Doris数据同步链路的完整过程。主要内容包括:1) 介绍MySQL与Doris的分工及ETL流程;2) 详细说明Docker环境搭建步骤,包含MySQL、Doris FE/BE节点的配置;3) 数据同步实操,通过DataX将MySQL数据迁移到Doris;4) 常见问题解决方案,如Docker网络配置、Python版本兼容等。特别指出使用mysqlwriter替代doriswriter的便捷方案,解决了BE节点内网IP访问问题

孟林洁的博客 696

ROS机器人未知环境自主探索功能包explore_lite最全源码详细解析(二)

本系列文章主要针对ROS机器人常使用的未知环境自主探索功能包explore_lite展开全源码的详细解析,并进行概括总结。 本系列文章共包含六篇文章,前五篇文章主要介绍explore_lite功能包中 explore.cpp、costmap_tools.h、frontier_search.cpp、costmap_client.cpp等源码实现文件中全部函数的详细介绍,第六篇文章进行概括总结,包含自主探索功能包explore_lite的简介,实现未知环境自主探索功能的原理及流程总结,效果演示,使用功能包exp

慕羽★的博客 1648

MySQL 索引深入理解:B+ 树、二级索引、联合索引与查询优化

理解二级索引后,就可以理解回表了。SELECT *MySQL 的执行过程分为三个步骤。覆盖索引(Covering Index)并不是一种独立的索引类型,而是一种查询效果。SQL 查询需要的所有字段,都能够直接从某个索引中获得,不需要再访问聚簇索引获取其他字段。例如:因为user_id已经包含在二级索引中,所以 MySQL 可以直接返回结果。不需要回表。联合索引(Composite Index)是同时使用多个字段建立的索引。它的排序规则是:首先按照user_id排序。当user_id。

m0_64642997的博客 135

基于OpenClaw的自动化文件管理与文档分类系统设计与实现

本文介绍了一种基于OpenClaw的自动化文件管理与文档分类系统。系统采用三层架构设计,包含数据层、逻辑层和应用层,核心组件涵盖文件扫描、处理、分类、移动等功能。技术选型上使用Python/FastAPI作为后端,结合PostgreSQL/Elasticsearch/MinIO存储方案,并整合scikit-learn/PyTorch等AI技术实现智能分类。系统实现了文件递归扫描、增量扫描、元数据提取等功能,通过文件哈希和修改时间检测变化。该方案能有效提升文件管理效率,实现文档的自动化分类与智能管理。

一键难忘的博客 754

Asterisk中MixMonitor之支持接通后录音

一、概述 当我们使用开源的IPPBX(比如Elastix-2.5/4.0或者ISSABEL-4.0/FreePBX)的时候Outbound和Inbound路由(RingGroup/FollowMe)中有一个Call Recording选项,其值包括:Allow、Record immediately、Record On Answer和Nerver。当选择为Record On Answer,其实就是MixMonitor自带一个b选项。 尽管Asterisk自带的MixMoni...

宽简厚重—博约 1156

MySQL 单点详解(14)

给定一条 SQL,联合索引的列该怎么排呢?

2401_88601046的博客 345

如何在 UniApp 中接入讯飞语音听写功能 文末附vue3+vue2完整代码

在开发过程中,我遇到了讯飞接口返回空数据的问题。通过仔细检查传送的每一帧数据,我发现问题出在第一帧的配置信息以及 Base64 数据的切片。确保每一帧的数据正确且符合接口要求,问题就能够得到解决。希望这个过程能帮助你在项目中顺利对接讯飞语音识别。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 997

MySQL 的 REPEATABLE READ 到底能不能防幻读——从快照读、当前读到 Next-Key Lock

本文系统讲解 InnoDB 可重复读级别下的幻读问题:从三类读异常切入,剖析快照读依赖 MVCC、当前读依赖 Next-Key Lock 的双防线机制,用双会话实验复现幻读仍会出现的边界场景,并解释各数据库在幻读防护上行为分歧的根源。

大模型全栈工程师 · Thanks_ks 297

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Flink SQL 连接器实战:MySQL Source、Flink CDC 与 JDBC Sink 全解析

提供有界流读取,适合批处理和维表场景,简单直接但无法感知变更。MySQL CDC提供无界流读取,通过「全量快照 + binlog 增量」的方式实现实时捕获,是实时数仓的核心入口,但需要 MySQL 侧正确配置 binlog 和权限。JDBC Sink负责结果回写,关键在于区分仅插入流和更新流,更新流必须依赖主键完成 upsert。掌握这三者的差异,本质上就是掌握了 Flink 中「有界与无界」「追加与更新」这两组最基本的语义。

2401_83857914的博客 455

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

MySQL 中 DISTINCT 与 GROUP BY 哪个效率更高?

DISTINCT与GROUP BY在纯去重场景下,MySQL 8.0 中执行计划通常一致,效率基本相同。老版本(8.0.13 之前)GROUP BY有隐式排序开销,DISTINCT往往更快。需要聚合或HAVING时,只能用GROUP BY。效率的真正决定因素是索引和执行计划,而不是这两个关键字本身。所以,与其问「哪个更快」,不如问「我的查询有没有走对索引」。

2601_96709915的博客 326

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

基于 FastAPI+LangChain+LangGraph+Agent+Vue3+MySQL 的房产数据采集与房价预测分析平台

本文介绍一个完整的「房产数据采集+AI房价预测」全栈项目,涵盖FastAPI异步后端、LangGraph Agent智能问答、爬虫采集、机器学习预测与Vue3前端,实现从数据采集到AI交互的闭环。项目融合多技术栈,具备真实业务场景与生产级设计(如降级策略、流式输出、多模型对比),兼具简历亮点与答辩演示优势,可系统性提升工程能力,并附高频面试题解析,是理想的技术综合实践项目。

深耕开发,专注编程实战。分享源码解析、项目搭建、问题排查笔记,沉淀技术,持续更新。 231

MySQL 8.0 双向主从复制故障修复与重建指南

MySQL 8.0 双向主从复制故障修复 SOP:XtraBackup 重建备用节点,解决 GTID 1236 / 历史日志冲突

「YYL DBA手记」 数据库 · 架构 · 运维 · 实战 yyldba.blog.youkuaiyun.com 298

MySQL 8.0停服了,信创替代的窗口正在关闭

最近跟几个做信创的朋友聊天,发现一个被忽略的问题。MySQL 8.0在今年4月正式停了官方支持,Oracle不再出安全补丁,bug也没人修了。但大量企业的业务系统还跑在上面。这事搁以前不算急,慢慢选就行了。但现在不行了,国资委79号文写得很清楚,2027年底前央国企核心系统得完成国产化替换。MySQL停服跟信创deadline撞在一起,留给MySQL用户做决定的时间不多了。我做过不少数据库迁移项目,MySQL替代这个事,坑比大多数人想的要多。今天聊几个我觉得最容易被忽视的点。

databasedb的博客 333

MySQL:库与表的操作

本文系统讲解MySQL库的操作,涵盖创建、查看、修改、删除数据库及字符集、校验规则的配置与影响。重点说明CREATE DATABASE语法、字符集(如utf8mb4)与校验规则(如utf8_general_ci vs utf8_bin)对大小写敏感性和排序的影响。通过实例演示不同校验规则下查询与排序结果差异。同时介绍SHOW DATABASES、USE、ALTER DATABASE、DROP DATABASE等操作,并强调备份恢复(mysqldump)与连接状态监控(SHOW PROCESSLIST)的重要

2401_87306079的博客 325

MySQL 主从复制进阶(2026):GTID、复制延迟排查与故障切换

load 高到底是 CPU 还是 IO?free 的 available 才是关键。us/sy/wa 决策树 + iostat/sar/iftop 实战,CPU/内存/磁盘/网络四大瓶颈定位方法论。

2502_91473995的博客 182

Linux、Shell、MySQL 测试卷逐题细讲(下篇:MySQL)

本文为Linux、Shell、MySQL测试卷下半段MySQL部分逐题讲解,包含填空、选择与SQL分析三类题型。填空题覆盖COUNT、HAVING、DISTINCT、事务、约束、索引等核心语法;选择题辨析存储过程、索引类型、ACID、存储引擎、用户管理等考点。SQL大题围绕员工、部门、职位、地点四张关联表,考察多表连接、分组聚合、自连接、相关子查询、NULL处理等高频写法,标注LEFT JOIN统计计数、NULL判断、子查询别名等易错坑点,同时梳理试卷OCR识别带来的干扰问题,重点提示SQL实操中的常见失误

dongziwen1的博客 185

任务4-1 了解MySQL的数据类型

本文梳理了MySQL数据类型:数值型涵盖整型与实型,金额首选DECIMAL;字符串型按长度与用途分为CHAR、VARCHAR、TEXT及BLOB;日期型包含DATE、TIME、DATETIME及TIMESTAMP;特殊类型有ENUM、JSON等。掌握类型特性是高效建表的基础。

howard2005的专栏 147

MySQL 为什么选择 B+ 树作为索引结构

B 树和 B+ 树都是多路平衡树,但 B+ 树在数据库场景下更有优势,主要体现在以下几个方面。对比对象主要问题B+ 树的优势二叉树 / 红黑树树高过大,I/O 次数多多路平衡,树高仅 3~4 层哈希表不支持范围查询、排序天然有序,支持范围与排序B 树节点存数据,扇出小;查询路径不一致非叶子只存键,扇出大;查询路径稳定B+ 树用更矮的树高减少磁盘 I/O,用有序的叶子链表支撑范围查询,用只存键的非叶子节点换取更大的扇出,是磁盘数据库索引场景下综合表现最优的选择。

2601_96129122的博客 319

mysql初阶--数据库设计

本文系统阐述数据库设计的核心方法论,从范式理论(1NF、2NF、3NF)出发,揭示数据冗余与操作异常的根源,强调3NF为工程实践的最优平衡点。通过E-R图构建实体关系模型,实现从业务需求到表结构的清晰映射。文章厘清类、实体、表三者对应关系,提出“抽象→建模→落地”的设计流程,帮助开发者建立可复用的数据库设计思维框架,避免“拍脑袋”建表的陷阱。

2601_97033068的博客 704
上一篇: 基于Redisson的分布式锁
下一篇: Spring AI MCP入门:实现与MCP Server交互
程序员职业指南
博客等级 码龄4年 2136粉丝 960原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值