深入理解事务

介绍事务

事务将应用程序的多个读、写操作捆绑在一起成为一个逻辑执行单元。即事务中的所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止 或者 回滚)。如果失败,应用程序可以安全地重试。

这样,由于不需要担心部分失败的情况(无论出于何种原因),应用层的错误处理就变得简单很多。因此事务被创造出来的目的是:简化应用层的编程模型。有了事务,应用程序可以不用考虑某些数据库内部潜在的错误以及复杂的并发性问题,这些都可以交给数据库来负责处理(我们称之为安全性保证)

即使没有事务支持,或许上层应用依然可以工作,然而在没有原子性保证时,错误处理就会异常复杂,而缺乏隔离性则容易出现并发性方面的各种奇怪问题。

事务提供的安全性保证

事务提供的安全性保证即 ACID ,分别代表原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和 持久性(Durability)。

  • ACID 语义中的原子性所定义的特征是:在出错时中止事务,并将部分完成的写入全部丢弃。
  • ACID 语义中的一致性主要是指:数据库处于应用程序所期待的“预期状态”。
  • ACID 语义中的隔离性意味着并发执行的多个事务相互隔离,它们不能相互干扰。
  • ACID 语义中的持久性保证一且事务提交成功,即使存在硬件故障或数据库崩溃,事务所写入的任何数据也不会消失。

ACID 最早由 TheoHarder 和 Andreas Reuter 于 1983 年为精确描述数据库的容错机制而定义。实际上,各家数据库所实现的 ACID 并不相同。例如,围绕着 “隔离性” 就存在很多含糊不清的争议。

原子性

通常,原子是指不可分解为更小粒度的东西。这个术语在计算机的不同领域里有着相似但却微妙的差异。例如,多线程编程中,如果某线程执行一个原子操作,这意味着其他线程是无法看到该操作的中间结果。它只能处于操作之前或操作之后的状态,而不能是两者之间的状态。

而 ACID 语义中的原子性并不关乎多个操作的并发性,它并没有描述多个线程试图访问相同的数据会发生什么情况,后者其实是由 ACID 的隔离性所定义。ACID 原子性其实描述了客户端发起一个包含多个写操作的请求时可能发生的情况,例如在完成了一部分写入之后,系统发生了故障,包括进程崩溃,网络中断,磁盘变满或者违反了某种完整性约束等;把多个写操作纳入到一个原子事务,万一出现了上述故障而导致没法完成最终提交时,则事务会中止,井且数据库须丢弃或撤销那些局部完成的更改。

假如没有原子性保证,当多个更新操作中间发生了错误,就需要知道哪些更改已经生效,哪些更改没有生效,这个寻找过程会非常麻烦。或许应用程序可以重试,但情况类似,并且可能导致重复更新或者不正确的结果。而原子性则大大简化了这个问题:如果事务已经中止,应用程序可以确定实质上没有发生任何更改,所以可以安全地重试。

ACID 语义中的原子性所定义的特征是:在出错时中止事务,并将部分完成的写入全部丢弃。换言之,不用担心数据库的部分失败,它总是保证要么全部成功,要么全部失败。

也许可中止性比原子性更为准确,不过我们还是沿用原子性这个惯用术语。

一致性

ACID 语义中的一致性主要是指:数据库处于应用程序所期待的“预期状态”。

对数据有特定的预期状态,任何数据更改必须满足这些状态约束(或者恒等条件)。如果某事务从一个有效的状态开始,并且事务中任何更新操作都没有违背约束,那么最后的结果依然符合有效状态。

这种一致性本质上要求应用层来维护状态一致(或者恒等),应用程序有责任正确地定义事务来保持一致性。这不是数据库可以保证的事情:即如果提供的数据修改违背了恒等条件,数据库很难检测进而阻止该操作(数据库可以完成针对某些特定类型的恒等约束检查,例如使用外键约束或唯一性约束。但通常主要靠应用程序来定义数据的有效/无效状态,数据库主要负责存储)。

原子性,隔离性 和 持久性是数据库自身的属性,而 ACID 中的一致性更多是应用层的属性。应用程序可能借助数据库提供的原子性和隔离性,以达到一致性,但一致性本身并不源于数据库。因此,字母 C 其实并不应该属于 ACID。

隔离性

大多数数据库都支持多个客户端同时访问。如果读取和写入的是不同数据,这肯定没有什么问题;但如果访问相同的记录,则可能会遇到并发问题(即带来竞争条件)。

一个简单的例子如图所示。假设有两个客户端同时增加数据库中的一个计数器。每个客户端首先读取当前值,在客户端增加 1,然后写回新值(这里假设数据库尚不支持自增操作)。由于有两次相加,计数器应该由 42 增加到 44,但实际上由于竞争条件最终结果却是 43。

image-20230202203649456.png


ACID 语义中的隔离性意味着并发执行的多个事务相互隔离,它们不能相互干扰。例如,如果某个事务进行多次写入,则另一个事务应该观察到的是其全部完成(或者一个都没完成)的结果,而不应该看到中间的部分结果。

经典的数据库教材把隔离定义为可串行化,这意味着可以假装一个事务是数据库上运行的唯一事务。虽然实际上它们可能同时运行,但数据库系统要确保当事务提交时,其结果与串行执行(一个接一个执行)完全相同。然而实践中,由于性能问题很少使用串行化隔离,更多的是使用弱隔离级别,在高性能与正确性之间做一个权衡。使用者可以根据自己的业务场景,选择一个合适的隔离级别。

一些流行的数据库,如 Oracle 甚至根本就没有实现串行化隔离。虽然 Oracle 也有声称 “串行化” 的功能,但它本质上实现的是快照隔离,快照隔离提供了比串行化更弱的保证。

持久性

数据库系统本质上是提供一个安全可靠的地方来存储数据而不用担心数据丢失。持久性就是这样的承诺,它保证一且事务提交成功,即使存在硬件故障或数据库崩溃,事务所写入的任何数据也不会消失。

对于单节点数据库 ,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或 SSD。在写入执行过程中,通常还涉及预写日志等,这样万一磁盘数据损坏可以进行恢复。而对于支持远程复制的数据库,持久性则意味着数据已成功复制到多个节点。为了实现持久性的保证,数据库必须等到这些写入或复制完成之后才能报告事务成功提交。

其实不存在完美的持久性。例如,所有的硬盘和所有的备份如果都同时被(人为)销毁了,那么数据库也无能为力。

参考资料

《数据密集型应用系统设计》中文版书

posted @ 2023-03-22 12:41  真正的飞鱼  阅读(330)  评论(0编辑  收藏  举报