本地事务的理论依据

放了方便描述,本问题讨论的是都是page-oriented系统。道理都是一样的,其他类型的系统也适用。在事务里,有两个最重要的特性:

  1. 原子性:原子性保证了事务的多个操作要么都生效要么都不生效,不会存在中间状态
  2. 持久性:持久性保证了一旦事务生效,就不会再因为任何原因而导致其修改的内容被撤销或丢失

众所周知,数据必须要成功写入硬盘等持久化存储器后才能拥有持久性。实现原子性和持久性的最大困难是“写入硬盘”这个操作并不是原子的,不仅有“写入”与“未写入”状态,还客观地存在着“正在写”的中间状态。所以如果不做额外保障措施,只是简单把内存中的数据写入磁盘,并不能保证原子性与持久性。这个中间态,大致可以归纳成3种情形:

  1. 未提交事务,部分持久化,程序崩溃:例如修改5个数据,已经修改了3个,其中2个已经持久化。程序崩溃后重启,因为事务未提交,需要把持久化后的数据恢复回去。
  2. 已提交事务,部分持久化,程序崩溃:例如修改5个数据,已经修改完,返回调用方成功,数据未持久化。程序就崩溃后重启,需要把丢失的变更重做回来。
  3. 已提交事务,完全没持久化,程序崩溃:同2。

我们可以看到,这些问题可以归纳成2个问题:

  1. 把脏数据恢复成之前的数据
  2. 把丢失的数据恢复回来

为了解决这两个问题,有3个流派,我们每个流派都说一下

  1. Shadow Paging
  2. Commit Logging
  3. Write-Ahead Logging

Shadow Paging

基本思想是这样的,对于问题1,如果我不in-place update,就没有脏数据了,也就没有把脏数据恢复之说; 对于问题2,如果事务提交成功的前提,数据已经持久化,那么就不用丢失数据了。因此Shadow Paging在修改数据的时候,会先copy一份副本,基于这个副本做修改,如果需要修改多个数据,那么就分别copy多个副本做修改,修改完后把数据持久化下来。事务提交时,就是把之前引用老数据的指针,指向新数据,然后持久化,持久化完后,事务就提交成功。如果被修改的指针分布在几个页面上,那么对每个一个页面,也是执行shadow paging的策略去更新,是一个递归的过程。这里大家可以看到有几个问题:

  1. copy page本身的开销,性能问题
  2. 修改引用page也可能走shadow paging,性能问题
  3. 提交事务需要数据频繁持久化,性能问题(是否是随机IO,取决于持久化层存储引擎)
  4. 一些隔离级别做起来成本高,例如支持read commit,那么就有mvcc,保存的是多个完整的page

可以看到shadow paging策略最大的问题,就是性能差。如果要优化的化,对于2、3,一般会结合接下来讨论的两个流派做优化。

Commit Logging

基本思想是这样的,对于问题1,如果事务不提交,我就不持久化,那就没脏数据了,也就没有把脏数据恢复之说;对于问题2,事务提交成功的前提,是我把所有修改记录,都append到日志中,提交时把事务提交的标记也append到log中,然后做日志持久化,这样就完成的事务提交。这时恢复数据就很容易的,而且性能也高,因为对于持久化存储append的性能很高。因此使用Commit Logging策略的系统,每次修改数据前,都先append log,log并不要求刷盘,事务里的所有数据都修改完了,append 一个commit标记到log中,然后对log进行刷盘,即事务完成提交了。对于事务提交后的部分持久化问题,可以通过在page处记录持久化时对page做修改的日志序号,避免重复做即可,或者把操作设计成保证幂等性。可以看到Commit Logging相比Shadow paging有不少优点:

  1. 不需要copy page,性能损耗小
  2. 提交事务,持久化的只是log,并且是append only,性能高
  3. mvcc好做,对于page内的每个记录一个版本号即可,不需要额外保存完整的page

但是Commit Logging也有一个明显的缺点,就是只有事务提交后,数据才能做持久化。这样在高并发常见下,可能不用充分利用硬盘的IO,而且对于大事务,所有数据都要在内存中hold住。为了改善这两点,就提出了Write-Ahead Logging。

Write-Ahead Logging

基本思想是这样的,对于问题1,如果事务不提交,持久化page的前提,是我记录下修改前的数据,那么脏数据就能恢复了,这个log称为undo log;对于问题2,解决方法和Commit Logging一样,这个log称为redo log。因此使用WAL的系统,修改数据的流程是这样的,对于每个修改诗句,修改前先append一条undo log,再append一条redo log,然后修改数据,事务未提交前,有数据要持久化时需要保证对应的undo log已经持久化。所有数据修改完后,append 一个commit标记到log中,然后对log进行刷盘,即事务完成提交了。系统崩溃服务重启,对数据进行恢复时,先不管三七二十一,把redo log里所有明确有提交的事务重放一边,然后在redo log里所有没提交的事务,去undo log那找log,把脏页的数据回滚回去。可以看到和Commit Logging相比,优点是:

  1. 充分利用硬盘IO,释放内存空间
  2. 大事务不需要把所有数据都hold在内存

理论化

我们将何时持久化变动数据,按照事务提交时点为界,划分为 FORCE 和 STEAL 两类情况:

  • FORCE:当事务提交时,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变动数据必须同时完成写入则称为 NO-FORCE。
  • STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL,不允许则称为 NO-STEAL。


    image.png

Shadow Paging FORCE + NO-STEAL。

Commit Logging NO-FORCE+ NO-STEAL。

Write-Ahead Logging NO-FORCE + STEAL。

现实中绝大多数数据库采用的都是 NO-FORCE 策略,因为只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。

你可能感兴趣的:(本地事务的理论依据)