0%

不可重复读与幻读的一些思考

在一次面试中,被问到一个问题“不可重复读会带来什么危害,能不能举一些具体的场景来说明?”。虽然对不可重复读这个概念是非常清晰的,但是一时间还真的无法想出具体的案例来说明不可重复读会在什么场景下带来危害。遗憾的是,网上的相关资料比较少,并且其中许多资料都有一些错误,在整理阅读后,我以本篇文章来记录一些自己的思考和理解。

严格意义上来说,读已提交这个隔离级别其实已经可以满足绝大多数的事务。在可重复读级别下,其实还会带来一定的困惑性,例如一个典型的查询-更新事务。数据库中的隔离级别中,可重复读其实并不是特别重要,因为单单解决不可重复读在大部分业务场景下仍然不能够解决所有的危害。这里以一个案例来说明不可重复读带来的危害,该案例及其相关的文献都来自博客文章

银行做活动。事务a查询某地区余额1000以下送一包餐巾纸,生成名单。事务b新增了一个新用户小明,并存款500。事务a查询1000到2000送一桶油,生成名单,这样小明没有收到礼物,而同时注册的小李存了1500却收到了一桶油。

不可重复读的危害

这里带来的后果是在长事务 A 的范围读取中,两次读取到的数据不一致,从而导致小明的账户获取了两次礼品,这很好理解。那么我们将这个案例进行泛化,可以发现这些问题主要是出现在数据库遍历的业务场景下,这些事务必须要保障多次读取到的数据来自同一时刻,即必须看到某一时刻下数据库的视图。这里即使是使用可重复读隔离级别也是不能够保证业务安全的,因为这里还会有可能出现幻读的问题——一个账户在活动截止后才被创建,而此时事务 A 仍未结束,就会出现新账户也被送出礼品的问题。

仔细思考,其实会出现不可重复读的业务场景下,同样也是会出现幻读这一问题的,因为在业务场景中很少是在一次事务中针对某一行数据进行多次查询。那么单单解决不可重复读,其实意义并不是特别大,只有将不可重复读与幻读一起解决才能够保证读数据库快照这一业务场景的正确性。

这一问题其实就是 ANSI SQL 四种隔离级别的缺陷:可重复读隔离级别并不能够真正解决这一业务场景下的问题,而能够解决这一问题的可串行化隔离级别又会导致数据库的性能大幅度降低——这一场景需要大范围查询数据库中的数据,会极大概率与其他事务发生冲突,这就会造成事务的并发量下降。在快照隔离这一事务隔离级别下,其实才能够最好地去解决这一业务场景问题。

在 OLAP 的应用场景中,大部分事务都是需要读取数据库快照的,例如:数据库备份、离线数据分析等。这些事务的运行时间较长,并且要求输入的数据来自同一时刻,否则事务便失去了意义。可以发现,一些通用数据库或面向 OLAP 的数据库通常都提供了快照隔离的事务等级。Oracle 是一个例外,这里猜测是由于 Oracle 的发布年代过早,而快照隔离这个概念在 1997 年才被提出。MySQL 定义的可重复读其实就是一个快照隔离的事务隔离等级。

总而言之,不可重复读带来的危害在多数情况下是与幻读带来的危害高度重叠的,它们两者会出现的业务场景也是类似的,那就是需要对某一时刻的数据进行分析的读快照场景。一些可以规避不可重复读的技术手段,同样可以很低成本地规避幻读,因此这两者通常是作为一个整体被解决的,而最终数据库给出的解决方案就是快照隔离。