MongoDB 基础系列十六:增删查改 CRUD Concepts 之线性读取

前言

此篇博文是 Mongdb 基础系列之一;

本文为作者的原创作品,转载需注明出处;

简介

在集群环境中,非常容易读到旧的数据或者是还没有持久化的数据,其实这类数据,我们统称为脏数据;自从 MongoDB 3.4 以来,MongoDB 通过增加了新的特性 linearizable read concern (线性读取的方式)来获取最新的持久化的数据;不过要注意的是,该方式只对单个文档的读取有效;本文将会介绍,MongoDB 是如何通过 db.collection.findAndModify() 来读取最新的数据,并且该数据不能被回滚;

原理

db.collection.findAndModify() 是如何保证对单个文档的读取不会读取到“脏数据”的呢?其实原理并不复杂,就是利用了 MongoDB 对单个文档写操作的事务原子性,当读取的时候,通过对该 document 的一个 dummy 字段进行 udpate,既使用了 document 上的排他锁,这样的话,在读的过程当中就不会有其它的进程对其进行修改该了;具体操作如下:

  • db.collection.findAndModify() 必须使用准确的完整匹配,而且被查询的字段上必须有 Unique index;
  • findAndModify() 必须对该文档进行修改;通常是对一个 dummy 字段进行修改;
  • findAndModify() 必须使用 write concern { w: “majority” };这一步很显然,必须保证写入成功,这里的写入成功是指,上一步的 udpate 操作必须同步到多个从节点,为什么要必须同步到其它多个从节点呢?因为不但要对 primary 节点上排它锁,而且必须对其相关的子节点上排它锁;这样,当前被读取的数据就不存在“脏读”的情况了;

例子

我们来看下面这个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 字段_dummy_field来供 db.collection.findAndModify() 读取的时候执行写操作;但是,如果在执行 db.collection.findAndModify() 操作之前,该 document 并没有该字段_dummy_field,那么 findAndModify() 会自动为其创建一个;

下面,来看一下,我们是如何线性读取 sku 的;

① 创建 unique index

首先,需要对被读取的字段 sku 建立索引;

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

② 使用 findAndModify 读取 committed data

1
2
3
4
5
6
7
8
var updatedDocument = db.products.findAndModify(
{
query: { sku: "abc123" },
update: { $inc: { _dummy_field: 1 } },
new: true,
writeConcern: { w: "majority", wtimeout: 5000 }
}
);
  • 精确匹配,可以看到,对要查询的字段 sku 必须使用精确匹配
  • 必须执行 update 操作,这里默认是对_dummy_field进行修改;保证读取的时候,对该字段上了排它锁,也就避免了脏读的情况;
  • new: true,可选字段,如果设置为 true,返回被修改后的 document,而不是先前的 document;(这里的 modified 其实可以忽略,因为毕竟只是对一个 dummy 字段进行了 udpate)
  • writeConcern: { w: "majority", wtimeout: 5000 },正如前文分析的那样,为了保证不会产生脏读的情况,必须使用majority;而wtimeout呢?为什么需要设置一个超时呢?显然呀,如果多个 processes 并发的在使用线性读的方式呢.. 是吧,都会在竞争该文档的锁,那么为了避免长时间的锁竞争,超时设置是必须的;

References

https://docs.mongodb.com/manual/tutorial/perform-findAndModify-linearizable-reads/