7

更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(番外)

 3 years ago
source link: https://qcngt.com/2020/08/24/snapshot-4.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(番外)

作者:青菜年糕汤

更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(,番外)

正文到第三篇就结束了,在它结尾时我提到:

只要能够通过其它途径解决更新丢失、写偏、幻读的问题,或者证明它们不会对业务造成影响,我们就可以用快照隔离这个更松更高效的隔离级别,来达到可序列化级别的正确性。 我想讲一个最近工作中遇到的实例,为这句话做注解。

这是开源项目FoundationDB Record Layer的一部分,所以无妨公开讨论。

与大多数传统数据库一样,这个数据库系统有这样到的机制:当二级索引(secondary index,下文都用“索引”指代“二级索引”)建成、开始正常运行之后,每次增、删、改数据,都会同时增、删、改数据对应的索引。

但我们在这个故事里,关注的是最初建成这个索引的过程。

如果在还没有数据的时候就定义了索引,那数据库天生就有正确的空白的索引。但如果当数据库已经有不少数据时,再定义新的索引,就需要经历构建索引这个过程,为现有的数据创建索引。

这个过程,就是会有个后台进程,扫描所有有关的数据,把数据按照新索引的定义写入索引。

这个过程中,要保证在读数据到写索引之间数据没有发生改变,最好用可序列化(serializable)隔离级别。

但如果数据很多,这个过程不可避免地要花很长时间。在这段时间中,不可避免地有新的增、删、改发生。数据发生改变,这个后台的索引事务就会不可避免地发生冲突,从而失败。

解决方法是把数据拆分成小段,每个事务各自负责一段。因为每段中的数据足够少,发生改变的概率就小,即使偶尔失败了也能很快重试成功。

上面讲的都是故事背景,下面开始讲最近出现的问题:

在一个流量很大的数据库中,无时不刻都有好多新数据被插入进来,而且这些数据都插入到差不多的位置。

为什么会都插入到差不多的位置呢?可以想像,假如说数据的主键(primary key)是一个自增的数列,或者是创建时的时间戳,那么新数据永远都在最后那段。

当要为那一段数据构建索引时,即使原来一条数据都没有,也会在构建的过程中不断有新数据插入。那构建索引的事务就会一直与新插入的数据起冲突,然后不得无休止地失败、重试。

一种解决方案是把这个范围再变得更小,只要小到极点,最终总是能解决的。但这要耗费大量的事务,实现起来也挺麻烦。

而另一种方案就是,尝试使用快照隔离(snapshot isolation),避免冲突。

让我们来分析一下,快照隔离可行吗?

首先,不会有更新丢失(lost update)的问题。因为每个事务都读的是原数据、写的是索引,对任何变量都不会有“读-修改-写”这样的操作。

但写偏(write skew)的问题是有的。

在事务读一些数据,到写入这些数据的索引的过程中,数据库仍在活跃运行,是可能会有用户增、删、改这个范围内的数据的。

我们不妨把插入与删改两种情况分开讨论。

如果有用户在我们读入数据之后、写入索引之前插入了新数据,构建索引的事务不会看到新数据,便不会把它写入索引。但没关系,用户插入新数据的那个事务自己会同时插入索引。因此这种情况不会对业务产生影响。

但如果有用户在我们读入数据之后、写入索引之前删除或修改了数据,构建索引的事务没看到它被删改,还是把原先的版本写入了索引。要记得,用户删改数据的那个事务本身已经正确地删改了索引,但现在又用原先的版本覆盖了它。这种情况是会产生错误的。

为了避免这个问题,我们只要把构建索引的过程中读到的所有数据都加入冲突检查(或上锁),就可以解决正确性的问题。

从性能上说,考虑到我们要解决的主要问题是频繁的插入,而删改并不那么频繁,因此把读到的数据加入冲突检查并不会真的造成太大的冲突。

综上,我们可以稍加处理,避免写偏的问题。

既然写偏不成问题,就更不用担心因幻读(phantom)而造成的写偏。

问题解决了。

其实从另一个角度想,这个做法是把我们前面讨论的幻读反其道而行之:

在这个例子中,正是因为我们不用在意幻读造成的问题,所以可以避免(使用可序列化)把整个读的范围都加入冲突检查,而只要(在快照隔离的基础上)把实际读取的一条条记录加入冲突检查,就可以保证正确性。

对尽可能少的数据进行冲突检查,就能尽可能减少冲突,带来更好的并发度。

你在工作中有没有也碰到过有关事务隔离性的挑战呢?欢迎留言交流。


这里我们全程都有个假设:同一个数据被重复多次创建索引,结果还是一样的,不妨碍正确性。

这种性质被称为幂等(idempotent)。对于普通的二级索引,这个是性质是能满足的,所以我们能放心地一边让数据库接受新的增、删、改,一边分段构建索引。

但事实上,在Foundation DB Record Layer中的“索引”会比这更广义,以至于一些索引未必是幂等的。

比如说,你可以定义一个索引用于统计某个字段不同取值出现的次数。每插入一条记录,就会对相应取值的计数器加一。对于这样的索引,重复创建索引就不可接受了。

对于不幂等的索引应该如何构建,并不属于这个系列的主题,就留给大家探索吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK