原子性

在 mongodb 中, 写入操作在单个文档的级别上是原子操作, 即使该操作修改单个文档中的多个嵌入文档也是如此。

多文档事务

当单个写入操作 (例如 db.collection.updateMany() ) 修改多个文档时, 每个文档的修改都是原子的, 但整个操作不是原子的。

执行多文档写入操作时, 无论是通过单个写入操作还是多个写入操作, 其他操作都可能会交织在一起。

从4.0 版开始, 对于需要对多个文档进行原子性或读取多个文档之间的一致性的情况, mongodb 为副本集提供多文档事务。

重要

在大多数情况下, 多文档事务比单个文档写入带来更高的性能成本, 多文档事务的可用性不应取代有效的架构设计。

在许多情况下, 非规范化数据模型 (嵌入式文档和数组) 将继续是数据和用例的最佳选择。

也就是说, 在许多情况下, 适当地建模数据将最大限度地减少对多文档事务的需求。

并发控制

并发控制允许多个应用程序同时运行, 而不会导致数据不一致或冲突。

一种方法是在只能具有唯一值的字段上创建唯一索引。

这样可以防止插入或更新创建重复的数据。

在多个字段上创建唯一索引, 以强制该字段值组合上的唯一性。

有关用例的示例, 请参阅 更新唯一索引 findAndModify() and Unique Index.

另一种方法是为写入操作在查询谓词中指定字段的预期当前值。

隔离保证

读取未提交

根据读取问题, 客户端可以在写入持久之前看到写入结果:

无论写问题, 使用 “本地”(local) 或 “可用”(available) 自知的其他客户端都可以在写入操作被确认给颁发客户端之前看到写入操作的结果。

对于多文档事务中的操作, 在事务提交之前, 事务中所做的数据更改在事务之外不可见。

但是, 在向颁发客户端确认提交操作之前, 其他客户端可以在事务提交时看到结果。

使用 “本地” 或 “可用” readConcern 的客户端可以读取数据, 这些数据随后可能会在副本集故障转移期间回滚。

读取未提交是默认的隔离级别, 适用于 mongod kic 独立实例以及副本集和共享群集。

读取未提交的单个文档原子性

对于单个文档, 写入操作是原子操作; 对于单个文档, 写入操作是原子操作。

也就是说, 如果写入更新文档中的多个字段, 则读取操作将永远不会看到仅更新了某些字段的文档。

但是, 尽管客户端可能看不到部分更新的文档, 但读取未提交意味着在更改持久之前, 并发读取操作仍可能看到更新的文档。

对于独立的 mongod 实例, 一组对单个文档的读和写操作是可序列化的。

使用副本集, 只有在没有回滚的情况下, 对单个文档的一组读和写操作才是可序列化的。

读取未提交的多个文档写入

当单个写入操作 (例如 db.collection.updateMany() ) 修改多个文档时, 每个文档的修改都是原子的, 但整个操作不是原子的。

执行多文档写入操作时, 无论是通过单个写入操作还是多个写入操作, 其他操作都可能会交织在一起。

从4.0 版开始, 对于需要对多个文档进行原子性或读取多个文档之间的一致性的情况, mongodb 为副本集提供多文档事务。

重要

在大多数情况下, 多文档事务比单个文档写入带来更高的性能成本, 多文档事务的可用性不应取代有效的架构设计。

在许多情况下, 非规范化数据模型 (嵌入式文档和数组) 将继续是数据和用例的最佳选择。

也就是说, 在许多情况下, 适当地建模数据将最大限度地减少对多文档事务的需求。

重要

在不隔离多文档写入操作的情况下, mongodb 表现出以下行为:

非时间点读取操作。假设读取操作从 t1 时开始, 并开始读取文档。然后, 写入操作将更新提交到其中一个文档, 稍后 t2。读取器可能会看到文档的更新版本, 因此看不到数据的时间点快照。

不可序列化的操作。假设读取操作在 t1 时读取文档 d1, 写入操作在以后的某个时间更新 d1。这引入了读写依赖关系, 因此, 如果要序列化操作, 则读取操作必须在写入操作之前。但也假设写入操作在 t2 时更新文档 d2, 读取操作随后在以后的 t2 读取 d2。这引入了写读依赖关系, 而这将要求读取操作在可序列化计划中的写入操作之后出现。有一个依赖关系周期, 这使得可序列化成为不可能。

读取可能会错过在读取操作过程中更新的匹配文档。

光标快照

在某些情况下, mongodb 游标可以多次返回同一文档。

当游标返回文档时, 其他操作可能会与查询交错。

如果其中一些操作是导致文档移动的更新 (在不推荐使用的 mmapv1 的情况下, 由文档增长引起) 或更改查询使用的索引上的索引字段;然后光标将多次返回相同的文档。

如果集合中有一个或多个从未修改过的字段, 则可以在此字段或这些字段上使用唯一索引, 以便查询返回的文档不会超过一次。

使用 hint() 进行查询, 以显式强制查询使用该索引。

单声道文字

默认情况下, mongodb 为独立的 mongoal 实例和副本集提供单调的写入保证。

对于单调的写入和共享的群集, 请参阅 原因一致性

实时订购

3.4 版中的新版本。

对于主文档上的读和写操作, 发出具有 “可线性化” 的读取注意事项的读取操作, 并使用 “多数人” 写入关注的写入操作, 使多个线程能够在单个文档上执行读取和写入操作, 就像执行了单个线程一样这些操作是实时的;

也就是说, 这些读取和写入的相应计划被认为是可线性化的。

因果一致性

3.6 版中的新版本。

如果操作在逻辑上依赖于前面的操作, 则该操作之间存在因果关系。

例如, 根据指定的条件删除所有文档的写入操作和验证删除操作的后续读取操作具有因果关系。

对于因果一致的会话, mongodb 按尊重其因果关系的顺序执行因果操作, 客户观察与因果关系一致的结果。

客户会话和原因一致性保证

为了提供因果一致性, mongodb 3.6 使客户会话中的因果一致性。

一个因果关系一致的会话表示具有 “多数” 读取关注和写入操作的相关序列具有 “多数” 写入关注项, 并且它们的顺序反映了因果关系。

应用程序必须确保一次只有一个线程在客户端会话中执行这些操作。

对于与因果关系相关的操作:

1、客户端启动客户端会话

重要

客户会话仅保证以下方面的因果一致性:

  1. 读取具有 “多数”(majority) 的操作;即返回数据已被大多数副本集成员确认, 并且是持久的。

  2. 写操作与 “多数” 写关心;即请求确认该操作已应用于副本集的大多数投票成员的写入操作。

有关因果一致性和各种读和写问题的详细信息, 请参阅因果一致性和读与写问题。

2、当客户端发出一系列具有 “多数” 读取关注和写入操作的读取过程 (具有 “多数” 写入关注) 时, 客户端在每个操作中都包含会话信息。

3、对于每个具有 “多数” 读取关注的读取操作, 并使用与会话关联的 “多数” 写入关注项写入操作, mongodb 返回操作时间和群集时间, 即使操作错误也是如此。客户端会话跟踪操作时间和群集时间。

Note

mongodb 不返回 unacknowledged (w: 0) 写入操作的操作时间和群集时间。未被确认的写入并不意味着任何因果关系。

虽然, mongodb 返回读取操作的操作时间和群集时间, 并在客户端会话中确认写入操作, 但只有具有 “多数” 的读取操作读取关注和写入操作具有 “多数” 写入关注项, 则可以保证因果一致性。有关详细信息, 请参阅原因一致性和读、写问题。

关联的客户端会话跟踪这两个时间字段。

注意

操作在不同会话之间可以在因果关系上保持一致。

mongodb 驱动程序和 mongo shell 提供了提高客户端会话的操作时间和群集时间的方法。

因此, 客户端可以提前群集时间和一个客户端会话的操作时间, 以便与另一个客户端会话的操作保持一致。

读取首选项

这些保证适用于 mongodb 部署的所有成员。

例如, 如果在因果关系一致的会话中, 您发出的写上有 “多数” 写关注点, 然后是读取从辅助 (即读取首选项 <secondary>) 与 “多数人” 读取关注的内容, 则读取操作将反映数据库在写入操作后。

分离

具有因果关系一致的会话中的操作不会与会话之外的操作隔离。

如果并发写入操作在会话的写入操作和读取操作之间进行交错, 则会话的读取操作可能会返回反映会话写入操作后发生的写入操作的结果。

因果一致性保证

因果一致性保证

读写因果一致性

线性读取 & findAndModify()

从副本集中读取时, 可以读取过时的数据 (即可能不反映读取操作之前发生的所有写入) 或不持久的数据 (即数据的状态可能反映未被多数人或 repl 确认的写入ica 设置成员, 因此可以回滚), 这取决于所使用的读取关注。

从3.4 版开始, mongodb 引入了 “可线性化” 读取关注, 返回不过时的持久数据。线性读取注意保证仅适用于读取操作指定唯一标识单个文档的查询筛选器的情况。

本教程概述了使用 mongodb 3.2 的部署的替代过程, 使用 db.collection.findAndModify() 读取未过时且无法回滚的数据。

对于 mongodb 3.4, 尽管可以应用概述的过程, 但请参阅 “可线性化” 阅读关注。

findAndModify

此过程使用 db.collection.findAndModify() 读取未过时且无法回滚的数据。

为此, 该过程使用带有写问题的 findAndModify() 方法来修改文档中的虚拟字段。具体而言, 该程序要求:

db.collection.findAndModify() 使用完全匹配的查询, 并且必须存在唯一的索引才能满足查询。

findAndModify()必须实际修改文档;即导致文档的更改。

必须使用写关注 { w: "majority" }

重要

“仲裁读取”(quorum read) 过程有一个巨大的成本, 而不仅仅是使用 “多数” 的读取关注, 因为它导致写入延迟, 而不是读取延迟。

只有在过时是绝对不能容忍的情况下, 才应使用这种技术。

初始化

db.products.insert( [
   {
     _id: 1,
     sku: "xyz123",
     description: "hats",
     available: [ { quantity: 25, size: "S" }, { quantity: 50, size: "M" } ],
     _dummy_field: 0
   },
   {
     _id: 2,
     sku: "abc123",
     description: "socks",
     available: [ { quantity: 10, size: "L" } ],
     _dummy_field: 0
   },
   {
     _id: 3,
     sku: "ijk123",
     description: "t-shirts",
     available: [ { quantity: 30, size: "M" }, { quantity: 5, size: "L" } ],
     _dummy_field: 0
   }
] )

此集合中的文档包含一个名为 _dummy_ 的虚拟字段, 该字段将由本教程中的 db.collection.findAndModify() 递增。

如果该字段不存在, db.collection.findAndModify() 操作将向文档中添加该字段。

该字段的目的是确保 db.collection.findAndModify() 导致对文档的修改。

具体步骤

1. 创建唯一的索引。

在将用于在 db.collection.findAndModify() 操作中指定完全匹配的字段上创建唯一索引。

本教程将在 sku 字段上使用完全匹配。

因此, 在 sku 字段上创建唯一的索引。

db.products.createIndex( { sku: 1 }, { unique: true } )

2. 使用 findAndModify 修改读取提交的数据。

使用 db.collection.findAndModify() 方法对要读取的文档进行简单更新, 并返回修改后的文档。

需要 { w: "majority" } 的写入关注。

若要指定要读取的文档, 必须使用唯一索引支持的完全匹配的查询。

下面的 findAndModify() 操作在唯一索引的字段 sku 上指定完全匹配, 并在匹配的文档中递增命名 _dummy_field 的字段。

虽然没有必要, 但此命令的写入问题还包括5000毫秒的 wtimeout 毫秒值, 以防止在写入无法传播到多数投票成员时操作永远阻塞。

var updatedDocument = db.products.findAndModify(
   {
     query: { sku: "abc123" },
     update: { $inc: { _dummy_field: 1 } },
     new: true,
     writeConcern: { w: "majority", wtimeout: 5000 }
   }
);

即使在副本集中的两个节点认为它们是主节点的情况下, 也只有一个节点能够使用 w: "majority" 完成写入。

因此, 只有当客户端已连接到执行操作的真正主服务器时, 具有 “多数” 写入问题的 findAndModify() 方法才会成功。

由于仲裁读取过程只增加文档中的虚拟字段, 因此您可以安全地重复对 findAndModify() 的调用, 并根据需要调整 wtimeout。

参考资料

write-operations-atomicity

perform-findAndModify-linearizable-reads