MongoDB 原子操作(Atomic Operations)完全指南
核心结论:MongoDB 的 原子操作是单文档级别的,通过 写操作符($set, $inc 等)实现 并发安全,无需锁,是高并发系统的基石。
一、原子操作核心原理
| 特性 | 说明 |
|---|---|
| 单文档原子性 | 一次操作只影响 一个文档 |
| 字段级更新 | 使用 $ 操作符修改字段 |
| 无锁实现 | 底层 MVCC + WiredTiger 并发控制 |
| 多文档需事务 | MongoDB 4.0+ 支持 |
二、常用原子操作符
| 操作符 | 功能 | 示例 |
|---|---|---|
$set | 设置字段值 | { $set: { status: "completed" } } |
$unset | 删除字段 | { $unset: { temp: "" } } |
$inc | 增减数值 | { $inc: { views: 1, likes: -1 } } |
$mul | 乘法 | { $mul: { price: 1.1 } } |
$min / $max | 取最小/最大 | { $min: { score: 95 } } |
$push | 追加数组元素 | { $push: { tags: "mongodb" } } |
$addToSet | 去重追加 | { $addToSet: { followers: userId } } |
$pop | 移除首/尾元素 | { $pop: { logs: 1 } } |
$pull | 按条件移除 | { $pull: { tags: "old" } } |
$rename | 重命名字段 | { $rename: { old: "new" } } |
三、原子性实战案例
1. 库存扣减(高并发安全)
db.products.updateOne(
{ _id: "p001", stock: { $gte: 1 } }, // 条件:库存充足
{ $inc: { stock: -1, sold: 1 } } // 原子扣减
)
原子性保证:即使 1000 个请求同时到达,只会成功扣减
stock至 0
2. 点赞/计数器
db.posts.updateOne(
{ _id: "post123" },
{
$inc: { likes: 1 },
$set: { lastLikedAt: new Date() }
}
)
3. 数组去重追加(关注系统)
db.users.updateOne(
{ _id: "u1" },
{ $addToSet: { following: "u2" } }
)
不会重复添加
u2
4. 条件更新(FindAndModify)
db.queue.findAndModify({
query: { status: "pending" },
sort: { priority: -1 },
update: { $set: { status: "processing", worker: "w1" } },
new: true // 返回更新后文档
})
四、原子操作 vs 多文档事务
| 场景 | 推荐方式 |
|---|---|
| 单文档计数、状态变更 | 原子操作符(首选) |
| 转账(扣A + 加B) | 多文档事务(4.0+) |
| 订单 + 库存 + 日志 | 事务 |
// 多文档事务示例
const session = await client.startSession();
try {
session.startTransaction();
await orders.insertOne(doc, { session });
await inventory.updateOne(filter, update, { session });
await session.commitTransaction();
} catch (e) {
await session.abortTransaction();
}
五、原子操作最佳实践
1. 使用 $inc 替代读改写
错误(非原子):
const doc = await coll.findOne({ _id: 1 });
doc.count += 1;
await coll.updateOne({ _id: 1 }, { $set: { count: doc.count } });
正确(原子):
await coll.updateOne({ _id: 1 }, { $inc: { count: 1 } });
2. 结合查询条件防止超卖
db.seckill.updateOne(
{
product_id: "p1",
stock: { $gt: 0 } // 关键:库存 > 0
},
{ $inc: { stock: -1 } }
)
3. 数组操作使用 $addToSet / $pull
// 收藏文章
db.users.updateOne(
{ _id: userId },
{ $addToSet: { favorites: postId } }
)
// 取消收藏
db.users.updateOne(
{ _id: userId },
{ $pull: { favorites: postId } }
)
六、性能对比
| 操作 | 方式 | 并发安全 | 性能 |
|---|---|---|---|
| 计数器 | 读改写 | No | 慢 |
| 计数器 | $inc | Yes | 快 10x |
| 数组追加 | 读改写 | No | 慢 |
| 数组追加 | $addToSet | Yes | 快 |
七、Node.js + Mongoose 原子操作
// 点赞
await Post.updateOne(
{ _id: postId },
{
$inc: { likes: 1 },
$addToSet: { likedBy: userId }
}
);
// 库存扣减
const result = await Product.updateOne(
{ _id: productId, stock: { $gte: 1 } },
{ $inc: { stock: -1 } }
);
if (result.matchedCount === 0) {
throw new Error("库存不足");
}
八、常见误区
| 误区 | 正确做法 |
|---|---|
findOneAndUpdate 默认返回旧文档 | 加 { new: true } |
使用 save() 修改计数器 | 改用 $inc |
| 多字段更新分开写 | 合并为一次 updateOne |
| 忽略查询条件 | 导致覆盖更新 |
九、原子操作 Checklist
| 检查项 | 是否满足 |
|---|---|
使用 $ 操作符? | Yes |
| 单文档操作? | Yes |
| 有并发场景? | Yes |
| 避免读改写? | Yes |
| 结合查询条件? | Yes |
十、终极案例:秒杀系统
// 1. 原子扣库存
const result = await db.seckill.updateOne(
{
product_id: "iphone16",
stock: { $gt: 0 },
start_time: { $lte: now },
end_time: { $gte: now }
},
{
$inc: { stock: -1, sold: 1 },
$set: { last_update: now }
}
);
if (result.matchedCount === 0) {
return { success: false, msg: "已售罄或未开始" };
}
// 2. 创建订单(可异步)
createOrder(userId, "iphone16");
总结:原子操作黄金三原则
| 原则 | 说明 |
|---|---|
| 1. 单文档 | 原子性保证 |
2. 用 $ 操作符 | 字段级更新 |
| 3. 带查询条件 | 防止超卖/覆盖 |
你现在可以:
// 一行实现并发安全的计数器
db.counters.updateOne({ _id: "visits" }, { $inc: { count: 1 } }, { upsert: true })
把你的业务场景告诉我,我帮你写原子操作:
- 操作类型(计数、库存、状态?)
- 并发量(100 QPS?10k QPS?)
- 失败策略(重试?排队?)
回复 3 个关键词,我生成完整原子操作代码 + 压力测试脚本!