主题
第五章 事务管理与并发控制
1. 事务的基本概念
1.1 事务的定义
事务 (Transaction) 是数据库操作的逻辑工作单位,由用户定义的一组操作序列组成。这组操作要么全部成功执行,要么全部不执行,是一个不可分割的整体。在 SQL 中,事务通常以 BEGIN TRANSACTION 语句开始,以 COMMIT(提交)或 ROLLBACK(回滚)语句结束。COMMIT 表示事务正常结束,其所有对数据库的修改将被永久保存;ROLLBACK 表示事务异常结束,系统会将数据库恢复到事务开始之前的状态。
事务是数据库并发控制和故障恢复的基本单位。理解事务的概念和特性是掌握数据库系统运行机制的前提。
1.2 事务的 ACID 特性
事务必须满足原子性、一致性、隔离性和持久性四个特性,简称 ACID 特性。这四个特性共同构成了数据库系统保证数据正确性的基础。
原子性 (Atomicity) 是指事务中包含的所有操作,要么全部成功执行并持久化,要么全部不执行。即使事务在执行过程中发生了故障,已经执行过的操作也必须被完全撤消,使数据库回到事务开始之前的状态。原子性由 DBMS 的事务管理子系统通过撤销日志 (Undo Log) 来保证。当事务需要回滚时(无论是因为用户主动执行 ROLLBACK 还是系统在故障恢复时发现事务未完成),系统根据 Undo Log 中记录的旧值对已经做过的修改逐一执行逆向操作。
一致性 (Consistency) 是指事务的执行必须使数据库从一个一致性状态转变到另一个一致性状态。一致性状态是指数据库中的数据满足所有已定义的完整性约束条件。例如在一个银行转账事务中,无论转账操作的中间过程如何,事务完成后两个账户的余额之和不应该发生变化。如果事务的执行结果违反了任何完整性约束,系统必须阻止该事务的提交。一致性是事务追求的最终目标,它由原子性、隔离性和持久性共同来保障。
隔离性 (Isolation) 是指在并发执行环境中,每个事务的执行不应感知到其他并发事务的存在。一个事务内部的操作及其使用的数据对并发执行的其他事务是隔离的,其他事务不应在该事务的中间状态干预其执行。隔离性通过并发控制机制(主要包括封锁和多版本并发控制)来实现。不同的隔离级别提供不同强度的隔离保证,在隔离强度和系统吞吐量之间进行权衡。
持久性 (Durability) 是指一旦事务成功提交,它对数据库所做的修改就是永久性的,即使在提交之后系统立即发生崩溃,已提交的修改也不会丢失。持久性通过重做日志 (Redo Log) 和预写式日志技术 (WAL, Write-Ahead Logging) 来保证。WAL 的核心规则是:在任何数据修改被写入磁盘的数据文件之前,对应的日志记录必须先被写入磁盘的日志文件。由于日志的写入是顺序 I/O 操作,速度远快于数据文件的随机 I/O 写入,因此 WAL 在保证安全性的同时不会成为性能瓶颈。在系统重启后,恢复管理器会扫描日志文件,对已提交但数据页尚未写入磁盘的事务执行重做 (Redo) 操作,从而恢复这些事务的修改。
ACID 四个特性看上去是四个孤立的定义,实际上它们共同回答的是同一个问题:在并发执行和故障不可避免的世界里,数据库如何仍然让结果“看起来可靠”。很多业务事故并不是程序直接报错,而是库存被悄悄超卖、余额被覆盖、统计口径前后不一致。这类问题之所以难处理,恰恰因为它们表面上不一定崩溃,实际上却破坏了业务可信度。也正因为如此,事务管理并不是数据库内核才需要关心的主题,它直接决定业务系统能否让用户和管理者信任结果。
2. 并发操作带来的数据不一致问题
当多个事务同时对数据库中的同一数据进行读或者写操作时,如果系统不对这些并发操作进行适当的控制和协调,就可能产生各种不正确的结果。这些问题可以归纳为以下四类。
丢失修改 (Lost Update) 发生在两个事务同时读取并修改同一数据项的场景中。事务 T1 和 T2 先后读取数据项 A 的值(假设为 100),然后各自基于这个值进行修改。T1 给 A 加上 50 并提交(A 变为 150),随后 T2 给 A 减去 20 并提交(T2 是基于它当初读到的 100 来计算的,所以将 A 设为 80)。最终 A 的值为 80,T1 所做的加 50 操作被完全覆盖,就好像从来没有发生过一样。
读脏数据 (Dirty Read) 是指一个事务读取了另一个尚未提交的事务所修改的数据。如果后者随后执行了回滚操作,则前者读到的数据就是一个在数据库历史中从未真正有效存在过的值。例如事务 T1 将余额从 1000 修改为 2000 但尚未提交,此时事务 T2 读取到余额为 2000 并基于此值进行业务处理。之后 T1 因某种原因回滚,余额恢复为 1000。T2 此前读取到的 2000 就是脏数据。
不可重复读 (Non-repeatable Read) 是指一个事务在其执行过程中两次读取同一数据项,却得到了不同的结果。这是因为在两次读取之间,另一个事务修改了该数据项并已经完成提交。例如事务 T1 在 10 点读取余额为 1000,事务 T2 在 10 点 01 分将余额修改为 500 并提交。T1 在 10 点 02 分再次读取余额时发现变成了 500。对于 T1 而言,同一个事务内对同一数据的两次读取产生了不同的结果。
幻读 (Phantom Read) 与不可重复读类似,但涉及的是记录数量的变化而非单条记录值的变化。一个事务按照某个查询条件查询一组记录,得到了 N 条结果。随后另一个事务插入或删除了若干满足该查询条件的记录并提交。当第一个事务再次执行相同的查询时,结果集的行数发生了变化,出现了之前不存在的新行(如同幻觉一般),或者之前存在的行消失了。
这些异常之所以必须区分,是因为它们对应的风险并不相同。丢失修改通常直接破坏更新结果,脏读会让事务基于从未真正成立的数据继续计算,不可重复读破坏同一事务内部观察的一致性,而幻读则影响范围判断和集合统计。数据库并发控制的目标,并不是把所有事务都强行串行化,而是在可接受的性能成本下,阻止这些危险冲突发生。换句话说,并发控制真正追求的是“像串行一样正确”,而不是“真的完全串行执行”。
