You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB事务锁能否解决分布式API并发竞态问题

MongoDB分布式场景下/join_game与/close_game竞态问题解决方案

关于MongoDB事务的行为澄清

  • WiredTiger存储引擎默认采用MVCC+文档级锁机制,不存在你担心的“事务持有全局完全锁阻塞所有请求”的问题:
    • 事务只会对当前操作涉及的目标文档加粒度极细的意向锁,不会锁整个集合或数据库,其他无关文档的读写完全不受影响
    • 两个事务同时修改同一个文档时,后发起的请求不会无限阻塞,等待时长超过maxTransactionLockRequestTimeoutMillis配置(默认5ms)后会直接抛出锁冲突错误,不会引发死锁或请求堆积
    • 这类锁冲突属于可重试的瞬时错误,捕获后做1-2次指数退避重试即可,不会引发非预期业务错误
  • 多文档事务确实能覆盖该场景的原子性需求,但这个场景完全不需要上事务,用MongoDB原生的单文档原子更新就能以更低的性能开销彻底解决竞态,不需要额外引入分布式队列、分布式锁等重组件。

最优方案:利用单文档原子更新消除竞态窗口

你之前的逻辑存在竞态的核心原因是:把「校验对局状态」和「更新对局参与者/状态」拆成了两个独立的数据库操作,两个操作之间存在时间差,给了并发请求修改状态的空间。
MongoDB单文档的读+匹配+写操作是天然原子的,只要把状态校验逻辑直接写到更新语句的匹配条件里,就能彻底消灭这个时间差,不管部署多少个服务实例,并发请求都不会出现状态判断和实际写入不一致的问题。

/join_game 接口改造逻辑

不要先查询Games集合判断状态再做更新,直接用findOneAndUpdate完成原子的状态校验+参与者添加:

// 原子操作:仅当对局处于开放状态时,才将用户加入参与者列表
const joinedGame = await db.collection("Games").findOneAndUpdate(
  {
    _id: targetGameId,
    status: "open", // 状态校验直接写在更新匹配条件中
    "participants.userId": { $ne: currentUserId } // 顺便做重复加入校验
  },
  {
    $push: {
      participants: {
        userId: currentUserId,
        joinAt: new Date()
      }
    }
  },
  { returnDocument: "after" }
)

// 匹配结果为空 = 对局已关闭/不存在/用户已加入,直接返回失败
if (!joinedGame) {
  return res.status(403).send("当前对局不可加入")
}

// 走到此处说明加入对局的核心逻辑已经原子生效,再更新用户侧统计即可
// 如果需要保证这一步和前面的对局更新强一致,给这两步套多文档事务即可
await db.collection("Users").updateOne(
  { _id: currentUserId },
  { $inc: { joinedCount: 1 } }
)

/close_game 定时任务接口改造逻辑

同样用原子更新做状态判断,避免重复关闭、以及关闭后仍有用户加入的问题:

// 原子操作:仅当对局处于开放状态时,才标记为关闭
const closedGame = await db.collection("Games").findOneAndUpdate(
  {
    _id: targetGameId,
    status: "open"
  },
  {
    $set: {
      status: "closed",
      closedAt: new Date()
    }
  },
  { returnDocument: "after" }
)

// 匹配结果为空 = 对局已经被关闭过,直接跳过后续逻辑
if (!closedGame) return

// 基于关闭时拿到的最新参与者列表判定获胜者,更新用户获胜统计
const winnerId = calcWinner(closedGame.participants)
await db.collection("Users").updateOne(
  { _id: winnerId },
  { $inc: { winCount: 1 } }
)

方案可靠性说明

  • 针对同一个Games文档的并发更新,MongoDB会串行执行,不存在并行修改的可能:
    • 如果关闭请求先执行完成,对局状态被改为closed,后续的加入请求匹配条件status: "open"不满足,更新直接返回空,不会出现关闭后仍有用户加入的问题
    • 如果加入请求先执行完成,用户被加入参与者列表,后续关闭请求执行时依然能匹配到开放状态的对局,拿到的参与者列表包含刚加入的用户,后续结算逻辑不会漏掉该用户,不会出现数据不一致
  • 该方案完全不依赖服务侧的内存状态,不管你扩容多少个服务实例、容器,所有并发控制逻辑都在MongoDB侧完成,没有分布式组件的维护成本,性能远高于分布式队列、分布式锁方案。
  • 如果业务要求「加入对局时更新Games文档」和「更新Users统计」必须同时成功或同时失败,给这两个操作套一层多文档事务即可,核心的竞态防护逻辑不需要改动,遇到锁冲突做简单重试即可。

内容的提问来源于stack exchange,提问作者Dave

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 03:36:25