概述
本章,您将学习到 MySQL 8 中有关锁机制的知识,内容包括:
- 锁机制的分类
- 演示 InnoDB 存储引擎的行锁
- 演示 InnoDB 存储引擎的表锁
- 锁的查看
- 有关乐观锁、全局锁和死锁的内容
- MVCC
由于内容较多,本文档只说明锁机制的分类以及演示 InnoDB 存储引擎的行锁
回顾事务
在前面《MySQL基础15 --- TCL》文章中,我们知道事务的定义:
事务(Transaction):事务是 MySQL的一种机制(当然其他的关系型数据库也有),它由一条或者多条 SQL 语句组成,这些成批的 SQL 语句是一个执行单元,它们互相依赖且是一个不可分隔的整体,要么全部执行,要么全部不执行,用来完成某一个业务或事情或功能,保证数据库的完整性或一致性。如果某一事务执行成功,则在该事务中进行的所有数据修改均会被提交,成为数据库中的永久组成部分。如果事务遇到错误且必须取消或回滚,则所有数据修改均被清除。
通过执行以下 MySQL 交互命令,知道只有 InnoDB 存储引擎才支持事务:
show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ndbcluster | NO | Clustered, fault-tolerant tables | NULL | NULL | NULL |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| ndbinfo | NO | MySQL Cluster system information storage engine | NULL | NULL | NULL |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
事务具有 ACID 的特性:
- A :原子性(atomicity,或称不可分割性)。事务中的所有操作必须作为一个不可分割的整体执行,要么全部成功提交(
COMMIT),要么全部失败回滚(ROLLBACK),不存在部分执行的中间状态。其实现依赖于数据库的日志机制(如 Undo Log)和事务控制命令。 - C :一致性(consistency)。确保事务执行前后,数据库始终满足预定义的数据完整性约束和业务规则,即使事务执行过程中可能存在临时不一致状态。
- I :隔离性(isolation,又称独立性)。指一个事务的执行不会被其他事务干扰。在生产环境下,多个事务并发执行时,需通过具体的隔离级别控制其相互干扰程度,确保数据一致性。
- D :持久性(durability),指事务提交后,对数据库的数据修改是永久性的,即使发生系统崩溃、断电等故障也不会丢失。其实现依赖于预写日志(WAL)机制和事务日志(如 Redo Log)。
以 A 用户向 B 用户银行转账为例,假设 A 的银行卡余额为 1000 元,B 的银行卡余额为 1000 元,现在需要完成 A 向 B 转账 500 元的这件事。
- A:只存在两种状态
- 转账成功 —— A 的银行卡余额减少 500 元, B 的银行卡余额增加 500 元
- 转账失败 —— 双方的银行卡余额未发生变化
- C:转账前后,双方银行卡的总金额 2000 元是不变的
- I:转账过程中其他事务无法看到中间状态(如 A 已扣款但 B 未到账),且无法修改相关账户数据。需要通过数据库锁机制和隔离级别实现,确保并发操作时数据一致性。
- D:转账成功后,双方银行卡的余额永久被修改,即生成了具体的银行流水条目(银行流水条目是不可篡改的审计记录)。
回滚:把正在执行的操作全部撤销,回到最初的状态。
在实际的生产环境下,会遇到多个事务同时执行的情况,前面的 ACID 特性提到了隔离性,若对事务不隔离,会出现各种并发问题(脏读、不可重复读、幻读)。
-
脏读(dirty reads) - 指读取到了实际并不存在的数据,其产生原因如下图所示:
事务 T1 读到了事务 T2 更新但还没有提交的数据,若事务 T2 回滚,则刚刚 T1 读取的数据就是临时且无效的。
-
不可重复读(Nonrepeatable read) - 指在一个事务内,最开始读到的数据和事务结束之前读到的数据不一致,其产生原因如下:
-
幻读(Phantom Read) - 指在一个事务中,两次或多次读到的行数据结果集不一致
Q:不可重复读和幻读是一样的吗?
不是。不可重复读针对的是已经存在的某一行或多行数据中的字段值(用的是「更新」,关键字 update);幻读针对的是行数据(插入或者删除,关键字为 insert、delete)
事务隔离靠的是隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,数据完整性与一致性越好,但并发性越弱。
| 隔离级别(从低到高) | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 1(READ UNCOMMITTED,读取未提交的数据) | 会出现 | 会出现 | 会出现 |
| 2(READ COMMITTED,读取提交的数据) | 不会出现 | 会出现 | 会出现 |
| 3(REPEATABLE-READ,可重复读,全局默认隔离级别) | 不会出现 | 不会出现 | 会出现 |
| 4(SERIALIZABLE,序列化) | 不会出现 | 不会出现 | 不会出现 |
可通过查询这个系统变量的值来了解当前连接会话的隔离级别:
select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
锁机制的分类
隔离级别的实现,其核心就是锁机制。
在 MySQL 8 InnoDB 存储引擎中,官方文档里提到了七种锁:
- 共享/排他锁(Shared and Exclusive Locks)
- 意向锁(Intention Locks)
- 记录锁(Record Locks)
- 间隙锁(Gap Locks)
- 临键锁(Next-key Locks)
- 插入意向锁(Insert Intention Locks)
- 自增锁(Auto-inc Locks)
但人们还有更加细致的划分:
- 从锁的机制思想划分 - 悲观锁、乐观锁
- 从锁的兼容性划分 - 共享锁、排他锁
- 从锁的粒度划分 - 行锁、表锁、页锁
- 从锁的模式划分 - 记录锁、间隙锁、临键锁、意向锁、插入意向锁、自增锁
- 从加锁的方式划分:隐式锁、显式锁
- 其他锁 - 全局锁、死锁
从锁的机制思想划分
悲观锁:又名悲观并发控制(Pessimistic Concurrency Control,缩写 "PCC"),是一种并发控制的方法。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放(即提交事务或者回滚事务后才释放锁)之后,其他事务才能够执行与该锁冲突的操作。说人话就是 —— 事务每次操作这行数据时会很悲观,总认为其他用户的事务会修改它,于是在操作数据之前先将其锁定。悲观锁是一种机制思想,并不是实际操作过程中特指的某一个锁。本质上,排他锁与共享锁都是悲观锁的实现。
乐观锁:又名乐观并发控制(optimistic concurrency control,缩写 "OCC"),也是一种并发控制的方法。和悲观锁相反,乐观锁假定认为操作的数据在一般情况下不会造成冲突,所以不会对操作的数据进行加锁,只有到数据提交时才通过一种机制(常见的是对记录的数据进行版本比对)来验证数据是否冲突。同样的,悲观锁是一种机制思想,并不是实际操作过程中特指的某一个锁。
从锁的兼容性划分
共享锁:S 锁,也称读锁(read lock)。该锁允许多个事务同时读取同一数据,但禁止任何事务在此期间修改该数据或添加排他锁,直到所有共享锁都被释放。这种锁确保了数据读取的并发性,同时防止了脏读和不可重复读的问题。
排他锁:X 锁,也称写锁(write lock)。该锁确保事务对数据操作的独占性,防止并发冲突。一旦事务对数据加上了排他锁,其他任何事务都无法再对该数据施加任何类型的锁(包括共享锁和排他锁),直到该事务提交或回滚释放锁。
| 共享锁(S 锁) | 排他锁(X 锁) | |
|---|---|---|
| 共享锁(S 锁) | 兼容 | 冲突 |
| 排他锁(X 锁) | 冲突 | 冲突 |
从锁的粒度划分
表锁:每次操作时锁住整张表,操作时其他事务无法访问该表
行锁:每次操作时仅锁定操作的行,其他事务可操作不同行。行锁包含共享锁与排他锁。
页锁:每次操作时锁定相邻的一组记录。其特性如下:
三者的对比如下所示:
| 特性 | 表锁 | 行锁 | 页锁 |
|---|---|---|---|
| 锁定粒度 | 整张表(最大) | 单行记录(最小) | 位于行锁与表锁之间 |
| 锁冲突概率 | 最高 | 最低 | 中等 |
| 并发度 | 最低 | 最高 | 一般 |
| 存储引擎支持 | 全部 | 仅 InnoDB | 仅 BDB |
| 死锁 | 不出现 | 可能出现 | 可能出现 |
| 其他 | 实现简单、开销小,适用于读多写少的场景 | 支持共享锁与排他锁。可防止幻读问题(通过间隙锁实现)。适用于高并发更新场景。 | 锁定相邻记录页,比表锁高效,比行锁简单。适用于需要介于表锁和行锁性能的场景。 |
从锁的模式划分
记录锁:行锁的一种,也是 InnoDB 中最基础的锁,它直接锁定索引中的某一条具体记录。READ COMMITTED 与 REPEATABLE-READ 隔离级别都支持记录锁。
间隙锁:在 REPEATABLE-READ 隔离级别下,为了解决幻读问题而引入的一种锁。该锁是行锁的一种,锁定的是索引记录中的间隙。
临键锁:在 REPEATABLE-READ 隔离级别下,为了解决幻读问题而引入的一种锁。该锁是行锁的一种,它是记录锁与间隙锁的结合,既可以锁定索引区间,也可以锁定索引记录。
例如,当某一个事务想对表添加表锁,它需要先确认表中没有任何行被其他事务加了行锁,对于一张存储了十几万行数据的表而言,逐行检查的效率非常地低,于是便引入了意向锁。有了意向锁之后,事务在给某行数据加锁前,会先自动获取该表的意向锁(IS 锁或 IX 锁),这样其他事务想获取表锁时,只需检查意向锁状态即可快速判断,避免了逐行检查。
| 共享锁(S) | 意向共享锁(IS) | 排他锁(X) | 意向排他锁(IX) | |
|---|---|---|---|---|
| 共享锁(S) | 兼容 | 兼容 | 冲突 | 冲突 |
| 意向共享锁(IS) | 兼容 | 兼容 | 冲突 | 兼容 |
| 排他锁(X) | 冲突 | 冲突 | 冲突 | 冲突 |
| 意向排他锁(IX) | 冲突 | 兼容 | 冲突 | 兼容 |
从这里可以看到,意向锁与意向锁之间是相互兼容的;排他锁与其他任何锁都是冲突的。注意!这里的共享锁或排他锁都是表级别的。
插入意向锁:多个事务同时将不同的数据插入到同一个索引间隙,当插入的位置不冲突时,则相互之间不会造成阻塞。它针对的是多个事务的数据插入,也就是常规的 insert 语句。插入意向锁并不属于意向锁,它是一种间隙锁,专门用于数据插入操作。
自增锁:特殊的表锁,顾名思义,针对自增长列。当 A 事务中插入包含了自增长列(auto_increment)的新增数据时,会持有自增锁,若 B 事务执行 insert 操作包含了自增长列的数据,需要等待 A 事务的释放。自增锁有几种工作模式,由系统变量 innodb_autoinc_lock_mode 的值控制,MySQL 8 中,其值默认为 2,即交叉模式。
从加锁的方式划分
先略过,后面会说明
其他锁
先略过,后面会说明。
演示 InnoDB 行锁
| 存储引擎 | 行锁 | 表锁 | 页锁 |
|---|---|---|---|
| InnoDB | √ | √ | |
| MyISAM | √ | ||
| BDB | √ | √ |
InnoDB 可使用行锁与表锁。在 MySQL 8 的 InnoDB 存储引擎中,行锁通过索引记录进行实现,具体包括:
- 记录锁
- 间隙锁
- 临键锁
- 插入意向锁
在 InnoDB 存储引擎中,先看下行锁与隔离级别的对应关系:
| 隔离级别 | 记录锁 | 间隙锁 | 临键锁 | 插入意向锁 |
|---|---|---|---|---|
| READ UNCOMMITTED | ||||
| READ COMMITTED | √ | |||
| REPEATABLE-READ | √ | √ | √(默认优先采用) | √ |
| SERIALIZABLE |
记录锁
需要满足以下两个条件才能持有记录锁:
- 对某一条记录上锁,必须是通过唯一索引或主键索引所在的列进行加锁
- 必须使用精准的筛选条件(
=),否则记录锁会使用临键锁
来看下面的例子:
create database locks;
use locks;
create table if not exists t1(
id int primary key auto_increment,
name varchar(10)
);
insert into t1 values(null,'frank'),(null,'jack'),(null,'vk'),(null,'tom');
select * from t1;
| Session 1 | Session 2 |
|---|---|
| use locks; set autocommit=0; start transaction; select * from t1 where id=1 for update; |
|
| use locks; set autocommit=0; start transaction; update t1 set name='zhang' where id=1; |
|
| commit; |
- Session 1 中,
for update即添加了记录锁(排他锁),其他事务对这条记录无法更新、删除,但是可以被读取 - Session 2 中,
update ...这条 SQL 语句的窗口会一直等待,出现 "ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction." 的提示,等待的时间由系统变量innodb_lock_wait_timeout的值来决定,默认 50 秒 - Session 1 中,提交事务后释放记录锁
间隙锁
由上面的内容可知:
在 REPEATABLE-READ 隔离级别下,为了解决幻读问题而引入的一种锁。该锁是行锁的一种,锁定的是索引记录中的间隙。
间隙锁:仅锁定间隙(不锁定记录)
要持有(添加)间隙锁,需要满足:
- 唯一索引和主键索引 - 当用排他锁锁住多条记录或者不存在的记录时,会产生间隙锁。指定给某条存在的记录加锁时(在筛选条件中使用
=),只会持有记录锁,并不会产生间隙锁 - 普通索引 - 不管是锁单条记录还是多条记录,其都会产生间隙锁
use locks;
create table if not exists tb1(
id int primary key auto_increment comment '用户 ID 标识',
name varchar(8)
);
insert into tb1 values(1,'小红'),(3,'小张'),(5,'小李'),(11,'小明');
若此时 id 列索引记录产生了间隙锁,则区间为:
- (-∞,1)
- (1,3)
- (3,5)
- (5,11)
- (11,+∞)
括号 ( 或 ) 表示开区间,方括号 [ 或 ] 表示闭区间。
-
对于主键索引,给某条记录加锁只会产生记录锁
Session 3 Session 4 use locks;
set autocommit=0;
start transaction;
select * from tb1 where id=5 for update;use locks;
set autocommit=0;
start transaction;
insert into tb1 values(4,'小东');
insert into tb1 values(6,'小西');commit; rollback; Session 4 中,那两条插入的 SQL 语句都能正确执行。
-
主键值的范围产生间隙锁
Session 5 Session 6 use locks;
set autocommit=0;
start transaction;
select * from tb1 where id>7 for update;use locks;
set autocommit=0;
start transaction;
insert into tb1 values(6,'小晋');
insert into tb1 values(8,'小东');
insert into tb1 values(12,'小西');
insert into tb1 values(2,'小章');commit; rollback; id>7,意味着 (5,11) 和 (11,+∞) 这两个区间都受到影响insert into tb1 values(6,'小晋');- 阻塞,因为 6 属于 (5,11) 这个间隙区间insert into tb1 values(8,'小东');- 阻塞,因为 8 属于 (5,11) 这个间隙区间insert into tb1 values(12,'小西');- 阻塞,因为 12 属于 (11,+∞) 这个间隙区间insert into tb1 values(2,'小章');- 正常执行
-
给主键不存在的记录添加锁
Session 7 Session 8 use locks;
set autocommit=0;
start transaction;
select * from tb1 where id=7 for update;use locks;
set autocommit=0;
start transaction;
insert into tb1 values(6,'小晋');
insert into tb1 values(8,'小东');
insert into tb1 values(12,'小西');
insert into tb1 values(2,'小章');commit; rollback; id=7,由于表中不存在这条记录,因此产生了间隙锁,受影响的区间为 (5,11)insert into tb1 values(6,'小晋');- 阻塞,因为 6 属于 (5,11)这个间隙区间insert into tb1 values(8,'小东');- 阻塞,因为 8 属于 (5,11)这个间隙区间insert into tb1 values(12,'小西');- 正常运行,因为 12 属于 (11,+∞) 这个间隙区间insert into tb1 values(2,'小章');- 正常运行,因为 2 属于 (1,3) 这个间隙区间
-
普通索引持有间隙锁
use locks; create table if not exists tindex1( id int primary key auto_increment comment '用户id号', number int, key index_number(number) ); insert into tindex1 values(1,110),(7,130),(15,140),(17,160);若此时 number 列索引记录产生了间隙锁,则区间为:
- (-∞,110)
- (110,130)
- (130,140)
- (140,160)
- (160,+∞)
Session 9 Session 10 use locks;
set autocommit=0;
start transaction;
select * from tindex1 where number=130 for update;use locks;
set autocommit=0;
start transaction;
insert into tindex1 values(6,129);
update tindex1 set id=8 where number=130;
insert into tindex1 values(8,131);
insert into tindex1 values(5,100);
insert into tindex1 values(3,161);
insert into tindex1 values(10,141);commit; rollback; - 由于 number 列应用的是普通索引,因此
number=130并不是记录锁。number=130,意味着 (110,130) 和 (130,140) 两个间隙区间都会受影响,见 Session 10 - 如果
number=135,则只有 (130,140) 这个间隙区间受影响。如果number>138,则 (130,140]、(140,160]、(160,+∞) 三个间隙区间都受影响,这里其实是产生了临键锁 insert into tindex1 values(6,129);- 阻塞update tindex1 set id=8 where number=130;- 阻塞insert into tindex1 values(8,131);- 阻塞insert into tindex1 values(5,100);- 正常运行insert into tindex1 values(3,161);- 正常运行insert into tindex1 values(10,141);- 正常运行
间隙锁的难度并不大,关键是要清楚事务中的 SQL 语句会使那些间隙区间受到影响。
临键锁
临键锁:锁定记录及其前后的间隙(左开右闭区间)。
| Session 11 | Session 12 |
|---|---|
| use locks; set autocommit=0; start transaction; select * from tindex1 where number>135 and number<146 for update; |
|
| use locks; set autocommit=0; start transaction; update tindex1 set id=10 where number=140; update tindex1 set id=20 where number=160; insert into tindex1 values(11,141); insert into tindex1 values(9,131); insert into tindex1 values(20,166); |
|
| commit; | |
| rollback; |
number>135 and number<146,在这个范围内只有 140 这个记录,且锁定前后的间隙区间,即 (130,140] 和 (140,160] 这两个区间受影响update tindex1 set id=10 where number=140;- 阻塞update tindex1 set id=20 where number=160;- 阻塞insert into tindex1 values(11,141);- 阻塞insert into tindex1 values(9,131);- 阻塞insert into tindex1 values(20,166);- 正常运行
插入意向锁
指多个事务同时将不同的数据插入到同一个索引间隙,当插入的位置不冲突时,则相互之间不会造成阻塞。它针对的是多个事务的插入,语法上就是常规的 insert 语句,就不具体演示了。
共享锁
该锁允许多个事务同时读取同一数据,但禁止任何事务在此期间修改该数据或添加排他锁,直到所有共享锁都被释放。
共享锁是行级别锁的一种,使用的关键字为 lock in share mode,普通索引、主键索引、唯一索引都可以持有共享锁。
还是以前面的 tindex1 表为例来说明:
| Session 13 | Session 14 |
|---|---|
| use locks; set autocommit=0; start transaction; select * from tindex1 where id=1 lock in share mode; |
|
| use locks; set autocommit=0; start transaction; select * from tindex1 where id=1; update tindex1 set number=100 where id=1; delete from tindex1 where id=1; select * from tindex1 where id=1 for update; select * from tindex1 where id=1 lock in share mode; |
|
| commit; | |
| rollback; |
select * from tindex1 where id=1;- 正常运行update tindex1 set number=100 where id=1;- 阻塞delete from tindex1 where id=1;- 阻塞select * from tindex1 where id=1 for update;- 添加排他锁,阻塞select * from tindex1 where id=1 lock in share mode;- 正常运行
行锁限制
行锁只在当前连接会话的事务中生效,一旦该事务提交或回滚,或退出连接会话,行锁的限制消失。另外,阻塞的限制情况对行锁所在的事务没有限制。












