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

Firebase事务中如何实现查询并更新文档?双人游戏匹配需求

在Firebase中用事务实现双人游戏的匹配逻辑

首先得明确:Firebase的事务确实只能对特定文档执行get/set/update/delete操作,没法直接在事务里做集合查询。但我们可以结合「客户端预查询+事务验证」的方式来实现你要的匹配逻辑,同时完全解决你担心的并发变更问题。

核心思路

先给游戏文档设计一个清晰的状态结构,比如每个游戏文档包含:

  • player1: 第一个玩家ID
  • player2: 第二个玩家ID(初始为null)
  • status: 游戏状态(waiting表示等待对手,playing表示已匹配)

实现流程分为两步:

  1. 客户端先查询是否存在status=waiting的游戏,拿到一个候选文档
  2. 把对这个候选文档的更新逻辑放进事务里,在事务中再次验证文档状态——如果状态还是waiting,就加入当前玩家并更新状态;如果已经被修改(比如被其他玩家抢先加入),就重试整个流程

具体代码实现(以JavaScript为例)

async function matchOrCreateGame(currentPlayerId) {
  let gameId;
  const maxRetries = 5; // 限制最大重试次数,防止无限循环
  let retryCount = 0;

  while (!gameId && retryCount < maxRetries) {
    retryCount++;
    try {
      // 第一步:查询等待中的游戏(客户端预查询)
      const waitingGames = await db.collection('games')
        .where('status', '==', 'waiting')
        .limit(1)
        .get();

      if (!waitingGames.empty) {
        // 拿到候选游戏的引用
        const gameRef = waitingGames.docs[0].ref;
        
        // 第二步:用事务验证并更新
        await db.runTransaction(async (transaction) => {
          const gameDoc = await transaction.get(gameRef);
          // 关键:事务内再次验证状态,确保没被其他客户端修改
          if (!gameDoc.exists || gameDoc.data().status !== 'waiting') {
            throw new Error('Game was already taken');
          }
          // 更新游戏,加入当前玩家
          transaction.update(gameRef, {
            player2: currentPlayerId,
            status: 'playing'
          });
          gameId = gameRef.id;
        });
      } else {
        // 没有等待的游戏,创建新游戏
        const newGameRef = db.collection('games').doc();
        await db.runTransaction(async (transaction) => {
          // 验证新文档确实还不存在(防止极端并发下重复创建)
          const gameDoc = await transaction.get(newGameRef);
          if (gameDoc.exists) {
            throw new Error('Game already created');
          }
          transaction.set(newGameRef, {
            player1: currentPlayerId,
            player2: null,
            status: 'waiting'
          });
          gameId = newGameRef.id;
        });
      }
    } catch (err) {
      console.log(`Attempt ${retryCount} failed: ${err.message}, retrying...`);
    }
  }

  if (!gameId) {
    throw new Error('Failed to match or create game after max retries');
  }
  return gameId;
}

关于你担忧的「查询与更新期间文档变更」问题

这正是事务要解决的核心问题:

  • 当你在客户端查询到一个waiting的游戏后,到事务执行更新前,这个文档可能被其他玩家修改了
  • 但事务内部的transaction.get(gameRef)会拿到最新的文档状态,如果发现状态已经不是waiting,我们就抛出异常,触发流程重试
  • Firebase的事务本身是基于乐观并发控制的,如果事务执行期间文档被修改,Firebase会自动重试事务函数(默认最多重试5次),确保最终要么成功执行,要么返回失败

额外注意事项

  • 索引配置:因为用到了where('status', '==', 'waiting'),需要在Firebase控制台给games集合的status字段创建单字段索引
  • 清理过期游戏:可以定期清理status=waiting且创建时间超过N分钟的文档,避免堆积无效游戏
  • 重试限制:一定要加最大重试次数,防止极端情况下无限循环

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 07:27:55