Firebase事务中如何实现查询并更新文档?双人游戏匹配需求
在Firebase中用事务实现双人游戏的匹配逻辑
首先得明确:Firebase的事务确实只能对特定文档执行get/set/update/delete操作,没法直接在事务里做集合查询。但我们可以结合「客户端预查询+事务验证」的方式来实现你要的匹配逻辑,同时完全解决你担心的并发变更问题。
核心思路
先给游戏文档设计一个清晰的状态结构,比如每个游戏文档包含:
player1: 第一个玩家IDplayer2: 第二个玩家ID(初始为null)status: 游戏状态(waiting表示等待对手,playing表示已匹配)
实现流程分为两步:
- 客户端先查询是否存在
status=waiting的游戏,拿到一个候选文档 - 把对这个候选文档的更新逻辑放进事务里,在事务中再次验证文档状态——如果状态还是
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
相关产品推荐
相关产品推荐

