MongoDB CRUD操作:快照查询
文章目录
- MongoDB CRUD操作:快照查询
- 对比本地和快照的读关注
- 举例
- 从相同的时间点运行查询
- 从过去某个时刻读取数据的一致状态
- 配置快照保留时间
- 磁盘空间和历史记录
使用快照查询可以读取最近某个时间点的数据,而且从MongoDB 5.0开始,可以使用读关注"snapshot"来查询二级节点上的数据,这显著提高了应用程序读取的通用性和弹性,而且无需像以前那样,先创建数据的静态副本,将其转移到一个单独的系统中,然后手动隔离这些长期运行的查询,以免干扰正常运行工作负载。相反,可以在从一致的数据状态读取数据的同时,针对实时事务数据库执行长期运行查询。
在辅助节点上使用读关注"snapshot"不会影响应用程序的写工作量,只有应用程序读取才会从隔离到辅助节点的长期运行查询中受益。
下面的情况建议使用快照查询:
- 执行多个相关查询,并确保每个查询从同一时间点读取数据。
- 确保从过去某个时间点的一致数据状态读取数据。
对比本地和快照的读关注
当使用MongoDB默认的本地读取关注执行长时间运行的查询时,查询结果可能包含与查询同时发生的写入数据,因此,查询可能会返回意外或不一致的结果。
为了避免这种情况,可以创建一个会话并指定读关注"snapshot",通过读取关注"snapshot",MongoDB可以通过快照隔离运行查询,这意味着查询只读取最近单个时间点出现的数据。
举例
从相同的时间点运行查询
读关注"snapshot"允许在会话中运行多个相关查询,并确保每个查询从相同的时间点读取数据。
例如,动物收容所有一个宠物数据库,其中包含每种宠物的集合。宠物数据库有cats
和dogs
两个集合,它们都包含一个adoptable
字段,表示宠物是否可以领养。其中,cats
集合中的文档如下所示:
{
"name": "Whiskers",
"color": "white",
"age": 10,
"adoptable": true
}
如果要查询所有集合中可供收养的宠物总数,为了提供一致的数据视图,需要返回两个集合同一时间点的数据。这时,就可以在会话中使用读关注快照:
mongoc_client_session_t *cs = NULL;
mongoc_collection_t *cats_collection = NULL;
mongoc_collection_t *dogs_collection = NULL;
int64_t adoptable_pets_count = 0;
bson_error_t error;
mongoc_session_opt_t *session_opts;
cats_collection = mongoc_client_get_collection (client, "pets", "cats");
dogs_collection = mongoc_client_get_collection (client, "pets", "dogs");
/* 使用 pets.cats 和 pets.dogs 数据作为示例 */
if (!pet_setup (cats_collection, dogs_collection)) {
goto cleanup;
}
/* 启动会话快照 */
session_opts = mongoc_session_opts_new ();
mongoc_session_opts_set_snapshot (session_opts, true);
cs = mongoc_client_start_session (client, session_opts, &error);
mongoc_session_opts_destroy (session_opts);
if (!cs) {
MONGOC_ERROR ("Could not start session: %s", error.message);
goto cleanup;
}
/*
* 执行下面的聚合管道,将合计值累加到adoptable_pets_count。
*
* adoptablePetsCount = db.cats.aggregate(
* [ { "$match": { "adoptable": true } },
* { "$count": "adoptableCatsCount" } ], session=s
* ).next()["adoptableCatsCount"]
*
* adoptablePetsCount += db.dogs.aggregate(
* [ { "$match": { "adoptable": True} },
* { "$count": "adoptableDogsCount" } ], session=s
* ).next()["adoptableDogsCount"]
*
* 注意,要实现这个操作必须要通过mongoc_collection_aggregate把客户端会话传递给选项,即:
*
* mongoc_client_session_append (cs, &opts, &error);
* cursor = mongoc_collection_aggregate (
* collection, MONGOC_QUERY_NONE, pipeline, &opts, NULL);
*/
accumulate_adoptable_count (cs, cats_collection, &adoptable_pets_count);
accumulate_adoptable_count (cs, dogs_collection, &adoptable_pets_count);
printf ("there are %" PRId64 " adoptable pets\n", adoptable_pets_count);
在上面的一系列操作中:
- 使用MongoClient()与MongoDB实例建立连接。
- 切换到
pets
数据库。 - 建立会话,该命令指定了
snapshot=True
,因此会话使用读关注快照。 - 对
pets
数据库中的每个集合执行下列操作:- 使用
$match
过滤adoptable
字段为True
的文件。 - 使用
$count
返回已筛选文档的计数。 - 用数据库中的计数递增
adoptablePetsCount
变量。
- 使用
- 打印
adoptablePetsCount
变量。
会话中的所有查询都会读取同一时间点出现的数据,因此,最终计数反映的是一致的数据快照。
注意:如果会话持续时间超过WiredTiger
历史记录保留期(默认为 300 秒),查询就会出现快照过旧(SnapshotTooOld)错误。
从过去某个时刻读取数据的一致状态
读关注快照可确保查询能读取最近某个时间点出现的数据。例如:一家在线鞋店有一个sales
集合,其中包含该店售出的每一件商品的数据。其中,sales
集合中的一个文档如下所示:
{
"shoeType": "boot",
"price": 30,
"saleDate": ISODate("2022-02-02T06:01:17.171Z")
}
每天晚上都会运行一个查询来查看当天售出了多少双鞋。每日销售查询如下所示:
mongoc_client_session_t *cs = NULL;
mongoc_collection_t *sales_collection = NULL;
bson_error_t error;
mongoc_session_opt_t *session_opts;
bson_t *pipeline = NULL;
bson_t opts = BSON_INITIALIZER;
mongoc_cursor_t *cursor = NULL;
const bson_t *doc = NULL;
bool ok = true;
bson_iter_t iter;
int64_t total_sales = 0;
sales_collection = mongoc_client_get_collection (client, "retail", "sales");
/* 使用'retail.sales'示例数据 */
if (!retail_setup (sales_collection)) {
goto cleanup;
}
/* 开始快照会话 */
session_opts = mongoc_session_opts_new ();
mongoc_session_opts_set_snapshot (session_opts, true);
cs = mongoc_client_start_session (client, session_opts, &error);
mongoc_session_opts_destroy (session_opts);
if (!cs) {
MONGOC_ERROR ("Could not start session: %s", error.message);
goto cleanup;
}
if (!mongoc_client_session_append (cs, &opts, &error)) {
MONGOC_ERROR ("could not apply session options: %s", error.message);
goto cleanup;
}
pipeline = BCON_NEW ("pipeline",
"[",
"{",
"$match",
"{",
"$expr",
"{",
"$gt",
"[",
"$saleDate",
"{",
"$dateSubtract",
"{",
"startDate",
"$$NOW",
"unit",
BCON_UTF8 ("day"),
"amount",
BCON_INT64 (1),
"}",
"}",
"]",
"}",
"}",
"}",
"{",
"$count",
BCON_UTF8 ("totalDailySales"),
"}",
"]");
cursor = mongoc_collection_aggregate (sales_collection, MONGOC_QUERY_NONE, pipeline, &opts, NULL);
bson_destroy (&opts);
ok = mongoc_cursor_next (cursor, &doc);
if (mongoc_cursor_error (cursor, &error)) {
MONGOC_ERROR ("could not get totalDailySales: %s", error.message);
goto cleanup;
}
if (!ok) {
MONGOC_ERROR ("%s", "cursor has no results");
goto cleanup;
}
ok = bson_iter_init_find (&iter, doc, "totalDailySales");
if (ok) {
total_sales = bson_iter_as_int64 (&iter);
} else {
MONGOC_ERROR ("%s", "missing key: 'totalDailySales'");
goto cleanup;
}
在上面的一系列操作中:
- 使用
$match
和$expr
对saleDate
字段进行过滤。-
$expr
允许在$match
阶段使用聚合表达式(如NOW
)。
-
- 使用
$gt
运算符和$dateSubtract
表达式,返回saleDate
大于执行查询前一天的文档。 - 使用
$count
返回匹配文档的计数。计数存储在totalDailySales
变量中。 - 指定读关注快照,确保查询从单一时间点读取。
因为sales
集合比较大,所以运行查询可能需要几分钟时间。由于是在线商店,销售可能在一天中的任何时间发生。
例如下面的情况:
- 查询在上午 12:00 开始执行。
- 一位顾客在上午 12:02 时购买了三双鞋。
- 查询在凌晨 12:04 执行完毕。
如果查询不使用读关注快照,那么在查询开始和查询结束之间发生的销售额就会包含在查询计数中,尽管这些销售额并不是在报告当天发生的。这可能导致报告不准确,某些销售额被计算两次。
指定读取 "快照 "后,查询只返回查询开始执行前不久数据库中的数据。
如果查询时间超过WiredTiger历史保留期(默认情况下为 300 秒),查询就会出错,并显示 SnapshotTooOld 错误。
配置快照保留时间
默认情况下,WiredTiger 存储引擎会保留 300 秒的历史记录。使用 snapshot=true
的会话,从会话中的第一个操作开始到最后一个操作结束,总共可使用 300 秒。如果使用会话的时间更长,会话就会因 SnapshotTooOld 错误而失败。同样,如果使用读关注快照来查询数据,且查询持续时间超过 300 秒,查询就会失败。
如果查询或会话运行时间超过 300 秒,可以考虑延长快照保留时间,可以通过修改minSnapshotHistoryWindowInSeconds
参数增加快照保留期,例如,把minSnapshotHistoryWindowInSeconds
的值设置为 600 秒:
db.adminCommand( { setParameter: 1, minSnapshotHistoryWindowInSeconds: 600 } )
磁盘空间和历史记录
需要注意,增加 minSnapshotHistoryWindowInSeconds
的值会增加磁盘使用量,因为服务器必须在指定的时间窗口内保持较早修改值的历史记录。磁盘空间的使用量取决于工作负载,工作量越大,需要的磁盘空间就越大。