Transaction Serializable隔离之串行执行

我们在前面几篇Transaction弱隔离相关的文章中介绍了读提交,snapshot隔离,write skew和Phantoms。但这些总给我们一种感觉就是他们只能解决部分问题,而且使用起来很不方便,需要注意很多方面。那么有没有什么好的办法来一次性解决所有问题呢?答案是有的,那就是我们今天要介绍的史上最强,超级无敌的Serializable隔离。

Serializable隔离一般会认为是最强的隔离级别。它保证了哪怕有多个transaction并行执行,它的结果和串行执行的结果是一样的。而这个是由数据库本身来保证的,也就是说数据库处理了所有的冲突情况。

既然Serializable隔离这么强,那么大家都用它就好了啊,我们为什么还需要考虑别的呢?显然世界上不会有免费的午餐,想要得到强大的力量,必然就要付出相应的代价。要想了解这个代价是什么,我们首先要来看看一般来说都是怎么实现Serializable隔离的,大概的方法有下面这些:

  1. 真正的串行执行所有的transaction。
  2. 两段锁,很长一段时间这个都是唯一的可行的方法。
  3. 优化的同步控制技术比如Serializable的snapshot隔离。

本文主要讨论如何在单节点上使用这些技术,多节点的实现我们以后再讨论。

真正的串行执行

最简单的想法就是把所有的transaction操作串行化,那么就没有我们遇到的这些并行的问题了。单毕竟串行执行可以有更好的性能,所以直到最近因为硬件提升了很多,对读写比较短的OLTP,大家才认为单thread执行是一个不错的选择。

这种方法现在被用于VoltDB/H-Store, Redis以及Datomic上。有时候单thread的执行效率比多thread还要好,因为它不要消耗精力来处理多thread的冲突。当然它也有其局限,就是性能不能超过单核的极限。对于单thread来说,transaction的组成和传统的方式不太一样。

使用Store Procedure来封装Transaction

我们之前有一种想法,就是假如用户能一次性把一件事情都做完,那么就会方便很多,比如说我们在之前的文章中提到的医生值班系统,当时的设计是医生先去查询是否有两个以上的人在值班,如果有就允许请假。如下图所示,假如我们把请假这件事变成一个store procedure,查询更新都在这个store procedure中。这样做的好处就是在查询到结果后,我们不需要返回(网络消耗)也不需要等待用户来确认(认为的操作延迟,比如去泡杯茶啥的),所有的查询结果都被保存在内存中,性能显然会提高很多。

Store Procedure已经被使用了很长一段时间了,必然也暴露出了很多缺点:

  • 每一个数据库都有它自己的Store Procedure的语言,和我们通常使用的通用语言不太相同,所以使用起来总是怪怪的。
  • 它是在数据库上运行的代码,所以调试,分析,版本控制都比较复杂。
  • 数据库对性能比较敏感,所以写得不好的store procedure其实对总体的性能影响会比较大。

当然有问题就有解决的方案,现在的数据库语言已经很友好了,比如VoltDB使用Java或者Groovy,Datomic使用Java或者Clojure,Redis使用Lua等等。

VoltDB还使用Store Procedure来进行replication:不是从一个节点拷贝数据到另外一个节点,他是在每一个节点都执行相同的Store Procedure。这其实就有一个额外的要求,就是Store Procedure的结果必须是固定的。比如说假如你要使用当前的时间等不确定的值的时候,需要调用特定的API。

使用Partitioning

我们上面提到的顺序执行的方案最大的问题就是只能使用一个CPU来执行,对于一些系统来说(比如写很重的系统),这就有可能成为一个瓶颈。那么有没有可能把多核使用起来呢?答案是肯定的,一个解决方案就是partition你的数据。假如你可以把数据做好partition,然后每一个transaction只访问一个partition,这样每一个partition就有它自己的transaction thread,从而可以让每一个CPU核产处理对应的partition,这样就可以把throughput提高了。

这样做了之后的问题就在于假如你有一个transaction需要访问多个partition,你就需要在所有的partition之间进行同步了。这样一来,其实跨partition的transaction其实消耗就会变大很多,所以速度也会变慢。

另外transaction是否可以做到单partition取决于数据的结果,比如说简单的key-value数据就很容易partition,而有很多二级index的数据则更多地会需要很多跨partition的同步。

串行执行的限制

正如我们上面介绍的这样,串行执行其实已经是serializable隔离的一个实现方案了,这个方案的实现有以下限制:

  • 每一个transaction必须很小很快,因为一个transaction会阻碍另外的transaction
  • 相关的数据集需要能够存在memory中,所以假如需要访问磁盘上的数据速度就会很慢。
  • 写速度需要足够低,这样单CPU可以达到,否则就需要把数据做好parition。
  • 跨partition的transaction是有可能的,但是使用会有限制。

总结

本文总结了Serializable隔离的串行执行,具体介绍了它的实现方案以及其优点和限制。我们会在后文继续介绍Serializable隔离的其他方法。

You may also like...

2 Responses

  1. June 15, 2021

    […] 我们在前面的《Transaction Serializable隔离之串行执行》中介绍了Serializable隔离的第一个实现方法:串行执行。本文来介绍第二个实现方法:两阶段锁(Two-Phase Locking)。这个方法也是一个由古到今一直流行的方法,需要注意的是他和我们通常说的两阶段提交(Two-Phase Commit)是完全不同的两个概念,我们会在后面的文章中再详细介绍两阶段提交的概念。 […]

  2. June 21, 2021

    […] 我们在《Transaction Serializable隔离之两阶段锁》中提到的两阶段锁其实就是一个悲观的同步控制策略:任何有可能出现冲突的数据,我们都加锁(不管是不是真的会有冲突),就有点类似多线程的编程。而《Transaction Serializable隔离之串行执行》中提到的串行执行,更是把悲观做到了极致,直接变成串行的单线程编程了。 […]

Leave a Reply

Your email address will not be published. Required fields are marked *