MongoDB 原子操作

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
计数器$incYes快 10x
数组追加读改写No
数组追加$addToSetYes

七、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 })

把你的业务场景告诉我,我帮你写原子操作:

  1. 操作类型(计数、库存、状态?)
  2. 并发量(100 QPS?10k QPS?)
  3. 失败策略(重试?排队?)

回复 3 个关键词,我生成完整原子操作代码 + 压力测试脚本!

文章已创建 2371

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部