事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个或多个 SQL 语句组成。事务的 ACID 特性是关系型数据库可靠性的基石,也是从单机走向分布式系统时面临的核心挑战。本文深入讲解 ACID 的每一项特性及其在 MySQL InnoDB 中的实现机制。
1. ACID 四大特性概述
| 特性 | 英文 | 定义 | 一句话理解 |
|---|---|---|---|
| 原子性 | Atomicity | 事务中的所有操作要么全部成功,要么全部失败回滚 | 不可半途而废 |
| 一致性 | Consistency | 事务执行前后,数据库从一个一致状态变为另一个一致状态 | 规则必须遵守 |
| 隔离性 | Isolation | 多个事务并发执行时互不影响 | 各安其位 |
| 持久性 | Durability | 事务一旦提交,即使系统故障,数据也不会丢失 | 落袋为安 |
2. 原子性(Atomicity):要么全成功,要么全失败
2.1 核心问题
银行转账是经典的原子性场景:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A'; -- 扣款
UPDATE accounts SET balance = balance + 100 WHERE id = 'B'; -- 加款
COMMIT;
如果第一步成功、第二步失败,A 少了钱但 B 没收到,数据处于不一致状态。原子性要求:这两步必须作为不可分割的整体执行。
2.2 InnoDB 实现:Undo Log
InnoDB 通过 Undo Log 实现原子性:
BEGIN Transaction T1
│
├─► UPDATE accounts SET balance = balance - 100 WHERE id = 'A'
│ └── 写入 Undo Log: balance 的旧值(比如 1000)
│ └── 写入 Redo Log: 确保 Undo Log 可恢复
│ └── 执行修改,A.balance 变为 900(脏页)
│
├─► UPDATE accounts SET balance = balance + 100 WHERE id = 'B'
│ └── 写入 Undo Log: balance 的旧值(比如 500)
│ └── 执行修改,B.balance 变为 600
│
├─► COMMIT
│ └── 写入 Redo Log commit 记录
│ └── 事务提交成功►► Undo Log 延迟清理(MVCC 需要)
│
└─► 若 ROLLBACK 中途失败或主动回滚
└── 读取 Undo Log,将 A.balance 恢复为 1000
└── 读取 Undo Log,将 B.balance 恢复为 500
事务状态标记为 回滚完成
3. 一致性(Consistency):规则永远有效
3.1 三种一致性视角
| 层面 | 定义 | 示例 |
|---|---|---|
| 数据库一致性 | 满足所有约束(主键、外键、CHECK、NOT NULL) | 转账后总额不变 |
| 外部一致性 | 业务规则与现实世界一致 | 库存不能为负 |
| 分布式一致性 | CAP 中的 C,多副本数据一致 | Raft 共识 |
3.2 ACID 中的一致性
ACID 中的一致性由 A、I、D 共同保障:
- 原子性确保操作不完整执行
- 隔离性防止并发破坏约束
- 持久性确保约束持久有效
同时需要开发者配合:正确的事务边界、业务校验、外键约束。
ALTER TABLE accounts ADD CONSTRAINT chk_balance_nonnegative
CHECK (balance >= 0);
-- 尝试透支会立即失败,数据库帮忙守住一致性底线
UPDATE accounts SET balance = balance - 200 WHERE id = 'A' AND balance >= 200;
4. 隔离性(Isolation):并发与隔离的权衡
4.1 为什么需要隔离
两个事务同时操作同一数据,会发生什么?
时间线:
T1: BEGIN T2: BEGIN
T1: UPDATE x=x-100
T2: SELECT x → ?
问题:T2 应该看到 x 的旧值还是新值?这取决于隔离级别。
4.2 隔离级别与并发异常
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式(InnoDB) |
|---|---|---|---|---|
| READ UNCOMMITTED | ✓ 允许 | ✓ 允许 | ✓ 允许 | 直接读取最新数据,无视锁 |
| READ COMMITTED | ✗ 不允许 | ✓ 允许 | ✓ 允许 | MVCC,每次 SELECT 生成新 Read View |
| REPEATABLE READ | ✗ 不允许 | ✗ 不允许 | ✗ 不允许 (*范围内) | MVCC,事务开始时生成 Read View |
| SERIALIZABLE | ✗ 不允许 | ✗ 不允许 | ✗ 不允许 | 所有 SELECT 加共享锁(S Lock) |
*InnoDB 的 REPEATABLE READ 通过 Gap Lock 解决了幻读,是 MySQL 的默认隔离级别。
4.3 三大并发异常详解
脏读(Dirty Read)
T1: BEGIN
T1: UPDATE accounts SET balance = 100 WHERE id = 1
(未提交)
T2: BEGIN
T2: SELECT balance FROM accounts WHERE id = 1 → 100(脏读!)
T1: ROLLBACK (事务回滚,balance 恢复为原值 1000)
结果: T2 读到了不存在的 100
解决:READ COMMITTED 及以上级别,SELECT 不读取未提交数据。
不可重复读(Non-Repeatable Read)
T1: BEGIN
T1: SELECT balance FROM accounts WHERE id = 1 → 1000
T2: BEGIN
T2: UPDATE accounts SET balance = 800 WHERE id = 1
T2: COMMIT
T1: SELECT balance FROM accounts WHERE id = 1 → 800(变了!)
T1: COMMIT
结果: 同一事务内两次读取结果不一致
解决:REPEATABLE READ 通过 Read View 快照读保证一致性视图。
幻读(Phantom Read)
T1: BEGIN
T1: SELECT * FROM accounts WHERE balance > 500 → [A, B, C](3条)
T2: BEGIN
T2: INSERT INTO accounts (id, balance) VALUES ('D', 600)
T2: COMMIT
T1: SELECT * FROM accounts WHERE balance > 500 → [A, B, C, D](4条!幻读)
T1: COMMIT
结果: 同一事务内两次范围查询结果行数不一致
解决:InnoDB REPEATABLE READ 通过 Gap Lock(间隙锁) 防止幻读。锁记录之间的间隙。
5. 持久性(Durability):数据永不丢失
5.1 WAL + Redo Log + Doublewrite
InnoDB 的持久性由多层保障:
用户 COMMIT:
│
├─► 事务日志先落盘(WAL 原则)
│ ├── Redo Log Buffer ──► fsync ──► Redo Log 文件(顺序写,高效)
│ └── Binary Log(如果开启)──► Sync
│
├─► 返回 SUCCESS 给客户端
│
└─► 脏页异步刷入数据表空间(后台 Page Cleaner)
└── Doublewrite Buffer 保护完整性
5.2 innodb_flush_log_at_trx_commit 参数
| 值 | 行为 | 数据安全 | 性能 | 适用场景 |
|---|---|---|---|---|
| 0 | 每秒刷盘一次 Redo Log | 差(可能丢1秒) | 最高 | 非核心数据 |
| 1(默认) | 每次 COMMIT 同步刷盘 | 最高 | 中 | 金融/核心系统 |
| 2 | 每次 COMMIT 刷日志缓存,每秒刷盘 | 高 | 高 | 折中方案 |
6. 两阶段锁与 MVCC
6.1 操作类型与锁
-- 排他锁(X Lock):写锁
SELECT ... FOR UPDATE;
UPDATE / DELETE / INSERT;
-- 共享锁(S Lock):读锁
SELECT ... LOCK IN SHARE MODE;
6.2 并发控制策略对比
| 策略 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| 两阶段锁(2PL) | 加锁、操作、解锁 | 实现简单 | 冲突多、死锁 |
| MVCC | 多版本并发控制 | 读写不阻塞 | 版本清理开销、幻读风险 |
| 乐观并发 | 先执行后校验(CAS) | 低冲突场景高效 | 高冲突回滚成本高 |
InnoDB 主要使用 MVCC + 间隙锁(Next-Key Lock) 实现 REPEATABLE READ 隔离级别。
7. 实际应用建议
| 场景 | 推荐隔离级别 | 原因 |
|---|---|---|
| OLTP 核心业务 | REPEATABLE READ(默认) | 防止幻读,保障报表一致性 |
| 报表/统计查询 | READ COMMITTED | 减少锁争用,允许新鲜数据 |
| 只读分析 | READ COMMITTED / Snapshot | 最大化并发 |
| 极短事务高并发 | READ COMMITTED | 减少 Gap Lock 开销 |
| 分布式全局事务 | SERIALIZABLE / 自研 | 强一致但性能极低 |
8. 总结
ACID 是关系型数据库的灵魂:
事务流程:
BEGIN ──► 获取事务 ID ──► 操作数据(写 Undo、Redo)──► COMMIT/ROLLBACK
│ │
├── Undo Log 保障原子性 ├── Redo Log 保障持久性
├── 约束/CHECK 保障一致性 └── Read View 保障隔离性
└── 锁/MVCC 保障隔离性
深入理解不同隔离级别下的并发行为和实现原理,是设计高并发系统、诊断数据不一致问题的关键。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。