MongoDB 文档操作是原子的和隔离的,但它们是不是一致?

Posted

技术标签:

【中文标题】MongoDB 文档操作是原子的和隔离的,但它们是不是一致?【英文标题】:MongoDB Document Operations are Atomic and Isolated, but Are They Consistent?MongoDB 文档操作是原子的和隔离的,但它们是否一致? 【发布时间】:2011-12-26 06:25:16 【问题描述】:

我正在将我的应用程序从 App Engine 数据存储区移植到 MongoDB 后端,并且对“文档更新”的一致性有疑问。我知道一个文档上的更新都是原子的和孤立的,但是有没有办法保证它们在不同的副本集之间是“一致的”?

在我们的应用程序中,许多用户可以(并且将会)尝试通过在一次更新期间向其中插入一些嵌入式文档(对象)来同时更新一个文档。我们需要确保这些更新在所有副本中以逻辑一致的方式发生,即当一个用户将一些嵌入文档“放入”父文档中时,其他用户不能将他们的嵌入文档放入父文档中,直到我们确保他们已经阅读并收到第一个用户的更新。

所以我所说的一致性是指我们需要一种方法来确保如果两个用户尝试恰好同时对一个文档执行更新,MongoDB 只允许其中一个更新通过,并丢弃另一个(或至少防止两者发生)。我们不能在这里使用标准的“分片”解决方案,因为单个更新不仅仅包含增量或减量。

保证一个特定文档的一致性的最佳方法是什么?

【问题讨论】:

我认为您将功劳归于错误的答案。这里的诀窍是原子操作 + findAndModify。在您的情况下,您希望 findAndModify 带有时间戳,以便后续写入失败,直到读取器刷新。 @GatesVP 这两个答案都很好,我鼓励大家阅读这两个答案,以形成更完整的 MongoDB 一致性图景。我选择了 mnemosyn 的回复,因为它解释了 MongoDB 的“写关注”策略以及安全与不安全读取的核心概念。我已经看过像 dcrosta 这样的例子,但需要准确地知道“不安全”读取可以保证和不能保证的内容。 在真实的分布式数据库世界中,你不能依赖时间戳来确定顺序。不同的节点可能(并且将会)有不一致的时钟。如果我们可以使用时间戳,我们就不需要像 Paxos 这样的共识协议。但是由于 MongoDB 本质上是一个单主数据库,因此可以随意使用时间戳。只是不要问哪种方式比旧的 *SQL 更好。 【参考方案1】:

可能还有其他方法可以完成此操作,但一种方法是对文档进行版本控制,并仅针对用户之前阅读过的版本发布更新(即,确保自上次发布以来没有其他人更新过该文档读)。下面是一个使用 pymongo 的技术的简短示例:

>>> db.foo.save('_id': 'a', 'version': 1, 'things': [], safe=True)
'a'
>>> db.foo.update('_id': 'a', 'version': 1, '$push': 'things': 'thing1', '$inc': 'version': 1, safe=True)
'updatedExisting': True, 'connectionId': 112, 'ok': 1.0, 'err': None, 'n': 1

上面注意,key“n”为1,表示文档被更新了

>>> db.foo.update('_id': 'a', 'version': 1, '$push': 'things': 'thing2', '$inc': 'version': 1, safe=True)
'updatedExisting': False, 'connectionId': 112, 'ok': 1.0, 'err': None, 'n': 0

这里我们尝试针对错误的版本进行更新,键“n”是 0

>>> db.foo.update('_id': 'a', 'version': 2, '$push': 'things': 'thing2', '$inc': 'version': 1, safe=True)
'updatedExisting': True, 'connectionId': 112, 'ok': 1.0, 'err': None, 'n': 1
>>> db.foo.find_one()
'things': ['thing1', 'thing2'], '_id': 'a', 'version': 3

请注意,此技术依赖于使用安全写入,否则我们不会收到指示已更新文档数量的确认。对此的一个变体将使用findAndModify 命令,如果没有找到与查询匹配的文档,它将返回文档或None(在 Python 中)。 findAndModify 允许您返回文档的新版本(即应用更新后)或旧版本。

【讨论】:

+1 很好的答案,但值得注意的是,这种方法让客户端(您的应用程序代码)负责检查 findAndModify 命令的结果,如果 findAndModify 重新发出更新失败(表明文档已被其他人更新)。我认为这是一个公平的权衡。 @dcrosta 这是有道理的......所以基本上只要我们使用***别的“安全写入”(WriteConcern.REPLICAS_SAFE 对于我们 Java 爱好者来说),几乎没有两个客户端访问不同副本的机会在同一时间并且两者(成功)编写冲突的“版本:1”更新? @nomizzz 另见:mongodb.org/display/DOCS/… @nomizzz 因为辅助节点不接受写入,即使复制也可以。如果您从辅助节点读取,则有稍高的机会(取决于负载和复制滞后)您将发生“冲突”写入 - 即在接受用户更改之前从辅助节点读取“旧”版本,然后写入新版本版本到主要。 REPLICAS_SAFE 没有事务语义,因此无论如何都会首先在主节点上进行写入——它只会让客户端进行写入,直到新数据复制到至少 2 个辅助节点才返回。【参考方案2】:

MongoDB 不提供主-主复制或多版本并发。换句话说,写入总是会发送到副本集中的同一台服务器。默认情况下,即使从辅助服务器读取也被禁用,因此默认行为是您一次仅与一台服务器通信。因此,如果您使用原子修饰符(如$inc, $push等),则无需担心安全模式下的结果不一致。

如果您不想将自己限制在这些原子修饰符上,按照 dcrosta(和 mongo docs)的建议进行比较和交换看起来是个好主意。然而,所有这些都与副本集或分片无关 - 在单服务器场景中是相同的

如果您还需要在数据库/节点故障的情况下确保读取一致性,则应确保以安全模式写入大多数服务器。

如果您允许不安全的读取,这两种方法的行为会有所不同:原子更新操作仍然可以工作(但可能会产生意想不到的结果),而比较和交换方法会失败。

【讨论】:

我不明白为什么我建议的比较和交换在复制下会失败,但在单服务器中不会​​。我的建议中的关键点是,您只在开始编辑时版本没有更改(即在 Web 表单中)时才编写,并让用户决定如果它已被修改(或以其他方式管理冲突)。在单服务器中,您仍然可以拥有多个用户,因此两种情况下都可能发生冲突。 如果您允许不安全读取(即从辅助读取),读取的版本可能不是最新的。在这种情况下,客户端认为当前版本是 12,但主服务器已经看到写入,将版本增加到 13。现在更新将失败,因为我们从辅助服务器读取。这在单服务器中是不会发生的,在只从主服务器读取时也不会发生。修改器操作仍会执行,但可能不会给出预期的结果,因为旧版本与向用户显示的不同。 如果另一个用户在用户读取版本(在您的示例中为 12)和尝试写入(即在网页提交上)。换句话说,构建代码以预期冲突并适当地处理它们(这是一个特定于应用程序的问题;你是对的,MongoDB 不会以任何方式为你处理这个问题)。 @dcrosta:您描述的是算法预期要做的事情。如果有人同时写作,显然存在冲突,而检测冲突是算法的工作。如果读取操作未读取当前状态,则算法失败(= 未执行预期操作)。这是出乎意料的行为,而不是算法中的写入冲突。 我想这取决于您在这种情况下确定的“预期”。从我的角度来看,它完全符合预期,因为如果从辅助设备读取,您允许(可能)过时的读取。但是,我不想陷入争论,我承认,在狭隘的情况下,它至少会表现出不同的行为,所以说重点。

以上是关于MongoDB 文档操作是原子的和隔离的,但它们是不是一致?的主要内容,如果未能解决你的问题,请参考以下文章

传递文档数组时,Mongoose 中 Model.create 的原子性

MongoDB 原子操作

Erlang ETS 原子和隔离

MongoDB中写操作的原子性是啥意思?

MongoDB事务

翻译MongoDB指南/CRUD操作