MySQL 事务的原理(ACID)
关键词:ACID、redo log、undo log、锁、MVCC、隔离级别。
目标:搞清楚“事务是什么、为什么能回滚、为什么能隔离”。
一、什么是事务?
在 MySQL(主要指 InnoDB 引擎)中,**事务(Transaction)**就是:
一组要么全部成功、要么全部失败的操作。
典型例子:转账
A 给 B 转 100 元:
1)A 账户减 100
2)B 账户加 100
这两个更新必须要么都成功,要么都不生效,否则就会出现“钱凭空出现或消失”的情况。
事务就是为了保证这类操作的整体性和安全性。
数据库里对事务的要求就是那 4 个字母:ACID。
二、ACID 四大特性概览
ACID 是事务必须满足的四个特性:
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
下面分别说是什么 + MySQL(InnoDB)是怎么做到的。
三、A:原子性(Atomicity)
3.1 概念
原子性 = 事务中的操作要么全部成功,要么全部失败。
不存在“只执行了一半”的状态对外可见。
转账例子:
- A 减了 100,B 没加 100 ⇒ 不允许;
- A 没减,B 加了 100 ⇒ 也不允许;
- 要么两个操作都成功,要么都回滚。
3.2 InnoDB 如何实现原子性?—— 依靠 undo log
- InnoDB 在执行更新前,会先记录一份“之前的数据”到 undo log(回滚日志);
- 如果事务回滚,就根据 undo log 把数据恢复到修改前的样子。
举个简单例子:
原始:A = 1000
事务中:UPDATE account SET balance = balance - 100 WHERE id = 'A';
内部分两部分:
- 把 A 从 1000 改成 900;
- 在 undo log 里记录一条信息:A 的 balance 从 1000 改成 900,可以还原为 1000。
如果事务失败 / 回滚:
- 通过 undo log 把 balance 从 900 改回 1000;
- 对外看起来,就像这个事务从来没发生过一样。
小结:
- undo log 是实现事务回滚的关键;
- 有了 undo log,才能保证“要么全做,要么全不做”的原子性。
四、C:一致性(Consistency)
4.1 概念
一致性 = 事务前后,数据库从一种合法状态转换到另一种合法状态。
“合法状态”通常指:
- 满足各种约束:主键约束、唯一约束、外键约束、检查约束等;
- 满足业务规则:比如总资产 = 各账户余额之和,不允许负数账户等。
一致性是一个结果属性:
- 它依赖于原子性、隔离性、持久性 + 应用自身的业务逻辑一起保障。
常见例子:
- 转账过程中的中间状态(比如 A 已减、B 未加)是不会对外暴露的;
- 事务提交成功后,数据必须满足各种约束条件,否则会回滚。
4.2 约束失败导致事务回滚
比如:
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20) UNIQUE
);
如果事务中插入了两个相同 name 的用户:
START TRANSACTION;
INSERT INTO user(id, name) VALUES(1, 'Tom');
INSERT INTO user(id, name) VALUES(2, 'Tom'); -- 冲突
COMMIT; -- 提交时发现唯一约束冲突
这个事务会失败并回滚,不一致的数据不会被提交。
小结:
- 一致性更多依赖于:数据库的各种约束 + 应用层逻辑 + 事务本身的 A / I / D。
- MySQL 负责在约束被破坏时让事务提交失败,从而保证最终一致性。
五、I:隔离性(Isolation)
5.1 概念
隔离性 = 并发执行的多个事务之间,在一定程度上互相“隔离”,互不干扰。
理想情况:
- 多个事务并发执行的结果,应该等价于某种顺序(串行)执行的结果。
- 这就是所谓的“可串行化(Serializable)”。
但完全串行执行会让性能掉死,所以数据库提供了多个隔离级别,在“性能”和“隔离性”之间做平衡。
5.2 隔离级别(四种)
SQL 标准定义了 4 个隔离级别(MySQL InnoDB 全支持):
- READ UNCOMMITTED(读未提交)
- READ COMMITTED(读已提交)
- REPEATABLE READ(可重复读)——MySQL InnoDB 默认
- SERIALIZABLE(串行化)
5.2.1 读未提交(READ UNCOMMITTED)
问题:
- 可能出现脏读(Dirty Read):
读到别的事务还没提交的修改。
一般不开这个级别。
5.2.2 读已提交(READ COMMITTED)
特点:
- 只能读到已经提交的数据;
- 避免了脏读。
问题:
- 可能出现不可重复读(Non-Repeatable Read):
同一个事务中,两次查询同一行数据,结果可能不一样。
5.2.3 可重复读(REPEATABLE READ)——InnoDB 默认
特点:
- 同一个事务里,多次读取同一行数据,结果保持一致;
- 避免了脏读 + 不可重复读。
问题:
- 仍然可能出现幻读(Phantom Read):
同一个事务中,多次按条件查询,行数可能不一样(有新插入的数据“凭空出现”)。
InnoDB 在这个级别下,通过 MVCC + 间隙锁(Next-Key Lock) 组合手段来“解决/缓解”幻读问题。
5.2.4 串行化(SERIALIZABLE)
特点:
- 强制所有事务串行执行,或者在读写时加上更严格的锁;
- 隔离性最好,但并发能力最差;
- 一般只在对数据一致性要求极端严格且并发不高时使用。
5.3 InnoDB 如何实现隔离?—— MVCC + 锁
隔离性背后有两套核心机制:
- MVCC(多版本并发控制)
- 锁(行锁、间隙锁、意向锁等)
5.3.1 MVCC(多版本并发控制)
MVCC 的目标:
在大多数“读”的场景下,让读操作不阻塞写,写操作也尽量不阻塞读。
做法:
- 每行记录有多个版本(通过 undo log + 隐藏列 来实现);
- 每个事务在开始时,会拿到一个“事务 ID”并创建一致性视图(Read View);
- 读数据时:根据事务 ID + 行的版本信息,找到对当前事务可见的版本。
通俗理解:
- 写数据时,会产生新版本,旧版本保留在 undo log 中;
- 读数据时,如果当前最新版本对该事务不可见,就去看更早的版本;
- “读已提交 / 可重复读”的区别,就是“视图”是每次查询新建还是事务开始时固定。
5.3.2 锁
简单按粒度分:
- 行锁:锁某一行记录;
- 间隙锁(Gap Lock):锁一个范围(间隙),防止插入;
- Next-Key Lock:行锁 + 间隙锁,锁
[前一个值, 当前值]区间; - 表锁:锁整张表(InnoDB 也有,但事务中一般用的是行锁+间隙锁)。
按用途分:
- 共享锁(S 锁):读锁,可多事务共享;
- 排他锁(X 锁):写锁,互斥。
InnoDB 默认是 行级锁 + MVCC:
- 查询时尽量通过 MVCC 读取“历史版本”,避免加锁;
- 修改时才会加行锁 / 间隙锁,保证并发修改不会乱。
小结:
- MVCC 负责“读写互不挡”;
- 锁负责“写写不乱 + 控制插入/删除范围”;
- 两者配合实现各种隔离级别。
六、D:持久性(Durability)
6.1 概念
持久性 = 一旦事务提交,它对数据库所做的修改就是永久性的,即使发生故障也不会丢失。
比如:
- 事务提交成功 → 服务器立刻宕机 → 重启后,已提交的数据仍然存在。
6.2 InnoDB 如何实现持久性?—— redo log + binlog
主要依赖两类日志:
- redo log(重做日志)—— InnoDB 引擎级别
- binlog(二进制日志)—— MySQL Server 层级别
6.2.1 redo log:物理日志,保证崩溃恢复
- 记录的是“对某个数据页做了怎样的物理改动”;
- 写入顺序追加,I/O 顺序性较好;
- 在事务提交前,相关的 redo log 必须刷到磁盘(或根据参数控制刷盘策略)。
简化流程:
- 事务执行期间:
- 修改内存中的数据页(Buffer Pool);
- 生成 redo log 写入 redo 日志缓冲区;
- 事务提交时:
- 至少要保证 redo log 已经刷新到磁盘(取决于
innodb_flush_log_at_trx_commit设置);
- 至少要保证 redo log 已经刷新到磁盘(取决于
- 崩溃恢复:
- 重启时读取 redo log,根据其内容重新把“已经提交但尚未写入磁盘的数据页”恢复出来。
有 redo log,在“先改内存、后刷磁盘”的情况下,仍能保证事务一旦提交就不会丢。
6.2.2 binlog:逻辑日志,主从复制 & 恢复用
- 记录的是“SQL 语句或行级别的逻辑变更”;
- 是 MySQL Server 层的日志,不只 InnoDB 使用;
- 常用于:主从复制、数据恢复(如
mysqlbinlog回放)。
6.2.3 redo log + binlog + 两阶段提交
为保证:
- 事务提交后既不会丢(依赖 redo log);
- 又能被正确复制到从库/用于恢复(依赖 binlog);
InnoDB 使用两阶段提交机制协调 redo log 和 binlog:
大致过程:
- 写入 redo log,标记为
prepare状态; - 写入 binlog;
- 将 redo log 标记为
commit状态;
这样:
- 崩溃恢复时,如果 redo 是 prepare 而 binlog 不完整,可以回滚;
- 如果 redo 是 commit,说明 binlog 也已经写好了,可以重放。
小结:
- redo log:保证“崩溃恢复”级别的持久性;
- binlog:保证“复制 / 审计 / 逻辑恢复”;
- 两者配合 + 两阶段提交,确保事务提交后既不丢,又能被正确复制与恢复。
七、ACID 与 MySQL 关键机制的对应关系
可以简单打个表:
| 特性 | 主要依赖的机制 |
|---|---|
| 原子性 A | undo log(回滚日志)、崩溃恢复流程 |
| 一致性 C | 约束(PK/UK/FK 等)+ 应用逻辑 + A/I/D 综合效果 |
| 隔离性 I | 锁(行锁、间隙锁)+ MVCC + 隔离级别 |
| 持久性 D | redo log + binlog + 刷盘策略 + 两阶段提交 |
注意:
- 一致性并不是由某一个内部机制直接保证,而是 ACID 四个特性 + 业务规则共同保证。
八、总结:从“表面行为”到“内部原理”
-
事务是什么?
- 一组操作的逻辑整体,要么全部成功、要么全部失败。
-
为什么能回滚?
- 因为有 undo log,记录了修改前的值。
-
为什么不会互相乱改?
- 因为有 锁 控制并发写;
- 有 MVCC 让读尽量不用加锁,又能看到一致视图。
-
为什么断电后数据还在?
- 因为有 redo log 刷盘;
- 重启时可以根据 redo 把已经提交的数据恢复出来。
-
为什么叫 ACID?
- A:undo log 支撑回滚 → 原子性;
- C:约束 + 逻辑 + A/I/D 综合保证一致性;
- I:锁 + MVCC + 隔离级别控制并发可见性;
- D:redo log + binlog + 两阶段提交保证持久性。
理解 ACID 背后的这些“日志 + 锁 + 版本”的组合拳,你基本就把 MySQL 事务的核心原理拿下来了。
85万+

被折叠的 条评论
为什么被折叠?



