这是一篇有趣的文章,因为我将列出数据库引擎中并发性的一些选项,但不会真正深入讨论它们。 特别是,即使有了并发控制,您也会陷入…数据库的奇怪情况(参见事务隔离级别和每个事务试图解决的问题),因为这是野兽的本性。

数据库内部管理公司

当然,理想的情况是,每个客户端都感觉自己是唯一接触数据库的客户端,并且在数据库运行时没有人能够接触到它。 最简单的方法是一次只允许一个客户端进入数据库。 这实际上是经常发生的事情,但实际上,这不是你真正想做的事情。 即使是嵌入式数据库也至少允许多个阅读器,所以这不是我们真正要处理的事情。

在我们进入实际的并发性讨论之前,我们首先需要弄清楚我们正在谈论的关于数据库引擎内部的并发性。 圣杯是不阻挡读者的作者和不阻挡作者的读者,允许阅读和写作不受阻碍地进行。

然而,在我们真正了解实现它的复杂性之前,让我们看看我们有什么样的问题 在.之后 我们已经解决了这个问题。 特别是,问题是当我们有多个客户端同时读取/写入相同的值时。 那我们该怎么办? 如果W1和W2都试图改变同一个记录,哪一个会赢? 我们把访问序列化了吗? 如果我们有一个W1和R2同时修改和读取同一个记录会发生什么? 直到写事务完成,我们给读者新的值,让它等待吗?

数据库中的经典模型曾经是数据库会锁定。 每当读/写一个特定的值时,你可以把它们看作读/写锁。 这些锁的发布时间表会影响事务隔离级别,但这对于本次讨论并不重要。

请注意,每个值的锁非常昂贵,因此数据库引擎要做的事情之一就是升级锁。 如果它注意到在一个特定的页面中有太多的锁,它将升级为一个页面锁(并且向前直到整个树/表被锁定)。

这向用户暴露了一个非常棘手的问题。

如果您的数据库中有一个热点(最近修改过的记录),那么很容易出现这样的情况:所有的客户端都在等待同一个锁,这实际上导致了一列火车。 请注意,在这种情况下,读者和作者都在等待对方,我们的想法是通过分散锁来获得并发性,希望他们不会争得太多。

处理这种情况的另一种方法叫做MVCC(多版本并发控制),通过这种方式,我们不用在更改后立即覆盖记录,而是保留旧值,这样读者就不必等到事务结束时才得到它。 如果作者修改相同的记录,他们仍然需要彼此等待,但是我们只是确保作者和读者不需要彼此等待。 MVCC有点复杂,因为您需要维护多个并发版本,但这在今天是非常常见的选择。

但是读者锁和作者锁的复杂性实际上是嵌入在拥有一个 会话 数据库。 在同一个连接中向数据库发布多个语句的能力,以及背后潜在的人类反应,使得系统更加复杂。 您必须在连接期间持有所有锁。 在大多数较新的数据库中,没有这样的概念——写事务在单个命令(或一批命令)的持续时间内保持不变,它是独立处理的,然后立即提交。

在这种情况下,跳过并发写入器的概念,转到并发读取器/单写入器模式,实际上更有意义。 在这种模式下,只能有一个写事务,您必须处理的唯一并发性是与读者的并发性,这可以通过MVCC有效地解决。 数据库同步工具 这使得数据库设计更加简单。 将这一点与输入/输出的串行特性结合起来,数据库的持久性依赖于这一特性(在以后的文章中会有更多的介绍),这实际上很有意义,因为它从数据库代码中去除了很多复杂性。

RavenDB、LMDB、MongoDB和CouchDB都是用一个并发编写器构建的。 事实上,即使像LevelDB或RocksDB这样的数据库实际上也是单个编写器(它们只是进行并发事务合并)。

那么,让我们暂时谈谈交易合并,好吗? LevelDB尤其是一个有趣的例子,因为您可以使用写批处理的概念来写入它,并且多个线程可以同时提交写批处理,从而为我们提供并发写入。 然而,它的实现方式非常有趣。 提交所有写批处理实例的所有线程将它们的批处理添加到一个队列中,然后在一个锁上竞争。 第一个获胜的将运行队列中的所有写批处理并提交它们。

这是一个简单、有吸引力的模型,但我非常不喜欢它。 问题是,您可能有两个写批处理来修改同一个记录,却没有一个很好的方法来了解这一点。 所以你不能真的推理。 因为无论如何都是使用单个锁,所以我更喜欢单个写入器模型,其中写入事务 知道 它是唯一可以修改东西的,所以它不需要担心与其他事务的并发性,这些事务可能对数据库中应该是什么有不同的想法。

当我们实现事务合并时,我们考虑到了显式的乐观并发性,并且成功了。 但这是一个复杂的模型,切换到一个单一的作者使整个事情变得简单得多。

相关文章: