Firestore问题:修改已有文档而非重复创建,避免生成新房间
问题:Firestore优先加入已有房间逻辑失效,重复创建新房间
需求与现状
需求为:优先将用户加入有空位的Firestore房间文档,无合适房间时再新建,要求操作尽可能高效(纳秒级更新)。现有Dart代码逻辑如下:
await room.get().then((QuerySnapshot Snapshots) async { // loop all collections for (var Element in Snapshots.docs) { int index = Element['users'].length; if (Element['users'][index - 1]['email'] != email && index < maxUsers!) { //print(index); List defaultList = []; var map = { '''data''' }; existedRoomId = Element.id; defaultList.add(map); userNotAddedToRoom = false; return await fi.runTransaction((transaction) async { DocumentSnapshot snapshot = Element; transaction.get(snapshot.reference); transaction.update(snapshot.reference, {'users': FieldValue.arrayUnion(defaultList)}); return null; }).then((value) { print("current user added to room"); }).catchError((error) => print("Failed to add to room: $error")); } } if (userNotAddedToRoom) { // create a room return await room .doc(roomId) .set({ ''data''' }) .then((value) => print("Room Added")) .catchError((error) => print("Failed to add room: $error")); } });
遇到的问题:即使存在有空位的房间,系统依然会创建新房间(两个房间的dateCreated时间几乎一致),无法实现优先修改已有文档的预期逻辑。
问题根源分析
- 并发竞争漏洞:多个请求同时查询房间列表时,都会看到“有空位的房间”,若其中一个请求的事务因并发冲突失败,会直接触发新建房间逻辑;甚至可能多个请求同时跳过加入步骤,重复创建房间。
- 判断逻辑错误:原代码中
Element['users'][index - 1]['email'] != email的判断完全不合理——它仅检查房间最后一个用户的邮箱,而非当前用户是否已在房间内,会导致符合条件的房间被错误跳过。 - 事务逻辑冗余:
transaction.get(snapshot.reference)属于冗余调用,事务内部执行更新时会自动获取最新快照;直接使用遍历得到的Element作为快照,可能存在缓存过期问题,无法反映实时文档状态。
修正后的代码实现
步骤1:前置过滤符合条件的房间
利用Firestore查询条件,提前筛选有空位且当前用户未加入的房间,减少客户端遍历开销,同时借助索引提升查询效率:
// 构造查询:用户数小于maxUsers,且当前用户不在房间内 Query eligibleRooms = room .where('users', whereNotIn: [{'email': email}]) .where('users.length', isLessThan: maxUsers!);
步骤2:事务内原子性验证与更新
在事务内部重新获取文档最新状态,再次验证空位和用户未加入的条件,彻底避免并发场景下的判断失效:
await eligibleRooms.get().then((QuerySnapshot snapshots) async { bool addedToRoom = false; // 遍历符合条件的房间,尝试加入 for (var doc in snapshots.docs) { try { await fi.runTransaction((transaction) async { // 事务内获取最新快照,确保状态实时 DocumentSnapshot freshSnapshot = await transaction.get(doc.reference); List users = freshSnapshot['users'] ?? []; // 二次验证:用户未加入且房间有空位 if (!users.any((u) => u['email'] == email) && users.length < maxUsers!) { var newUserMap = { 'email': email // 填充其他用户必要数据 }; transaction.update(freshSnapshot.reference, { 'users': FieldValue.arrayUnion([newUserMap]) }); addedToRoom = true; existedRoomId = freshSnapshot.id; } }); if (addedToRoom) { print("current user added to room"); break; // 成功加入后终止遍历 } } catch (error) { print("Failed to add to room: $error"); // 事务失败(如并发冲突),继续尝试下一个房间 continue; } } // 无可用房间时新建 if (!addedToRoom) { try { await room.doc(roomId).set({ 'users': [ { 'email': email // 填充房间初始化数据 } ], 'dateCreated': FieldValue.serverTimestamp() }); print("Room Added"); } catch (error) { print("Failed to add room: $error"); } } });
关键优化点
- 原子性校验:事务内部再次验证房间状态,避免并发场景下的“幻读”问题,确保更新操作的安全性。
- 查询前置过滤:减少客户端无效遍历,提升整体执行效率。
- 容错处理:单个房间事务失败时自动尝试下一个,避免因局部错误触发不必要的房间创建。
- 修正判断逻辑:替换原有的无效判断,改为检查当前用户是否已在房间内。
内容的提问来源于stack exchange,提问作者Armando Francisco
相关产品推荐
相关产品推荐

