如何避免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记录,仅保留最早生成的一条
避免极端并发场景下的残留异常数据。
原数据库表结构

内容的提问来源于stack exchange,提问作者Dong Wang
相关产品推荐
相关产品推荐

