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

如何避免Realtime Database的onWrite触发器执行时数据变更导致生成重复游戏ID

问题根因分析

你遇到的重复生成gameId的问题本质是分布式并发读写竞争:

  • 两个玩家几乎同时将status更新为placeholder,两个onWrite触发器会被同时触发,且大概率运行在云函数的不同实例中,你定义的全局waitingPlayers变量是实例内存级别的,既不跨实例共享,也无法处理同实例的并发请求,完全不能作为全局状态判断依据。
  • 两次触发的回调在查询users表时,都已经能读到两个status为placeholder的用户记录,各自独立执行matchmaker逻辑后就会生成两条不同的gameId记录。
解决方案

1. 核心修复:用Realtime Database事务保证原子操作

所有涉及「先判断状态再写入」的逻辑必须用数据库事务实现,事务会保证操作的原子性,避免并发脏读:

  • 新增独立的match_queue节点存储待匹配用户,替代每次全量遍历users表,既提升性能也避免读取到无关用户数据。
  • 对match_queue节点执行事务操作,只有在队列内用户数≥2时才执行匹配逻辑,事务会自动处理并发冲突,只有第一次修改会生效,其余并发请求会自动重试,重试时队列内已无匹配成功的用户,不会重复生成game记录。

2. 优化触发器逻辑

  • 将监听范围从整个users/{uid}节点缩小到users/{uid}/status字段,触发条件从onWrite改为onUpdate,仅当status从其他值变为placeholder时才执行匹配逻辑,减少无效触发。
  • 移除所有全局内存状态变量,所有匹配相关状态全部存储在数据库中,符合云函数无状态的设计要求。

3. 参考实现代码

export const searchWaitingPlayers = functions.database.ref("users/{uid}/status").onUpdate(async (change, context) => {
  const uid = context.params.uid;
  const newStatus = change.after.val();
  // 仅处理进入匹配状态的事件
  if (newStatus !== "placeholder") {
    return null;
  }
  const matchQueueRef = admin.database().ref("match_queue");
  // 将当前用户加入匹配队列,记录加入时间用于排序
  await matchQueueRef.child(uid).set(admin.database.ServerValue.TIMESTAMP);

  // 对匹配队列执行事务,原子处理匹配逻辑
  return matchQueueRef.transaction(async (currentQueue) => {
    if (!currentQueue) return currentQueue;
    // 按加入时间排序待匹配用户
    const waitingUids = Object.keys(currentQueue).sort((a, b) => currentQueue[a] - currentQueue[b]);
    // 不足2人不执行匹配
    if (waitingUids.length < 2) {
      return currentQueue;
    }
    // 取出最先加入队列的2名玩家
    const [player1Uid, player2Uid] = waitingUids.slice(0, 2);
    // 从事务缓存中移除已匹配的2名玩家
    delete currentQueue[player1Uid];
    delete currentQueue[player2Uid];

    // 生成唯一gameId
    const gameId = admin.database().ref("games").push().key;
    // 批量原子更新所有相关数据
    await admin.database().ref().update({
      [`games/${gameId}`]: {
        player1: player1Uid,
        player2: player2Uid,
        status: "waiting",
        createdAt: admin.database.ServerValue.TIMESTAMP
      },
      [`users/${player1Uid}/status`]: "waiting",
      [`users/${player1Uid}/currentGameId`]: gameId,
      [`users/${player2Uid}/status`]: "waiting",
      [`users/${player2Uid}/currentGameId`]: gameId
    });
    // 提交事务更新匹配队列
    return currentQueue;
  });
});

4. 兜底异常处理

可新增定时触发的云函数,每分钟扫描一次数据库:

  • 清理status为placeholder但不在match_queue中的用户,重新加入队列
  • 清理同一组玩家对应的重复game记录,仅保留最早生成的一条
    避免极端并发场景下的残留异常数据。
原数据库表结构

database table

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 04:57:00