事务处理几乎是每一个信息系统中都会涉及到的问题,它存在的意义就是保证系统中的数据是正确的,不同数据间不会产生矛盾,数据的修改后的结果使我们期望的,也就是保证数据状态的一致性(Consistency)。
想要达成数据库状态的一致性,需要三个方面的努力:
这就是 ACID 的概念。 A I D 是手段, C 是目的。
本地事务是指仅操作特定单一事务资源的、不需要“全局事务管理器”进行协调的事务。
本地事务是最基础的一种事务处理方案,通常只适用于单个服务使用单个数据源的场景,它是直接依赖于数据源(通常是数据库系统)本身的事务能力来工作的,事务的开启、终止、提交、回滚、嵌套、设置隔离级别、乃至与应用代码贴近的传播方式,全部都要依赖底层数据库的支持
在程序代码层面,我们最多只能对事务接口做一层标准化的包装(如 JDBC 接口),并不能深入参与到事务的运作过程当中。
实现原子性和持久性所面临的困难是,“写入磁盘”这个操作不会是原子的,不仅有“写入”与“未写入”,还客观地存在着“正在写”的中间状态。
Fenix’s Bookstore 是一个在线书店。一份商品成功售出,需要确保以下三件事情被正确地处理:
用户的账号扣减相应的商品款项;
商品仓库中扣减库存,将商品标识为待配送状态;
商家的账号增加相应的商品款项。
由于写入存在中间状态,可能发生以下情形:
这种数据恢复操作被称为 崩溃恢复。
为了能够顺利地完成崩溃恢复,在磁盘中写数据就不能像程序修改内存中变量值那样,直接改变某表某行某列的某个值,必须将修改数据这个操作所需的全部信息(比如修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值等等),以日志的形式(日志特指仅进行顺序追加的文件写入方式,这是最高效的写入方式)先记录到磁盘中。
只有在日志记录全部都安全落盘,见到代表事务成功提交的 Commit Record 后,数据库才会根据日志上的信息对真正的数据进行修改,修改完成后,在日志中加入一条 End Record 表示事务已完成持久化,这种事务实现方法被称为 Commit Logging
日志一旦成功写入 Commit Record ,那整个事务就成功了。及时修改数据时崩溃了,重启后根据这部分日志恢复现场、继续修改即可,这保证了持久性。
如果日志没有成功就发生崩溃,系统重启后会看到一部分没有 Commit Record 的日志,将这一部分日志回滚即可整个事务就像完全没有发生过一样,这保证了原子性。
**Commit Logging 存在一个巨大的缺陷:**所有对数据的真实修改都必须发生在事务提交、日志写入了 Commit Record 之后,即使事务提交前磁盘 I/O 有足够空闲、即使某个事务修改的数据量非常庞大,占用大量的内存缓冲,无论何种理由,都决不允许在事务提交之前就开始修改磁盘上的数据,这一点对提升数据库的性能是很不利的。
为了解决这个缺陷,前面提到的 ARIES 理论终于可以登场了。ARIES 提出了 “ Write-Ahead Logging ” 的日志改进方案,其名字里所谓的“提前写入”(Write-Ahead),就是允许在事务提交之前,提前写入变动数据的意思。
Write-Ahead Logging 先将何时写入变动数据,按照事务提交时点为界,分为了 FORCE 和 STEAL 两类:
FORCE;如果不强制变动数据必须同时完成写入则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略,只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。STEAL,不允许则称为 NO-STEAL。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。 Commit Logging 允许 NO-FORCE,但不允许 STEAL。因为假如事务提交前就有部分变动数据写入磁盘,那一旦事务要回滚,或者发生了崩溃,这些提前写入的变动数据就都成了错误。
Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL,它给出的解决办法是增加了另一种称为 Undo Log 的日志。当变动数据写入磁盘前,必须先记录 Undo Log,写明修改哪个位置的数据、从什么值改成什么值,以便在事务回滚或者崩溃恢复时,根据 Undo Log 对提前写入的数据变动进行擦除。
Undo Log 现在一般被翻译为**“回滚日志”**,此前记录的用于崩溃恢复时重演数据变动的日志,就相应被命名为 Redo Log,一般翻译为“重做日志”。
由于 Undo Log 的加入,Write-Ahead Logging 在崩溃恢复时,会以此经历以下三个阶段:
End Record 的事务,组成待恢复的事务集合 重做阶段和回滚阶段的操作都应该设计为幂等的