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

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时间几乎一致),无法实现优先修改已有文档的预期逻辑。


问题根源分析

  1. 并发竞争漏洞:多个请求同时查询房间列表时,都会看到“有空位的房间”,若其中一个请求的事务因并发冲突失败,会直接触发新建房间逻辑;甚至可能多个请求同时跳过加入步骤,重复创建房间。
  2. 判断逻辑错误:原代码中Element['users'][index - 1]['email'] != email的判断完全不合理——它仅检查房间最后一个用户的邮箱,而非当前用户是否已在房间内,会导致符合条件的房间被错误跳过。
  3. 事务逻辑冗余: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 18:04:55