一、事务隔离性概述
事务隔离性(Isolation) 是指多个并发事务之间相互隔离,一个事务的执行不应该影响其他事务的执行。
MySQL 支持四种事务隔离级别,默认隔离级别:可重复读(Repeatable Read)。

锁的行为与事务的隔离级别紧密相关:
**读未提交:**通常不加锁,通过直接读最新数据实现,存在脏读、幻读等问题。
**读已提交:**每次读取的都是已提交的最新数据(快照)。 对于 UPDATE、DELETE,只对实际修改的行加锁(记录锁),不使用间隙锁,所无法避免幻读。
**可重复读(InnoDB 的默认级别):**在事务开始时创建一致性视图,整个事务期间都读取这个视图。使用临键锁作为默认的行锁算法,通过锁住记录和间隙来防止幻读。
**串行化:**最高的隔离级别,通过强制事务串行执行来避免所有并发问题。 在这个级别下,InnoDB 可能会隐式地将普通的 SELECT 语句转换为 SELECT ... FORSHARE,从而加共享锁,导致读读也可能阻塞。
二、MySQL 保证隔离性的机制
MySQL 的锁机制是为了在并发访问下,保证数据的一致性、完整性而设计的。它可以大致分为以下三个维度来理解:
-
锁的粒度:锁定的范围大小
-
锁的模式:锁的兼容性,即多个锁之间能否共存
-
锁的类型:从程序员视角看,锁的共享与排他特性
按锁的粒度划分
1 全局锁, 作用范围:整个数据库实例。
sql
FLUSH TABLES WITH READ LOCK;
SET read_only = ON
**效果:**使数据库处于只读状态,所有数据修改操作(DML)和数据结构变更操作 (DDL)都会被阻塞。
**使用场景:**全库逻辑备份。但请注意,在 InnoDB 中,由于有 MVCC,通常使 用 mysqldump --single-transaction 来进行不锁表的热备份,这比全局锁更好。
2 表级锁,作用范围:整张表
sql
LOCK TABLES table_name READ/WRITE; 和 UNLOCK TABLES;
MyISAM 引擎默认使用表锁。
**元数据锁:**不需要显式使用,在执行 DML(如 SELECT, INSERT, UPDATE, DELETE)时自动加,防止表结构被修改。读锁之间不互斥,但写锁是排他的。
**意向锁:**InnoDB 特有的表级锁,用于快速判断表内是否有行锁冲突。
**效果:**锁定整张表,粒度大,并发性能差。
3 行级锁, 作用范围:单行或多行记录
**支持引擎:**InnoDB。
**效果:**粒度最小,只锁定需要操作的行,极大提高了并发性能。但管理开销最大,容易 导致死锁。
**实现方式:**InnoDB 通过给索引项加锁来实现行锁。这意味着:如果查询条件没有用到索引,InnoDB 会退化为表锁,行锁实际上是加在索引记录上的。
InnoDB 的行锁模式(锁的类型)
1 共享锁,简称:S 锁。
特性 :又称为读锁。一个事务获取了某行的共享锁后,其他事务也可以获取同一行的共享锁 (读读兼容),但不能获取该行的排他锁(读写不兼容)。
加锁方式:
sql
SELECT ... LOCK IN SHARE MODE; -- 在旧版本中
SELECT ... FOR SHARE; -- 在 MySQL 8.0+ 中推荐使用
2 排他锁,简称:X 锁。
**特性:**又称为写锁。一个事务获取了某行的排他锁后,其他事务既不能获取该行的共享锁,也不能获取该行的排他锁(写写、读写都不兼容)。
加锁方式:
sql
SELECT ... FOR UPDATE;
DML 语句(UPDATE, DELETE, INSERT)会自动给涉及的行加上排他锁。
兼容性矩阵:

三、行锁的算法(锁定了哪些数据)
InnoDB 的行锁是通过对索引项加锁实现的,根据查询条件和索引使用情况,锁定的范围有所不同。
3.1 记录锁
**锁定:**单个索引记录。
场景:当语句精确匹配到某一条记录,且使用了唯一索引(包括主键)时。
示例:
sql
SELECT * FROM users WHERE id = 10 FOR UPDATE;
如果 id 是主键,这条语句会在 id=10 这条索引记录上加 X 锁。
3.2 间隙锁
**锁定:**一个索引区间,但不包括记录本身。例如锁住 (5, 10) 这个开区间。
**目的:**防止其他事务在区间内插入新的记录,从而解决幻读问题。
**场景:**使用范围查询或者查询不存在的记录时。
示例:
sql
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;
这条语句会锁住 id 在 (5, 10) 区间内的所有"间隙",防止插入 id=6,7,8,9 的新记录。
3.3 临键锁
**锁定:**一个索引记录 + 该记录之前的间隙。它是 记录锁 + 间隙锁 的组合。
**目的:**InnoDB 默认的行锁算法。它既锁住了记录本身,也锁住了它之前的间隙,从在 "可重复读"隔离级别下有效地防止了幻读。
**示例:**如果表中有记录 id=5, 10, 15。
sql
SELECT * FROM users WHERE id > 10 AND id <= 15 FOR UPDATE;
这条语句会锁定 (10, 15] 这个区间。它锁住了 id=15 这条记录(记录锁),
以及 (10,15) 这个间隙(间隙锁)。
四、 死锁
当两个或多个事务相互等待对方释放锁时,就会发生死锁。
示例:
事务 A:UPDATE table SET ... WHERE id = 1; (持有 id=1 的 X 锁)
事务 B:UPDATE table SET ... WHERE id = 2; (持有 id=2 的 X 锁)
事务 A:UPDATE table SET ... WHERE id = 2; (等待事务 B 释放 id=2 的锁)
事务 B:UPDATE table SET ... WHERE id = 1; (等待事务 A 释放 id=1 的锁)
-> 死锁发生!
InnoDB 的处理方式:
InnoDB 有一个内置的死锁检测机制,会主动选择一个回滚成本较小的事务(通常就是影响行数较少的事务)进行回滚,并报出 1213 错误(Deadlock found when trying to get lock)。
另一个事务则可以继续执行。
如何避免死锁:
尽量让事务以相同的顺序访问表和行。
在事务中,尽量一次锁定所有需要的资源,减少事务大小。
使用较低的隔离级别(如 Read Committed)可以减少间隙锁的使用,从而降低死锁概率。
为表添加合理的索引,避免全表扫描导致锁表。