事务与 ACID 原理

从数据库事务的 ACID 四大特性出发,深入讲解原子性、一致性、隔离性与持久性的实现机制,以及四种隔离级别下脏读、不可重复读与幻读的产生原因与解决方案。

事务(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 保障隔离性

深入理解不同隔离级别下的并发行为和实现原理,是设计高并发系统、诊断数据不一致问题的关键。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「数据库」更多文章

  1. 备份恢复与高可用方案
  2. 数据库性能监控与诊断
  3. NewSQL 选型对比