12数据库系统概念(原书第7版)

事务管理

ACID 属性的实现

事务ACID状态转换并发执行

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第12章:事务管理(Transaction Management)

一、导读

1.1 本章学习目标

事务是数据库操作的逻辑单位。本章讲解事务的概念、ACID 属性、并发执行的问题。

本章的核心学习目标包括:

  • 理解事务的概念:数据库操作的逻辑单位
  • 掌握 ACID 属性:原子性、一致性、隔离性、持久性
  • 理解并发执行的问题:丢失更新、脏读、不可重复读、幻读
  • 掌握可串行化:冲突可串行化、视图可串行化
  • 了解事务隔离级别
  • 理解并发控制的实现机制:封锁、MVCC、时间戳排序

1.2 为什么事务管理如此重要

事务管理是数据库系统的核心功能。它保证:

  • 原子性:事务要么全部执行,要么全部不执行
  • 一致性:事务执行后数据库处于一致状态
  • 隔离性:并发事务互不干扰
  • 持久性:事务提交后数据永久保存

理解事务管理,对于开发可靠的数据库应用至关重要。无论是银行转账、机票预订还是电商下单,事务管理都是保证数据正确性的基石。

1.3 核心问题引导

在学习本章时,请思考以下问题:

如果没有事务机制,银行转账可能出现什么问题?

并发执行为什么需要控制?不控制会导致哪些异常?

可串行化为什么是并发控制的正确性标准?

隔离级别越高越好吗?如何选择合适的隔离级别?


二、核心概念详解

2.1 事务的概念

事务(Transaction)是数据库操作的逻辑单位,由一组操作组成。

sql
BEGIN TRANSACTION;
    UPDATE Account SET balance = balance - 100 WHERE id = 'A';
    UPDATE Account SET balance = balance + 100 WHERE id = 'B';
COMMIT;

事务的边界由 BEGIN 和 COMMIT/ROLLBACK 确定。COMMIT 表示事务成功完成,ROLLBACK 表示事务失败并撤销所有修改。

2.2 ACID 属性

2.2.1 原子性(Atomicity)

事务中的操作要么全部执行,要么全部不执行。

转账事务:
1. 从 A 账户扣除 100 元
2. 向 B 账户增加 100 元

如果步骤 1 成功但步骤 2 失败,必须回滚步骤 1。

实现机制:日志(Undo Log)。数据库在执行每个修改操作前,先将修改前的旧值写入日志。当事务需要回滚时,通过日志恢复旧值。

2.2.2 一致性(Consistency)

事务执行后,数据库从一个一致状态转换到另一个一致状态。

一致性约束:
- A 账户余额 + B 账户余额 = 总金额
- 转账前后总金额不变

实现机制:完整性约束、触发器、应用代码。一致性是事务的最终目标,其他三个属性(原子性、隔离性、持久性)都是为了保证一致性。

2.2.3 隔离性(Isolation)

并发事务的执行效果与串行执行相同。

事务 T1:读取 A 的余额
事务 T2:修改 A 的余额

T1 和 T2 并发执行时,T1 应该看到 T2 修改前或修改后的值,
不应该看到中间状态。

实现机制:锁、多版本控制。隔离性是并发控制的核心目标,需要在正确性和性能之间权衡。

2.2.4 持久性(Durability)

事务提交后,其对数据库的修改永久保存,即使系统崩溃也不会丢失。

实现机制:日志(Redo Log)、检查点。Redo Log 记录了所有修改操作,系统崩溃后可以通过重做日志恢复已提交事务的修改。检查点机制定期将内存中的数据写入磁盘,减少恢复时需要重做的日志量。

2.3 并发执行的问题

并发事务可能导致以下问题:

2.3.1 丢失更新(Lost Update)

两个事务同时修改同一数据,一个事务的修改被覆盖。

T1: 读取 A = 100
T2: 读取 A = 100
T1: A = A + 10, 写入 A = 110
T2: A = A + 20, 写入 A = 120  ← T1 的更新丢失

丢失更新是最基本的并发问题,任何隔离级别(除 READ UNCOMMITTED 外)都应该避免。

2.3.2 脏读(Dirty Read)

一个事务读取了另一个未提交事务的修改。

T1: 修改 A = 100
T2: 读取 A = 100  ← 脏读
T1: 回滚,A 恢复为 90
T2: 基于错误的 A = 100 继续执行

脏读的危险在于 T2 基于一个"不存在"的数据值做出了决策。

2.3.3 不可重复读(Non-Repeatable Read)

一个事务两次读取同一数据,结果不同。

T1: 读取 A = 100
T2: 修改 A = 200, 提交
T1: 再次读取 A = 200  ← 不可重复读

不可重复读影响需要多次读取同一数据的应用逻辑,如报表生成。

2.3.4 幻读(Phantom Read)

一个事务两次查询,第二次查询看到了第一次查询没有的行。

T1: 查询所有余额 > 100 的账户,得到 10 行
T2: 插入一个新账户,余额 150, 提交
T1: 再次查询所有余额 > 100 的账户,得到 11 行  ← 幻读

幻读与不可重复读的区别在于:不可重复读是已有行的修改,幻读是新行的插入。

2.4 可串行化

可串行化是并发控制的正确性标准。

2.4.1 串行调度

事务一个接一个执行,没有交错。

调度 1:T1 → T2
调度 2:T2 → T1

串行调度一定是正确的,但并发度为零,性能很差。

2.4.2 冲突可串行化

如果调度可以通过交换相邻的非冲突操作转换为串行调度,则它是冲突可串行化的。

冲突操作:

  • 同一数据项的读写(R-W 冲突)
  • 同一数据项的写读(W-R 冲突)
  • 同一数据项的写写(W-W 冲突)

两个读操作(R-R)不冲突,可以交换。

2.4.3 优先图(Precedence Graph)

优先图用于判断调度是否冲突可串行化:

  • 节点:事务
  • 边:如果 Ti 的某个操作在 Tj 的某个操作之前,且它们冲突,则有一条从 Ti 到 Tj 的边

如果优先图无环,则调度冲突可串行化。环的存在意味着事务之间存在循环依赖,无法排成线性顺序。

2.5 事务隔离级别

SQL 标准定义了四种隔离级别:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不可能可能可能
REPEATABLE READ不可能不可能可能
SERIALIZABLE不可能不可能不可能
sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

不同的数据库系统默认隔离级别不同:MySQL 默认 REPEATABLE READ,PostgreSQL 和 Oracle 默认 READ COMMITTED。


三、深入分析

3.1 事务状态转换

活跃 → 部分提交 → 提交
  ↓
失败 → 中止

事务的状态转换反映了事务的生命周期:

  • 活跃状态:事务正在执行操作
  • 部分提交状态:事务的最后一个操作已执行,但修改还在缓冲区中
  • 提交状态:事务的所有修改已持久化
  • 失败状态:事务检测到错误或系统故障
  • 中止状态:事务被回滚,数据库恢复到事务开始前的状态

3.2 并发控制的实现机制

  • 封锁(Locking):共享锁(S 锁)允许并发读,排他锁(X 锁)独占写。两阶段封锁协议(2PL)保证可串行化:事务分为扩展阶段(只加锁不释放)和收缩阶段(只释放不加锁)。
  • 时间戳排序:每个事务有唯一的时间戳,按时间戳顺序执行。冲突时通过回滚较新事务来解决。
  • 多版本并发控制(MVCC):维护数据的多个版本,读操作读取旧版本,写操作创建新版本。读写不冲突,大大提高了并发度。PostgreSQL 和 MySQL 的 InnoDB 引擎都使用 MVCC。

3.3 死锁处理

  • 死锁预防:等待-死亡(Wound-Wait)方案中,老事务可以抢占新事务的资源;受伤-等待(Wait-Die)方案中,新事务等待老事务,否则被中止。两种方案都通过限制等待关系来防止死锁。
  • 死锁检测:等待图(Wait-for Graph)中,如果存在环则发生死锁。数据库定期检测等待图,发现死锁后选择一个"牺牲者"回滚。
  • 死锁恢复:回滚代价最小的事务,释放其持有的锁,打破死锁环。

四、实践案例

4.1 案例一:银行转账事务

sql
START TRANSACTION;

UPDATE Account SET balance = balance - 100 WHERE id = 'A';
UPDATE Account SET balance = balance + 100 WHERE id = 'B';

COMMIT;

如果在两个 UPDATE 之间系统崩溃,原子性保证第一个 UPDATE 会被回滚,不会出现扣款成功但存款失败的情况。

4.2 案例二:使用 JDBC 控制事务

java
Connection conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);

try {
    Statement stmt = conn.createStatement();
    stmt.executeUpdate("UPDATE Account SET balance = balance - 100 WHERE id = 'A'");
    stmt.executeUpdate("UPDATE Account SET balance = balance + 100 WHERE id = 'B'");
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

在应用程序中控制事务时,务必在 catch 块中执行 rollback,并在 finally 块中关闭连接。

4.3 案例三:隔离级别的选择策略

  • READ COMMITTED:适合大多数 OLTP 场景,如 Web 应用。性能好,能避免脏读。
  • REPEATABLE READ:适合需要一致性读的场景,如报表生成。MySQL 默认级别。
  • SERIALIZABLE:适合对一致性要求极高的场景,如金融交易。性能最差。

选择隔离级别的原则:在满足业务需求的前提下,选择最低的隔离级别以获得最好的并发性能。


五、常见误区

5.1 误区一:"隔离级别越高越好"

隔离级别越高,并发度越低,性能越差。需要根据业务需求选择合适的隔离级别。大多数 Web 应用使用 READ COMMITTED 就足够了。

5.2 误区二:"事务越长越好"

长事务持有锁的时间长,降低并发度,增加死锁风险。应该尽量缩短事务的执行时间,将非数据库操作(如网络调用、文件操作)移出事务。

5.3 误区三:"REPEATABLE READ 完全消除了幻读"

MySQL 的 InnoDB 在 REPEATABLE READ 级别下通过间隙锁(Gap Lock)大幅减少了幻读,但并非完全消除。在某些特殊场景下(如先更新后插入),仍可能出现幻读现象。

5.4 难点:理解可串行化

可串行化是抽象的概念,需要通过优先图来理解。关键是要区分"冲突可串行化"和"视图可串行化":前者基于冲突操作的交换,后者基于读写视图的等价性。冲突可串行化是视图可串行化的充分条件。

5.5 难点:理解 MVCC

MVCC 通过维护数据的多个版本实现并发控制。每个事务看到的是事务开始时的数据快照,不会被其他事务的修改干扰。实现细节包括版本链、Read View 和 Undo Log 的配合使用。


六、本章小结

第12章讲解了事务管理:

事务 是数据库操作的逻辑单位

ACID 属性 保证事务的可靠性

并发执行 可能导致丢失更新、脏读、不可重复读、幻读

可串行化 是并发控制的正确性标准

隔离级别 提供不同级别的并发控制

后续章节将讲解并发控制的具体实现。


七、思考题

请用银行转账的例子,分别说明原子性、一致性、隔离性和持久性的含义。

丢失更新和不可重复读有什么区别?分别通过什么隔离级别可以避免?

什么是冲突可串行化?如何使用优先图判断一个调度是否冲突可串行化?

比较两阶段封锁协议(2PL)和 MVCC 的优缺点。各适合什么场景?

为什么长事务会降低数据库的并发性能?如何优化长事务?

MySQL 的 REPEATABLE READ 隔离级别是否完全消除了幻读?请说明原因。

设计一个场景,说明在 SERIALIZABLE 隔离级别下仍然可能出现的问题(如死锁)。

解释死锁的两种预防策略(等待-死亡和受伤-等待)的工作原理和区别。

12/17事务管理
0%