mongodb 无原生 cas 命令,但可通过 findoneandupdate() 结合条件匹配(如 version: 5)与原子更新(如 $inc: {version: 1})模拟 cas 语义;关键在于读出旧值、构造带比较条件的 filter、原子更新并检查 matchedcount === 0 判断是否失败。

什么是 MongoDB 的 CAS 行为
MongoDB 本身没有 CAS 命令,也不像 Memcached 那样提供内置的 CAS token;但它通过 findOneAndUpdate() 或 updateOne() 的「条件匹配 + 原子更新」组合,能模拟出等价的 CAS 语义:即“只有当我读到的旧值(如 version 或 updatedAt)仍存在时,才允许写入”。这本质上就是 Compare-And-Swap 的逻辑落地。
为什么不能直接用 save() 或 updateOne({ _id })
常见错误是只靠 _id 更新,或依赖框架自动递增版本但没校验——这等于放弃比较环节,只剩 “Set”:
-
save()在 Mongoose 中会自动检查__v,但仅限于从 DB 查出的完整文档实例;用new Model({})或 JSON 覆盖后调用save(),__v会被忽略或重置为 0 -
updateOne({ _id: id }, { $set: {...} })完全不检查任何前提状态,后写必然覆盖前写 - Spring Data 的
@Version只在save()和update()(非原生命令)中生效,绕过 ORM 直连 driver 就失效
怎么写出真正起作用的 CAS 式更新
核心就三点:读出旧值 → 构造带比较条件的 filter → 原子更新并检查结果。以 version 字段为例:
✅ 正确写法(Node.js + Driver):
const result = await collection.findOneAndUpdate(
{ _id: docId, version: 5 }, // ← Compare:必须显式带上你读到的 version
{
$set: { status: 'done' },
$inc: { version: 1 } // ← Swap:升版必须和业务更新一起发生
},
{ returnDocument: 'after' }
);
if (result.matchedCount === 0) {
// ← 这才是 CAS 失败的唯一可靠信号:旧 version 已不存在
throw new Error('CAS failed: version mismatch');
}
⚠️ 注意事项:
-
matchedCount === 0是唯一可信指标;modifiedCount可能为 0(比如字段值没变),跟冲突无关 - 别在事务里包一层
findOneAndUpdate—— WiredTiger 会对文档加写锁,反而制造排队,失去 CAS 的轻量优势 - 初始化文档时,
version必须设为数字(0或1),不能是null、undefined或字符串
重试时最容易漏掉的上下文重置
写个 while 循环不算完,很多线上问题出在“重试用了旧快照”:
- 每次重试前必须重新
findOne()拿最新文档,不能复用第一次读出的version和业务字段 - 如果更新前调了发短信、扣余额等外部服务,这些操作必须幂等;否则重试 = 重复扣款
- 除了
matchedCount === 0,还要捕获result.lastErrorObject?.code === 11000(唯一键冲突),它可能和 CAS 失败同时发生
复杂点在于:CAS 不是开关,而是整条读-算-写链路上每个环节都得对齐时间点。稍有松动,乐观就变幻觉。











