Snapshot的隔离和Repeatable的读

我们在上文的《Transaction弱隔离之读提交的介绍和实现》中详细介绍了读提交,总得来说它是一个很好的保证,但是有了它是否就足够了?答案是否定的,即使有了读保证,我们仍然会有问题,而这就是本文准备详细介绍的内容 。

我们首先来看下面这个图:

我们假设Alice有1000块存在银行里,分别在两个account里面,开始的时候一个里面放了500块。然后Alice开始查询两个账户一共多少钱,这个查询是一个transaction,它开始查到账户1里面有500,然后这个时候发生了一个交易,就是把账户2的钱转了100到了账户1. 在整个第二个transaction(转账的transaction,包含两个操作,一个是把账户1加100,一个是把账户2减少100)完成后,之前查询的transaction的第二个查账户2的操作才到了,所以它看到的账户2的数值是400,这就导致了Alice看到的账户总额变成了900而不是1000。这个过程中,并没有脏读,因为第二个400的读是在transaction完成之后才读的,所以完全符合之前读提交,但仍然还是有了问题。

这个问题我们称之为read skew,同时也是一个不repeatable读的例子:就是在整个transaction结束的之前再去读transaction开始读的数据会发现有不同的值。就比如例子中账户1的值,我们在transaction开始读的时候它的值是500,假如我们在transaction即将结束的时候去读的话它的值就变成了600,这就是不repeatable的读。

当然在上面的例子中其实问题不是很严重,因为假如Alice再去读一下,就会发现它的余额又变成了1000,并没有发生真的丢失的问题。但是在某些情况下这种错误会导致很严重的问题。

备份

我们通常需要备份整个数据库,而这个备份的过程中原来的数据也是在同时执行的。假如什么的情况发生的时候,正好原来的数据库出问题了,我们需要切换到备份数据库,那么这个错误就是永久的了,Alice的100元就再也找不回来了。

一些分析查询和集成检测

通常我们需要运行一些分析的查询,用于分析,比如说银行会用一个查询来看看少掉的余额和取出的钱是否相等,这个时候这些查询就会得到一些错误的信息,从而影响分析。

解决这个问题的常见方法是使用过Snapshot隔离,所谓snapshot隔离就是一个transaction看到的数据是一个这个transaction开始的时候的snapshot,如果在这个transaction执行过程中数据发生了改变,对整个transaction来说是不可感知的。这样就可以解决我们上面提到的问题。

目前很多数据库都支持snapshot的隔离,比如PostgreSQL,MySQL,Oracle,SQL Server等等。

Snapshot隔离的实现

和上面的脏读的思想类似,要实现snapshot隔离就需要数据库能同时保持不同的版本数据,然后一个transaction的过程中它只会读到它开始的时候的版本的数据,对后面修改的版本数据不去感知。这种技术我们也称之为Multi-Version Concurrency Control(MVCC)。

下图就展示了这一实现的方法:

在这里,每一个账户其实有多个版本的数值,这里对每行数据有两个域,一个是create by,用来表示谁插入了这行数据。另外一个是delete by,用来表示谁delete了数据。这也意味着即使一个数据被delete了,它也不是真的delete了,而是更新了delete by这个域。只是一段时间之后,一个类似垃圾回收的机制来处理这些真的被delete的数据。这样一来,在transaction最后去读账户2的钱的时候,其实它是可以看到不同版本的,它需要选择的就是在整个transaction开始对的时候的版本(就是这里已经设置了deleted by的版本)。这样就可以做到snapshot的隔离了。

一致的snapshot的规则

当一个transaction到来时,怎么决定哪个snapshot可以被看到呢?一般会使用Transaction ID按照下面的方法来决定:

  • 当一个transaction开始的时候,数据库其实维护了一个所有正在进行的transaction的列表。那么就可以知道哪些写入是由这些没有完成的transaction实现的,所有的这些没有完成的transaction的写都会被忽略。
  • 所有终止的transaction的写也会被忽略。
  • 所有由这个transaction后面的transaction写入的值都被忽略,也就是说哪怕在这个transaction后面有一个已经提交的transaction的写,对当前的transaction来说也是忽略的。
  • 其他的写就都是可以看到的了。

这个规则就基本保证了snapshot的隔离,就像上面例子中,我们在读账户2的时候看到的还是500,因为这个delete是后一个transaction做的,所以不需要管这个修改。

Snapshot隔离的index处理

可能你会好奇维护多个版本的数据库中是如何实现index的呢?一个比较常见的方法是把index指向多个版本的数据,然后再通过一个query来把可以简单的版本filter出来。当我们回收版本的时候,只需要把这个query的查询修改一下就可以了。

现实中,大家都在这个多版本的index上下足了功夫,比较它其实还是很可能会影响最终的性能的。比如说有人会引入一个称之为Append-only的B-Tree。就是每次更新的时候并不是更新tree上的page,而是创建一个新的拷贝。当然这种方法需要一个后台的process来进行压缩和垃圾回收。

Repeatable读

很多数据库也实现了类似的snapshot隔离,但是它们取了一个别的名字,称之为repeatable读或者serializable等。这个原因是因为这一块并没有一个规定的标准名字(标准制定早于snapshot隔离的发明),所以最后造成的结果就是PostgreSQL和MySQL称之为repeatable读。这一块大家了解一下就好了。

总结

本文就总结了transaction弱隔离中的第二个部分,那就是snapshot隔离或者repeatable读的原理和实现方法。

You may also like...

3 Responses

  1. July 7, 2021

    […] 大多数MySQL使用的不是简单的行锁。他们使用的是行锁和一种称之为多版本同步控制(MVCC)的技术。这个技术我们在之前的《Snapshot的隔离和Repeatable的读》中有详细的介绍。 […]

  2. November 28, 2021

    […] Snapshot的隔离和Repeatable的读 […]

  3. January 21, 2022

    […] 我们在前文《Transaction弱隔离之读提交的介绍和实现》和《Snapshot的隔离和Repeatable的读》中介绍了read commit和snapshot隔离。这两篇文章其实更多地讨论的是当有写发生的时候如何进行读,除了脏写之外没有更多地讨论如何处理两个写同时发生的情况。其实当有两个写同时发生的时候还会有很多问题需要处理,其中最常见的就是两个写有冲突,这个时候就会发生更新丢失的问题。 […]

Leave a Reply

Your email address will not be published.