MySQL进阶07 — 锁机制1

概述

本章,您将学习到 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:只存在两种状态
    1. 转账成功 —— A 的银行卡余额减少 500 元, B 的银行卡余额增加 500 元
    2. 转账失败 —— 双方的银行卡余额未发生变化
  • C:转账前后,双方银行卡的总金额 2000 元是不变的
  • I:转账过程中其他事务无法看到中间状态(如 A 已扣款但 B 未到账),且无法修改相关账户数据。需要通过数据库锁机制和隔离级别实现,确保并发操作时数据一致性。
  • D:转账成功后,双方银行卡的余额永久被修改,即生成了具体的银行流水条目(银行流水条目是不可篡改的审计记录)。

回滚:把正在执行的操作全部撤销,回到最初的状态。

在实际的生产环境下,会遇到多个事务同时执行的情况,前面的 ACID 特性提到了隔离性,若对事务不隔离,会出现各种并发问题(脏读、不可重复读、幻读)。

  • 脏读(dirty reads) - 指读取到了实际并不存在的数据,其产生原因如下图所示:

    file

    事务 T1 读到了事务 T2 更新但还没有提交的数据,若事务 T2 回滚,则刚刚 T1 读取的数据就是临时且无效的。

  • 不可重复读(Nonrepeatable read) - 指在一个事务内,最开始读到的数据和事务结束之前读到的数据不一致,其产生原因如下:

    file

  • 幻读(Phantom Read) - 指在一个事务中,两次或多次读到的行数据结果集不一致

    file

Q:不可重复读和幻读是一样的吗?

不是。不可重复读针对的是已经存在的某一行或多行数据中的字段值(用的是「更新」,关键字 update);幻读针对的是行数据(插入或者删除,关键字为 insertdelete

事务隔离靠的是隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,数据完整性与一致性越好,但并发性越弱。

隔离级别(从低到高) 脏读 不可重复读 幻读
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 隔离级别下,为了解决幻读问题而引入的一种锁。该锁是行锁的一种,它是记录锁与间隙锁的结合,既可以锁定索引区间,也可以锁定索引记录。

提示
由于有了间隙锁和临键锁,在 REPEATABLE-READ 隔离级别下是可以解决幻读问题的。
意向锁:表锁的一种,其本身并不直接锁定任何数据行,而是作为一种 "标记" 存在,用于表明事务后续准备对表中的某些行施加共享锁(S 锁)或排他锁(X 锁)。意向锁由存储引擎自行维护,使用者无法手动操作。意向锁又划分为意向共享锁(IS 锁) 和意向排他锁(IX 锁)。

例如,当某一个事务想对表添加表锁,它需要先确认表中没有任何行被其他事务加了行锁,对于一张存储了十几万行数据的表而言,逐行检查的效率非常地低,于是便引入了意向锁。有了意向锁之后,事务在给某行数据加锁前,会先自动获取该表的意向锁(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
提示
在 REPEATABLE-READ 隔离级别下,InnoDB 存储引擎默认优先采用临键锁,当 SQL 操作中包含了唯一索引或主键索引时,临键锁会退化成为记录锁。

记录锁

需要满足以下两个条件才能持有记录锁:

  1. 对某一条记录上锁,必须是通过唯一索引或主键索引所在的列进行加锁
  2. 必须使用精准的筛选条件(=),否则记录锁会使用临键锁

来看下面的例子:

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,+∞)

括号 () 表示开区间,方括号 [] 表示闭区间。

  1. 对于主键索引,给某条记录加锁只会产生记录锁

    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 语句都能正确执行。

  2. 主键值的范围产生间隙锁

    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,'小章'); - 正常执行
  3. 给主键不存在的记录添加锁

    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) 这个间隙区间
  4. 普通索引持有间隙锁

    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;- 正常运行

行锁限制

行锁只在当前连接会话的事务中生效,一旦该事务提交或回滚,或退出连接会话,行锁的限制消失。另外,阻塞的限制情况对行锁所在的事务没有限制。

Avatar photo

关于 陸風睿

GNU/Linux 从业者、开源爱好者、技术钻研者,撰写文档既是兴趣也是工作内容之一。Q - "281957576";WeChat - "jiulongxiaotianci",Github - https://github.com/jimcat8
用一杯咖啡支持我们,我们的每一篇[文档]都经过实际操作和精心打磨,而不是简单地从网上复制粘贴。期间投入了大量心血,只为能够真正帮助到您。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇