Skip to content

MongoDB - 原子操作

原子性(Atomicity)确保操作要么完全完成,要么根本不发生,从而防止数据不一致。在 MongoDB 中,对单个文档的操作始终是原子性的。本教程将探讨如何利用这一点实现可靠的数据更新,并介绍现代的多文档事务。

策略一:通过嵌入式文档实现单文档原子性

Section titled “策略一:通过嵌入式文档实现单文档原子性”

对于频繁更新的相关数据,最佳实践是将其嵌入到单个文档中。这利用了 MongoDB 原生的单文档原子性。当您更新文档时,其所有嵌入字段都在一个原子操作中完成更新。

我们以一个电子商务库存场景为例。我们将在一个文档中跟踪产品的库存及其购买历史。

// 首先,我们向 'products' 集合中插入一个产品
db.products.insertOne({
"_id": "SKU12345",
"productName": "Quantum Laptop Pro",
"category": "Electronics",
"stock": {
"total": 10,
"available": 10
},
"purchaseHistory": []
});
// 返回值: { "acknowledged": true, "insertedId": "SKU12345" }

当顾客购买此笔记本电脑时,我们需要原子性地减少可用库存并记录购买。这可以防止两个顾客同时购买最后一件商品而产生的竞态条件。我们使用 findOneAndUpdate() 来实现此目的,因为它在一次原子性的服务器调用中查找并更新文档。

// 如果库存可用,则原子性地处理购买
db.products.findOneAndUpdate(
// 过滤器:通过 ID 查找产品并确保库存可用
{
_id: "SKU12345",
"stock.available": { $gt: 0 }
},
// 更新:减少可用库存并在购买历史中添加新条目
{
$inc: { "stock.available": -1 },
$push: {
"purchaseHistory": {
"customerId": "CUST987",
"purchaseDate": new Date()
}
}
},
// 选项:返回更新后的文档以确认更改
{
returnNewDocument: true
}
);

这个单一操作保证了只有在库存可用时才减少库存,并且只有在库存成功减少时才记录购买。整个更改是原子性的。

如果某个操作需要原子性地更新多个文档怎么办?例如,将资金从一个用户账户转移到另一个账户。从 MongoDB 4.0 开始,您可以为这些场景使用多文档 ACID 事务。

事务通过会话(session)对象处理。事务中的所有操作都会被暂存,然后作为一个单一的原子单元被提交(应用)或中止(丢弃)。

以下是银行转账的一个概念性示例:

// 注意:这需要副本集配置。
// 1. 启动客户端会话。
const session = db.getMongo().startSession();
// 2. 启动事务。
session.startTransaction();
const accountsCollection = session.getDatabase('bank').collection('accounts');
try {
// 3. 在事务中执行操作。
// 从发件人账户扣款
accountsCollection.updateOne(
{ _id: 'accountA' },
{ $inc: { balance: -100 } },
{ session }
);
// 向接收人账户存款
accountsCollection.updateOne(
{ _id: 'accountB' },
{ $inc: { balance: 100 } },
{ session }
);
// 4. 提交事务。
session.commitTransaction();
console.log('事务成功提交。');
} catch (error) {
// 5. 如果发生任何错误,则中止事务。
console.log('发生错误。中止事务。');
session.abortTransaction();
throw error;
} finally {
// 6. 结束会话。
session.endSession();
}

当您的数据结构允许时,始终优先选择单文档原子模型。它比多文档事务性能更高且复杂性更低。仅当跨多个文档的原子性是绝对必要时才使用事务。