前言
在MySQL InnoDB引擎中,事务ACID特性是数据库可靠存储的基石。redo log保证事务持久性,undo log实现事务原子性,锁机制解决写写冲突 ,MVCC多版本并发控制实现读写不阻塞。这四者相互配合,在保证数据一致性的前提下支撑高并发业务。很多面试中锁、MVCC、事务隔离级别问题,本质都是考察这几个组件如何协同工作。
java
ACID
├─ A 原子性 → undo log
├─ D 持久性 → redo log
└─ I 隔离性 → 锁 + MVCC
C 一致性:原子、隔离、持久共同保障的最终结果
一、事务ACID四大特性
事务是一组不可拆分的SQL操作集合,要么全部执行成功,要么全部失败回滚。ACID是事务必须满足的四个特性:
A --- Atomicity 原子性
事务是最小执行单元,事务内所有操作,要么全部成功,要么全部回滚。
- 底层实现:undo log(回滚日志)
- 原理:修改数据前,会把数据旧镜像写入undo log。事务失败回滚时,利用undo log还原数据;事务提交后,undo log不会立刻删除,由Purge线程清理不再被MVCC引用的版本。
C --- Consistency 一致性
事务执行前后,数据库状态始终保持合法。不会出现违反数据约束的非法数据。
一致性是最终目标 ,不是由单一组件实现。原子性、隔离性、持久性三者共同保证一致性。
约束包括:主键唯一、外键约束、字段非空、业务层面数据平衡(例如转账前后账户总金额不变)。
I --- Isolation 隔离性
多个并发事务之间相互隔离,防止并发事务带来的数据异常。隔离级别定义并发事务之间数据可见性规则。并发会产生三类经典问题:脏读、不可重复读、幻读。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted(读未提交) | ✅存在 | ✅存在 | ✅存在 |
| Read Committed(读已提交 RC) | ❌不存在 | ✅存在 | ✅存在 |
| Repeatable Read(可重复读 RR,MySQL默认) | ❌不存在 | ❌不存在 | 快照读无幻读;当前读依靠临键锁防止幻读 |
| Serializable(串行化) | ❌不存在 | ❌不存在 | ❌不存在 |
隔离级别越高,并发性能越低;隔离级别越低,并发越高,但数据异常风险越大。
D --- Durability 持久性
事务一旦提交成功,修改永久落盘,即使数据库宕机、断电,数据也不会丢失。
- 底层实现:redo log(重做日志)
- 原理:事务提交时优先写入redo log,内存中的脏页不会立刻刷磁盘。数据库崩溃重启后,通过redo log重放已经提交事务,恢复数据。
📌ACID组件关系图

二、redo log 与 undo log 概念以及区别
redo log(重做日志)
核心作用:保障事务持久性D
- 记录内容:修改之后的数据页内容,属于物理日志。
- 写入时机:事务执行过程不断写入redo log buffer;事务提交时刷入磁盘redo log文件。
- 使用场景:数据库宕机,内存脏页没有刷到磁盘,重启数据库,读取redo log重放已经提交事务。
- 相关参数:
innodb_log_file_size、innodb_flush_log_at_trx_commit。
📌redo log 刷盘流程|WAL预写日志流程

undo log(回滚日志)
核心作用:保障事务原子性A;同时为MVCC提供历史数据版本
- 记录内容:修改之前的旧数据,属于逻辑日志。update记录旧值,delete记录反向insert操作。
- 写入时机:修改数据之前写入undo log。
- 使用场景:
- 事务回滚,利用undo log还原旧数据,保证原子性;
- MVCC快照读,沿着版本链读取历史版本。
- 分类:
- insert_undo:insert语句产生,事务提交直接删除,不参与MVCC;
- update_undo:update/delete产生,用于MVCC,由Purge线程回收。
📌undo log 版本链示意图

redo log vs undo log对比表
| 对比维度 | redo log | undo log |
|---|---|---|
| 核心职责 | 持久性D | 原子性A + MVCC多版本 |
| 记录内容 | 修改之后的数据页 | 修改之前的旧数据 |
| 日志类型 | 物理日志 | 逻辑日志 |
| 核心作用 | 崩溃恢复,恢复已提交修改 | 事务回滚、提供历史快照版本 |
| 生命周期 | 崩溃恢复完成即可丢弃 | 事务提交后,若仍被MVCC引用,则保留,Purge线程清理 |
| 写入时机 | 事务执行中,提交刷盘 | 修改数据之前写入 |
| 解决问题 | 宕机丢失已提交事务 | 事务回滚、快照读 |
补充:binlog属于MySQL Server层日志,用于主从复制、数据备份,不属于ACID底层保障日志。
三、MySQL InnoDB锁机制全解
InnoDB锁基于索引实现,如果没有命中索引,行锁会退化成表锁。所有锁在事务提交或者回滚之后释放。
📌锁的分类结构图

3.1 共享锁S & 排他锁X
- S共享锁(读锁) :
SELECT ... LOCK IN SHARE MODE。多个事务可以同时加S锁;S锁和X锁互斥。 - X排他锁(写锁) :UPDATE、DELETE、INSERT、
SELECT ... FOR UPDATE。X锁和所有锁互斥。
3.2 表级锁
- 意向锁 IS / IX:表级锁,InnoDB自动添加。作用:高效判断行锁与表锁之间冲突。加行S锁前先加IS锁;加行X锁前先加IX锁。意向锁之间互相兼容。
- MDL元数据锁:保护表结构。DML语句获取MDL读锁(读锁之间兼容);DDL语句获取MDL写锁(写锁互斥)。长事务持有MDL读锁,会阻塞DDL,进而阻塞后续所有DML,是线上常见故障点。
LOCK TABLES:手动表锁,生产环境禁止使用。
3.3 行级锁(RR隔离级别下生效)
- 记录锁 Record Lock:锁定索引上某一条具体记录。
- 间隙锁 Gap Lock:锁定索引记录之间的间隙,不锁定记录本身。RC隔离级别没有间隙锁;间隙锁之间互相兼容,主要防止其他事务在间隙插入数据。
- 临键锁 Next‑Key Lock = 记录锁 + 间隙锁,左开右闭区间,RR默认行锁算法,用来解决当前读幻读 。
- 唯一索引等值查询命中记录 → 临键锁退化为记录锁;
- 唯一索引等值查询找不到记录 → 退化为间隙锁。
📌临键锁区间示意图

3.4 特殊锁
- 插入意向锁:INSERT操作时在间隙上加的锁。多个事务向同一个间隙的不同位置插入数据互不阻塞。
- AUTO‑INC锁 :自增主键ID分配锁,通过
innodb_autoinc_lock_mode控制并发策略。
锁兼容性矩阵
| 锁类型 | X锁 | IX锁 | S锁 | IS锁 |
|---|---|---|---|---|
| X锁 | 互斥 | 互斥 | 互斥 | 互斥 |
| IX锁 | 互斥 | 兼容 | 互斥 | 兼容 |
| S锁 | 互斥 | 互斥 | 兼容 | 兼容 |
| IS锁 | 互斥 | 兼容 | 兼容 | 兼容 |
死锁
- 死锁产生4个必要条件:互斥条件、占有且等待、不可剥夺、循环等待。破坏任意一条即可避免死锁。
- 常见场景:事务交叉顺序更新数据、范围查询触发临键锁、无索引更新退化为表锁。
- 规避方案:固定事务加锁顺序、SQL必须走索引、避免长事务、RC隔离级别减少间隙锁。
四、MVCC多版本并发控制
MVCC全称Multi‑Version Concurrency Control,多版本并发控制。实现快照读不加锁,读写不阻塞,RC、RR隔离级别生效。
4.1 MVCC三大核心组件
- 隐藏列(每行数据自带)
DB_TRX_ID:6字节,最近修改该行记录的事务ID;DB_ROLL_PTR:7字节,回滚指针,指向undo log里旧版本数据;DB_ROW_ID:6字节,没有主键时的隐藏主键,非MVCC核心。
- undo log:存放旧版本数据,串联成版本链。
- 版本链 :多次修改同一行数据时,
DB_ROLL_PTR不断指向undo log历史版本,形成版本链表,链表头部是最新数据。
4.2 ReadView 可见性视图
ReadView是事务查询时生成的快照,用来判断当前事务可以看到哪个版本的数据,包含4个字段:
m_ids:创建ReadView时刻,所有未提交活跃事务ID集合;min_trx_id:m_ids集合里面最小事务ID;max_trx_id:创建ReadView时,系统将要分配的下一个事务ID;creator_trx_id:创建ReadView的当前事务ID。
📌ReadView可见性判断流程图

可见性判断逻辑:
row_trx_id == creator_trx_id:数据是当前事务自己修改的,可见;row_trx_id < min_trx_id:修改该数据的事务已经提交,可见;row_trx_id >= max_trx_id:这个版本是 ReadView 创建之后才产生,不可见;row_trx_id 在 m_ids集合中:事务还未提交,不可见;否则可见。
4.3 RC 与 RR 隔离级别 ReadView 差异
- RC(读已提交) :每一条 SELECT 语句都会新建 ReadView。每次查询都会读取最新提交的数据,产生不可重复读。
- RR(可重复读) :事务第一条 SELECT 创建 ReadView,后续 SELECT 复用同一个 ReadView。同一个事务多次查询看到相同快照,实现可重复读。
4.4 快照读 vs 当前读
- 快照读 :普通
SELECT语句,不加锁,读取版本链历史数据。MVCC 的应用场景。 - 当前读 :UPDATE、DELETE、INSERT、
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE,加锁读取最新版本数据,依赖锁机制保证隔离。
RR 隔离级别:快照读依靠 MVCC 屏蔽幻读;当前读依靠临键锁锁住索引间隙,阻止插入新数据,避免幻读。
4.5 Purge 线程
Purge 线程负责清理不再被任何 ReadView 引用的 undo log 历史版本。长事务会持续持有 ReadView,阻止 Purge 清理 undo log,造成 undo log 膨胀,磁盘占用持续上涨,数据库性能下降。
五、SQL 演示:脏读、不可重复读、幻读
准备测试表,新建两个 MySQL 会话:T1、T2。
SQL
CREATE TABLE test_account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
money INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO test_account(name,money) VALUES ('A',100),('B',200);
实验 1:脏读(Read Uncommitted 读未提交)
脏读:一个事务读到另一个事务未提交的数据。
SQL
-- T1会话
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
-- T2会话
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE test_account SET money = 999 WHERE id=1; -- T2修改,但不提交
-- T1查询,可以读到T2未提交的money=999,发生脏读
SELECT * FROM test_account;
-- T2执行回滚
ROLLBACK;
RC 及以上隔离级别,不会出现脏读。
实验 2:不可重复读(RC 读已提交)
不可重复读:同一个事务内,两次读取同一行记录,中间被其他事务修改并提交,两次查询结果不一致。
SQL
-- T1会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM test_account WHERE id=1; -- 第一次读取 money=100
-- T2会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE test_account SET money=666 WHERE id=1;
COMMIT; -- T2提交修改
-- T1再次查询,读到money=666,同一事务两次结果不一致,不可重复读
SELECT * FROM test_account WHERE id=1;
RR 隔离级别不会出现不可重复读,因为事务复用同一个 ReadView。
实验 3:幻读演示
幻读:同一事务,相同范围查询,前后查询行数不一致,其他事务插入 / 删除满足条件的数据。
场景:RC 隔离级别,当前读,出现幻读
SQL
-- T1会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM test_account WHERE id>1 FOR UPDATE; -- 当前读
-- T2会话
START TRANSACTION;
INSERT INTO test_account(name,money) VALUES ('C',300);
COMMIT;
-- T1再次当前读,可以读到T2新插入的数据,出现幻读
SELECT * FROM test_account WHERE id>1 FOR UPDATE;
RR 隔离级别下执行同样
select ... for update会触发临键锁,锁住索引间隙,T2 无法执行 INSERT,不会出现当前读幻读。
六、总结与生产实践建议
- ACID:原子性由 undo log 保障;持久性由 redo log 保障;隔离性依靠锁 + MVCC;一致性是最终目标。
- redo log 保存修改后数据页,用于崩溃恢复;undo log 保存旧数据,用于事务回滚和 MVCC 版本链。
- MVCC 依靠隐藏字段 + undo 版本链 + ReadView 实现快照读无锁;RC 每次查询新建 ReadView,RR 事务第一次 select 创建并复用 ReadView。
- 锁解决写写冲突;MVCC 解决读写冲突。RR 隔离级别:快照读依靠 MVCC 防止幻读,当前读依靠临键锁防止幻读。
- 生产实践建议
- 高并发业务可以考虑 RC 隔离级别,无间隙锁,并发性能更好;但 RC 会存在不可重复读、当前读幻读,业务层需要兼容;
- DML 语句务必保证命中索引,否则行锁退化为表锁;
- 禁止长事务,长事务会引发 MDL 阻塞、undo log 膨胀;
- DDL 操作避开业务高峰,防止 MDL 锁阻塞线上业务;
- 业务尽量避免大范围范围更新,防止临键锁锁定大量索引区间,引发锁等待、死锁。