一、前言
本文从 InnoDB 两套读写模型切入,结合官方术语一致性非锁定读(快照读)、一致性锁定读(当前读),逐层解析事务 ACID、四大隔离级别、MVCC 底层原理、锁体系与幻读误区,搭配时序案例与线上故障,完整梳理 MySQL 并发控制核心逻辑。
数据库事务隔离、锁、MVCC 机制生效的前提是多事务并发执行。如果事务串行执行,事务之间不会互相干扰,也就不需要复杂的隔离与并发控制体系。只有当多个事务同时读写数据库时,才会产生数据冲突、数据可见性异常,因此需要一套完整的并发控制方案。
InnoDB 依靠事务、锁、MVCC 三套核心体系,解决多事务并发读写的数据一致性问题,支撑 MySQL 绝大多数 OLTP 高并发业务: 事务:提供 ACID 隔离基础,定义并发执行边界,所有 CRUD 操作都依托事务上下文执行; MVCC 多版本并发控制:服务于一致性非锁定读(consistent nonlocking read,俗称快照读) ,实现无锁读写,解决高并发读场景的性能问题; 悲观锁体系:服务于一致性锁定读(locking read,俗称当前读),处理并发写冲突,保障数据强一致性。
两套读写模型贯穿全文:**一致性非锁定读(快照读)**依托 MVCC 实现无锁隔离,**一致性锁定读(当前读)**依托锁机制实现强一致隔离。隔离级别差异、三类读异常、锁冲突、幻读问题,本质上都是这两套机制在不同场景下的组合表现。本文整合底层原理、实战时序案例、源码细节、线上故障场景,完整梳理 InnoDB 并发核心逻辑。
二、事务核心基础(所有并发的前提)
2.1 事务 ACID 特性
事务具备原子性、一致性、隔离性、持久性四大核心特性。其中隔离性是并发控制的核心,用来约束多个并发事务之间的数据可见范围,解决事务间相互干扰的问题,由 MVCC 和锁机制共同落地实现。
2.2 隐式事务与显式事务
InnoDB 中不存在脱离事务的 SQL,所有 CRUD 操作都运行在事务上下文内。事务依附于数据库会话(连接),一个连接同一时刻仅能存在一个活跃事务。事务分为两类:
- 隐式事务(MySQL 默认):默认 autocommit=1,单条 SQL 自动包装为独立短事务,执行完成后立即自动提交,无需手动操作。普通查询、增删改语句都属于独立隐式事务。
- 显式事务:通过
begin / start transaction手动开启,多条 SQL 共用同一个事务快照与锁资源,必须手动执行commit提交或者rollback回滚。
核心重点:事务并非只用于写操作,只读 SELECT 同样属于只读事务,会生成 ReadView、持有 MDL 锁,并且受隔离级别约束。隔离级别分为全局级别与会话级别,会话级别仅作用于当前连接中新开启的事务,不会影响其他连接;隔离级别只管读操作的数据可见性,不会限制其他事务的写入操作,只会改变当前事务读取数据的规则。唯一特例:RR 隔离级别下**一致性锁定读(当前读)**使用的临键锁,可以物理阻塞其他事务插入数据,这是锁机制带来的效果,并不是隔离级别直接禁止写入。
三、InnoDB 两种读模型(全文逻辑枢纽)
术语映射(InnoDB 官方文档): 一致性非锁定读(consistent nonlocking read)= 快照读 一致性锁定读(locking read)= 当前读
隔离级别差异、MVCC 原理、锁机制,全部是这两种读模式在不同场景下的组合表现。阅读后续章节时,可以始终以「这条 SQL 是一致性非锁定读(快照读)还是一致性锁定读(当前读)」作为判断起点。
多事务并发的核心矛盾分为读写冲突、写写冲突,InnoDB 通过两套机制分别处理: 写写冲突、**一致性锁定读(当前读)**读写冲突:由悲观锁解决,保障强一致性,存在互斥阻塞; **一致性非锁定读(快照读)**读写冲突:由 MVCC 解决,无锁设计,高并发、互不阻塞。
3.1 一致性非锁定读(快照读,consistent nonlocking read)
普通不加锁的 SELECT 查询属于一致性非锁定读(快照读),也是 MVCC 唯一的应用场景。快照读不会读取数据库内最新的数据,而是读取 undo log 生成的历史数据快照,全程不加行锁。
核心价值:读写完全互不阻塞。当其他事务正在更新某一行并持有排他锁时,快照读不会被阻塞,直接读取该行历史版本,大幅提升数据库并发吞吐量。数据可见性完全由 ReadView 读视图规则控制。
3.2 一致性锁定读(当前读,locking read)
**一致性锁定读(当前读)**的核心是读取数据库最新的数据版本,读取的同时会对索引记录加锁,保证数据在事务提交前不会被其他事务修改,以此实现强一致性。
update、delete 属于写语句,但底层执行时会先执行读取操作。当前读描述的是读取数据的行为,不是整条 SQL 的语句类型。
所有写类 DML 语句执行分为两步: 第一步(一致性锁定读/当前读):读取索引上最新版本数据,同时加锁; 第二步(写操作):基于读到的最新数据,执行修改/删除逻辑。
因此 update/delete 本质是一致性锁定读(当前读) + 写入修改的组合操作,天然属于当前读范畴。而 select ... for update / lock in share mode 是单纯的一致性锁定读,没有后续写入逻辑。
所有一致性锁定读(当前读)SQL 汇总: UPDATE / DELETE:原生一致性锁定读,自动加 X 排他锁 SELECT ... LOCK IN SHARE MODE:手动一致性锁定读,加 S 共享锁 SELECT ... FOR UPDATE:手动一致性锁定读,加 X 排他锁
**一致性锁定读(当前读)**依靠锁机制实现隔离,读写互相阻塞,牺牲并发能力换取数据强一致性。
四、并发三大读异常
脏读、不可重复读、幻读,都是读操作在并发场景下观测到的现象,不同读模式下,对异常的感知结果会存在区别。脏写不属于三类异常,InnoDB 在所有隔离级别下,都会通过行锁直接阻止脏写,不需要隔离级别介入。
4.1 脏读(读到未提交数据)
事务 A 读取到事务 B 尚未 commit 的修改数据。核心危害是对方事务可以回滚,导致当前事务读到虚假、无效的数据,业务逻辑不可用。
4.2 不可重复读(同一行数据前后不一致)
同一个事务内,前后两次查询同一行已有数据,查询结果不一致。原因是两次查询间隙,其他事务修改该行并提交。核心特征:聚焦已有行的内容更新。
4.3 幻读(行数异常变化)
同一个事务内,前后执行范围查询,结果集行数凭空增多或者减少。原因是其他事务在查询区间内执行了插入/删除并提交。核心特征:聚焦行的新增、删除,影响结果集总行数。
核心区分:不可重复读针对已有行更新 ,幻读针对行的新增/删除。
五、四大事务隔离级别(逐级递进+深度原理+误区详解)
隔离级别本质:控制当前事务一致性非锁定读(快照读)/**一致性锁定读(当前读)**的数据可见范围,决定是否能观测到脏读、不可重复读、幻读三类异常。其中 RR 隔离级别必须区分快照读与当前读,依靠两套机制分别处理异常。
5.1 读未提交 RU
无快照隔离、无间隙锁,直接读取内存最新数据,无论其他事务是否提交。三类读异常全部存在,没有隔离能力,生产环境完全禁用。
5.2 读已提交 RC
核心规则:每一次普通快照读(一致性非锁定读),都会新建全新 ReadView,仅识别已提交事务数据;RC 不存在间隙锁,只有记录锁。 ✅ 解决脏读:新视图过滤所有未提交事务的修改,杜绝脏读 ❌ 无法解决不可重复读:同一个事务多次查询会生成多个视图,可以读到后续提交的新数据 ❌ 无法解决幻读:没有间隙锁,一致性锁定读(当前读)仅锁定已有记录,其他事务可以随意插入新数据
一句话总结:RC 只能屏蔽未提交数据,无法保证同一个事务多次查询结果一致。
5.3 可重复读 RR(InnoDB 默认)
核心规则:事务内第一条**一致性非锁定读(快照读)**生成 ReadView,整个事务复用这一份固定快照;**一致性锁定读(当前读)**依托临键锁实现强隔离。RR 是唯一依靠两套机制分层解决异常的隔离级别。
5.3.1 快照读隔离(MVCC 实现)
固定 ReadView 快照,彻底解决脏读、**一致性非锁定读(快照读)**场景下的不可重复读;同时屏蔽事务开启之后新增的已提交数据,观测不到幻读现象(仅仅是看不见,不会阻止外部写入)。
5.3.2 当前读隔离(锁机制实现)
**一致性锁定读(当前读)**不走 MVCC 快照,依托 RR 独有的临键锁(记录锁+间隙锁),锁住索引区间与间隙,物理阻塞其他事务插入新数据,从根源避免当前读场景下的幻读。
5.3.3 关键误区纠正
❌ 错误:RR 完全消除幻读 ✅ 正确:MVCC 解决纯**一致性非锁定读(快照读)场景的幻读(看不见),临键锁解决纯 一致性锁定读(当前读)**场景的幻读(不让写)。但在快照读与当前读混合场景下,仍然有可能观测到幻读。原因:ReadView 固定不变,但事务自身通过一致性锁定读修改的数据,DB_TRX_ID 会标记为本事务ID,该记录对本事务快照读可见。
具体场景演示: 事务 A 在 RR 隔离级别执行快照读 SELECT * FROM t WHERE id > 10,查询得到 5 行;事务 B 插入 id=15 并提交。事务 A 执行一致性锁定读 UPDATE t SET name='x' WHERE id > 10,当前读命中B插入的id=15,该行DB_TRX_ID被更新为事务A的trx_id;事务A再次执行快照读,ReadView不会刷新,但这条新行DB_TRX_ID等于自身事务ID,对A可见,读到6行,幻读现象发生。
时序梳理:
- 事务A:
select * from t where id>10,一致性非锁定读(快照读),返回5行,生成固定ReadView- 事务B:
insert into t values(15, 'xxx'); commit;插入并提交id=15- 事务A:
update t set name='x' where id>10;一致性锁定读(当前读),读到id=15并更新,该行DB_TRX_ID修改为A的事务ID- 事务A:
select * from t where id>10;一致性非锁定读(快照读),返回6行;新记录trx_id是当前事务A的ID,满足可见规则
5.4 串行化 Serializable
最高隔离级别,InnoDB 自动将普通 SELECT 转为 SELECT ... LOCK IN SHARE MODE,所有读操作都变成一致性锁定读(当前读),读写互相阻塞、事务串行执行。
可以彻底解决三类读异常,但并发性能极差、吞吐量很低,几乎不用于业务场景。
5.5 四大隔离级别超级汇总表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 核心实现原理 |
|---|---|---|---|---|
| 读未提交 RU | 存在 | 存在 | 存在 | 无任何隔离机制,直接读最新内存数据 |
| 读已提交 RC | 不存在 | 存在 | 存在 | 每次一致性非锁定读新建视图;仅记录锁、无间隙锁 |
| 可重复读 RR | 不存在 | 不存在(快照读) | 不存在(纯场景) | 事务固定ReadView + 临键锁双重防幻读(存在混合读写漏洞) |
| 串行化 Serializable | 不存在 | 不存在 | 不存在 | 所有读自动转为一致性锁定读加共享锁,读写完全互斥 |
六、MVCC 深度原理
MVCC 全称多版本并发控制,仅作用于一致性非锁定读(快照读),是 InnoDB 实现高并发读写互不阻塞的核心。核心组成:undo 版本链(数据多版本存储)+ ReadView(版本可见性规则)。
6.1 版本链:行隐藏字段、trx_id底层细节与undo版本链
行隐藏字段
每条聚簇索引记录自带两个核心隐藏字段,是 MVCC 实现的基础:
- DB_TRX_ID:最后修改该行数据的写事务 ID
- DB_ROLL_PTR:回滚指针,指向 undo log 中的上一历史版本
trx_id 底层细节
- 仅写事务(INSERT/UPDATE/DELETE)会分配全局自增 trx_id,纯只读事务没有真实 trx_id,标记为 0;
- MySQL5.7 使用 32 位 trx_id,上限约42亿。并非达到上限立刻归零循环;达到上限后会暂停分配新事务ID,等待旧事务ID被Purge回收后,才能继续复用。长事务会阻碍旧ID回收,极端高并发场景下可能引发事务ID分配停顿。MySQL 8.0.17 升级为64位trx_id,基本不存在耗尽风险;8.0.0~8.0.16版本依旧是32位。
- 旧 trx_id 需要等 undo 版本被 Purge 清理后才能回收复用,长事务会阻塞回收,造成 trx_id 积压;
- 只读事务没有 trx_id,不会影响 ReadView 采集全局活跃事务与可见性判断。
undo版本链
版本生成完整流程:执行 UPDATE/DELETE 修改数据时,InnoDB 不会直接覆盖原有数据:
- 先将聚簇索引中当前完整最新行数据拷贝到 undo log,生成旧版本;
- 更新聚簇索引最新行的业务数据;
- 更新当前行 DB_TRX_ID 为当前写事务 ID;
- 更新 DB_ROLL_PTR 指向刚生成的 undo 旧版本;
- 多次更新会持续生成历史版本,通过回滚指针串联成 undo 版本链。
- 核心规则:聚簇索引存储最新数据,undo log 只存放历史旧版本。
6.2 ReadView:字段定义与版本可见性判断逻辑
字段定义
ReadView 是一致性非锁定读的「可见性裁判」,四个成员变量决定版本筛选规则(m_ 为源码成员变量前缀):
- m_creator_trx_id:创建视图的当前事务 ID
- m_ids:视图创建瞬间,全局所有活跃未提交写事务 ID 集合
- m_min_trx_id:活跃事务最小 ID
- m_max_trx_id:下一待分配的事务 ID
MVCC 可见性判断逻辑
从版本链最新版本向旧版本逐一遍历,命中第一条可见版本就直接返回:
- DB_TRX_ID == m_creator_trx_id:当前事务自身修改的数据,直接可见;
- DB_TRX_ID < m_min_trx_id:修改事务在视图创建前已提交,版本可见;
- DB_TRX_ID >= m_max_trx_id:修改事务在视图创建后开启,版本不可见;
- 区间内事务ID:不在 m_ids(已提交)则可见,在 m_ids(未提交)则不可见。
6.3 RC 与 RR 在 ReadView 上的核心差异
隔离级别差异的唯一核心:ReadView 创建时机不同。 RC 读已提交:每一次一致性非锁定读(快照读) ,都会新建全新 ReadView,可以读取后续提交数据,存在不可重复读; RR 可重复读:事务内第一条一致性非锁定读创建视图,全程复用,快照固定,以此实现可重复读。
6.3.1 RC、RR 并发时序实战案例
前置数据:id=1,name='王',开启事务A、事务B并发执行
| 时序 | 事务A | 事务B | 现象解释 |
|---|---|---|---|
| T1 | begin | begin | 仅开启事务,无ReadView生成 |
| T2 | select 查询(结果:王) | - | RR生成固定视图;RC生成临时视图(一致性非锁定读) |
| T3 | - | update name='李' 未提交 | B未提交,AB均看不到新数据 |
| T4 | select 查询(结果:王) | - | 未提交数据被过滤,无脏读(一致性非锁定读) |
| T5 | - | commit | B事务提交,数据更新完成 |
| T6 | select 查询 | - | RR返回王(可重复读);RC返回李(不可重复读) |
6.3.2 RR 幻读分层实战案例
案例1:快照读幻读(MVCC 屏蔽·看不见) 事务A开启事务并执行范围查询(一致性非锁定读,空结果),生成固定快照;事务B插入新数据并提交。事务A再次查询,依旧返回空。 原理:快照固定,看不到后续新增数据,观测无幻读,但真实数据已经写入数据库。
案例2:当前读幻读(临键锁阻止·不让写) 事务A执行范围查询 for update(一致性锁定读),触发临键锁锁住索引间隙;事务B执行插入直接阻塞,无法写入新数据,物理杜绝幻读。
6.4 Purge 线程与 undo 清理机制
事务提交之后 undo 不会立刻删除,后台 Purge 线程异步回收空间。核心规则:只要存在活跃长事务依赖旧快照,对应的 undo 版本就全部无法清理。
长事务带来的影响: 阻塞 Purge 线程,undo 日志持续堆积,磁盘占用暴涨 旧 trx_id 无法回收,加剧事务 ID 复用压力 数据库读写性能持续下降,引发连锁故障
补充:mysqldump 搭配 --single-transaction 备份时会开启长快照事务(一致性非锁定读),同样会阻塞 undo 清理,需要避开业务高峰。
线上可通过 SHOW ENGINE INNODB STATUS 查看 History list length 字段,该值代表尚未被 Purge 清理的 undo 版本链长度。数值持续大于 10000 并且不断增长,说明存在长事务阻塞 Purge,需要排查活跃长事务。
6.5 MVCC 与业务乐观锁的区别
很多开发者容易混淆数据库MVCC和业务乐观锁,二者实现层次、解决的问题完全不同。
数据库MVCC:属于数据库底层机制,依靠undo版本链与ReadView控制一致性非锁定读的可见性,目标是实现读写不阻塞,本身并不会阻止并发写覆盖。
业务乐观锁(version版本号):是业务代码层实现的方案,一般在表中增加version字段,更新时校验版本号,专门用来防止并发写覆盖,和InnoDB底层MVCC没有关联。
七、InnoDB 锁体系(一致性锁定读强一致性保障)
MVCC 负责一致性非锁定读(快照读),锁体系负责**一致性锁定读(当前读)**与并发写冲突,两套机制互补,共同实现事务隔离。锁粒度从大到小:全局锁 > 表级锁(MDL锁、手动表锁、意向锁) > 行级锁。
7.1 全局锁
语法:FLUSH TABLES WITH READ LOCK,锁定全库,所有 DML、DDL 语句都会阻塞,仅用于特殊全量备份场景,生产环境极少使用。
7.2 表级锁:MDL锁 + 手动表锁 + 意向锁
7.2.1 MDL 元数据锁(Metadata Lock)
MDL 即元数据锁,访问一张表时会自动获取,锁持有周期覆盖整个事务生命周期,事务提交后才会释放,不会在单条语句执行完毕就释放。
作用:保护表结构元数据,保证查询、DML 执行期间,表结构不会被其他事务修改。
- 共享MDL(读MDL):SELECT / DML 语句自动获取,共享MDL之间互相兼容,多个事务可同时持有;
- 排他MDL(写MDL):ALTER、DROP、RENAME 等DDL语句申请获取,排他MDL和所有类型MDL互斥。
典型线上问题:长事务持有表的共享MDL,此时执行DDL会被阻塞;后续所有对该表的查询、DML语句都需要申请MDL,全部排队等待,短时间内连接大量堆积,触发线上故障。
7.2.2 手动表锁
手动显式添加的表读锁、表写锁,互斥性强,并发能力极低,业务开发场景基本不会使用。
7.2.3 意向锁 IS/IX
InnoDB 内部自动维护,无需手动操作。事务在加行锁之前,会先标记对应的表级意向锁,用于快速校验表锁冲突,提升加锁效率,对业务侧无感知。意向锁之间互相兼容,仅与手动表锁产生互斥。
7.3 行锁三大核心类型
重要知识点:InnoDB 的行锁是加在索引记录上,不是直接加在物理数据行上。 当 SQL 没有可用索引时,InnoDB 会走全表扫描,对扫描到的每一条聚簇索引记录加行锁。逻辑上是行锁,但因为遍历了整张表的索引,锁覆盖全部记录,效果等同于锁表。
-
记录锁 Record Lock 精准锁定单条索引记录,RC 隔离级别仅存在记录锁,不存在间隙锁。分为 S 共享锁、X 排他锁,读写互斥、读读兼容。
-
间隙锁 Gap Lock(RR 独有) 锁定两条索引之间的空隙,左开右开区间,仅阻止新数据插入,不锁定已有记录,是防幻读的核心基础。
-
临键锁 Next-Key Lock(RR 默认加锁单元) 临键锁 = 记录锁 + 间隙锁,左开右闭区间,锁定索引记录与前后间隙,彻底杜绝一致性锁定读场景下的幻读。
退化规则:唯一索引精准等值匹配时,临键锁退化为纯记录锁,不再锁定间隙;范围查询、二级索引场景不会退化,锁范围更大。
7.4 锁等待与锁超时机制
多个事务产生互斥锁冲突时,后申请的事务进入锁等待状态。InnoDB 通过 innodb_lock_wait_timeout(默认50秒)控制超时。
严谨表述:超时后,回滚该条SQL对应的锁申请以及本条SQL已经产生的数据修改;但事务本身不会自动整体回滚,事务仍然保持打开状态,应用程序可以继续执行后续SQL,由业务代码决定最终提交或者手动回滚。
锁等待常见诱因:长事务持有锁不释放、SQL 缺少索引导致锁范围放大、临键锁范围过大、热点行并发更新。 排查方案:通过 innodb_trx、innodb_lock_waits 定位阻塞事务,kill 长会话快速恢复业务。
八、死锁原理、案例与生产规避
8.1 死锁四大必要条件
互斥、持有并等待、不可剥夺、循环等待。四个条件同时满足,就会触发死锁。
8.2 经典死锁场景
两个事务交叉持有对方需要的行锁,形成环形等待。InnoDB 可以自动检测死锁,回滚代价更小的事务,解除死锁。
8.3 生产最优规避方案
统一多行数据更新顺序(按主键升序) 严格控制事务长度,杜绝长事务 避免单事务更新多条分散的热点数据
九、线上生产实战故障案例(长事务连锁风险)
长事务是 MySQL 线上绝大多数并发故障的根源,会引发多类核心故障:
案例1:长事务阻塞Purge,undo磁盘暴涨 长事务持有旧快照(一致性非锁定读),阻塞 undo 清理,日志持续堆积,磁盘占满会导致数据库写入瘫痪。解决:kill 长会话,等待 Purge 后台回收空间。规避:监控长事务、禁止闲置的手动长事务。
案例2:长事务持有MDL读锁,DDL阻塞引发雪崩 长事务没有提交,持续持有表的共享MDL锁。此时执行ALTER TABLE等DDL,申请排他MDL锁被阻塞;后续所有访问该表的SQL都需要申请MDL,全部排队阻塞,连接池快速耗尽,线上业务雪崩。
规避原则:DDL操作避开业务高峰;执行DDL前,优先排查并杀掉该表上存在的长事务。
案例3:长事务持有行锁,引发大面积业务超时 长事务持有行锁/临键锁不释放,后续同区间请求全部阻塞,连接池耗尽,接口雪崩。规避:事务内部不执行耗时操作,DML执行完成后立即提交。
案例4:trx_id耗尽风险 MySQL5.7、MySQL 8.0.0~8.0.16 使用32位trx_id,上限约42亿;达到上限后暂停分配新事务ID,等待旧ID被Purge回收。长事务会阻碍回收,超高并发场景下可能出现事务ID分配停顿。MySQL 8.0.17 及之后版本升级为64位trx_id,基本无此风险。
十、全文终极总结
1、InnoDB 并发控制分为两套核心体系:一致性非锁定读(快照读)依托 MVCC,一致性锁定读(当前读)依托悲观锁,各司其职、互补配合。 2、MVCC 通过 undo 版本链存储多版本数据,通过 ReadView 控制可见性;RC、RR 的差异仅在于 ReadView 的创建时机,RR 通过固定快照实现可重复读。 3、RR 隔离级别双层防幻读:MVCC 快照屏蔽一致性非锁定读 场景的幻读,临键锁物理阻止一致性锁定读场景的幻读,是 InnoDB 默认隔离级别的核心优势,但存在快照读+当前读混合场景的残余幻读漏洞。 4、所有写语句本质是「一致性锁定读+写入」,必须加锁保证写写互斥,杜绝脏写;一致性非锁定读无锁,实现高并发读写互不阻塞。 5、InnoDB行锁加载在索引上,无索引全表扫描会锁住全部索引记录,等效锁表;MDL元数据锁属于表级锁,锁周期贯穿整个事务,长事务阻塞DDL是线上高频故障点; 6、锁等待超时只会回滚单条SQL,不会自动回滚整个事务;线上并发故障、锁等待、undo 膨胀、性能衰减,根源大多来自长事务、索引失效、锁范围放大,生产环境需要重点监控。