事务简介
本课程讲解了在数据库和分布式系统背景下事务的概念。
事务
让我们以银行转账这一经典例子来说明。假设特朗普正在通过网上银行向拜登的账户转账100美元。为了正确反映这笔交易,银行软件必须从特朗普的账户中扣除100美元,并向拜登的账户增加100美元。
这涉及对数据库的两次写操作,我们希望这两个操作要么全部成功,要么全部失败,以确保账户记录的一致性。如果其中一个操作失败而另一个成功,那么参与者的账户余额将出现错误。
事务正是为解决这一需求而设计的。事务由一组读写操作组成,这些操作构成一个逻辑单元,并作为一个整体执行。事务中的所有操作要么全部成功,要么在至少有一个操作失败时全部被回滚或中止。当事务中的某个操作失败时,其余操作将被回滚。事务提供了一组称为ACID的安全保障。事务简化了错误处理:应用程序开发者无需担心部分失败的情况,可以安全地重试整个事务。
ACID
ACID 是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)四个特性的缩写。该术语最早由 Andreas Reuter 和 Theo Härder 提出,用于描述数据库故障容错机制的属性或安全保证。从高层次上看,所有符合 ACID 标准的数据库都遵循这些特性的基本含义和目标,但具体实现方式差异很大,不同厂商提供的数据库其 ACID 保证可能并不相同。接下来我们将逐一分析 ACID 的各项特性。
原子性
在数据库领域中,“原子性”不应与并发/多线程编程中的“原子性”概念混淆。在并发编程中,原子性指的是程序状态只能体现一个已完成原子操作的效果,或者在操作开始前的状态,而不会出现正在进行中的部分执行结果。例如,在 Java 中,对整型变量的赋值是原子的。如果一个线程正在读取一个整型变量,而另一个线程同时在写入该变量,则读取线程不可能读到仅更新了2个字节(共4个字节)的部分结果。读取线程要么读到新值,要么读到旧值。相反,在 Java 中,对 long 和 double 类型的赋值不是原子的,读取线程可能看到部分更新的结果。Java 提供了机制来避免读取到部分更新的数据。
而在数据库中,原子性意味着在一个原子事务中包含的所有操作必须作为一个整体执行;如果其中任意一个操作失败,则之前所有已成功的操作都将被回滚或中止。如前所述,这使得数据库编程对开发者更加友好。应用程序可以放心重试事务,而无需担心重复写入或因先前部分失败导致的副作用。
一致性
以我们前面讨论的银行转账为例,对于银行应用而言,所有账户的借方和贷方必须保持平衡。这种条件可被视为一种不变量(永远不会改变的事实),必须始终满足。另一个不变量可能是:用户的账户余额永远不能为负数。一致性意味着:如果事务开始前数据库的不变量已经满足,并且事务中的写操作能够保持不变量的有效性,那么在事务成功完成后,数据库的不变量仍然会被维持并成立。
一致性更多依赖于应用程序的设计,而非数据库本身,因为应用程序可能写入无效数据,从而破坏不变量。虽然数据库的原子性和隔离性可以帮助应用程序实现一致性,但仅靠数据库本身无法独立保证一致性。
隔离性
通常情况下,多个客户端会同时与数据库交互。类似于多线程/并发编程中的防范措施,我们不希望一个客户端的操作与其他客户端的操作交错执行,从而导致数据库进入非预期状态,尤其是在多个并发事务操作同一数据记录时。隔离性保证了任何运行在数据库上的事务,都如同它是唯一在运行的事务一样。即使多个事务并发执行,它们对数据库的影响也应表现为按顺序依次执行的效果。这也被称为可串行化——即事务看起来像是一个接一个地运行并提交,尽管它们实际上可能并发执行。另一种理解隔离性的角度是:如果一个事务进行了多次写入,那么另一个事务看到的应该是全部写入结果,或者完全未看到任何写入,而不会只看到其中一部分。
可串行化的隔离级别虽然能提供最强的一致性保障,但代价高昂,会对性能造成显著影响。在实际应用中,可串行化隔离很少使用,大多数数据库提供较弱的隔离保证。
持久性
持久性是指数据库承诺:一旦事务提交,其数据变更就会永久保存,即使发生硬件或软件故障也不会丢失。在单节点数据库中,实现持久性通常意味着将数据写入持久存储设备(如硬盘)。此外,还可以定期备份数据库。数据变更通常先写入预写日志(Write-Ahead Log),这是一种仅追加的日志文件,用于在系统崩溃后恢复数据库。分布式数据库则通过复制变更来实现持久性。然而,这些措施并不能保证绝对的持久性。所有存储介质可能在同一时刻失效,或所有副本同时损坏,从而违背持久性承诺。尽管如此,采取这些措施仍能显著降低数据丢失的风险。