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

Flutter+Firebase实时数据库匹配系统竞态问题求助

Flutter一对一聊天匹配APP竞态条件问题解决

问题概述

开发的Flutter一对一聊天匹配APP核心功能为随机匹配聊天对象,但当前匹配流程存在竞态条件,约40%场景下双方无法进入同一聊天室。

技术栈与匹配逻辑

  • 技术栈:Flutter + Firebase Realtime Database
  • 存储结构:待匹配用户存储于potentialMatches节点,每个用户节点包含年龄、性别、位置偏好、matchUid(初始为空字符串,匹配后存储对方UID)等字段
  • 匹配流程:
    1. 用户进入搜索模式后加入potentialMatches节点
    2. 系统筛选符合年龄、性别、位置偏好的潜在匹配对象,随机选择目标用户
    3. 将当前用户UID写入目标用户的matchUid节点,同时更新自身matchUid为目标用户UID
    4. 双方通过数据库监听器感知matchUid更新,进入聊天室

核心问题

部分场景下被匹配用户无法检测到matchUid更新,导致双方无法同步进入聊天室。已尝试Future.delay、StreamController、节流/防抖、Timer、Completer及额外数据库查询等方案,均未解决。

数据库结构

Firebase实时数据库potentialMatches节点结构

核心代码

void _activateSearchMode() async {
    //Create reference for potential matches in DB
    DatabaseReference potentialMatchesRef =
        FirebaseDatabase.instance.ref('potentialMatches');

    Stream<DatabaseEvent> potentialMatchesAvailable =
        potentialMatchesRef.onValue;

    potentialMatchesStream =
        potentialMatchesAvailable.listen((DatabaseEvent event) {
      List<PotentialMatches> potentialMatchesList = [];

      final potentialMatchesMap =
          event.snapshot.value as Map<dynamic, dynamic>?;

      for (var key in potentialMatchesMap?.keys ?? {}) {
        if (key != currentUserData.currentUser!.uid) {
          potentialMatchesList
              .add(PotentialMatches.fromRTDB(potentialMatchesMap![key]));
        }
      }

      if (_isUserAvailable && potentialMatchesList.isNotEmpty) {
        _findMatch(
            PotentialMatches.fromRTDB(
                potentialMatchesMap![currentUserData.currentUser!.uid]),
            potentialMatchesList,
            potentialMatchesMap);
      }
    });
  }

Future<void> _findMatch(
      final PotentialMatches currentUser,
      final List<PotentialMatches> potentialMatches,
      final potentialMatchesMap) async {
    final isUserClaimed = _checkIfClaimed(currentUser);

    switch (isUserClaimed) {
      case false:
        final matchFound = getRandomUser(currentUser, potentialMatches);

        if (matchFound != null) {
          // update both matched found DB references for both current user and matched user
          _claimMatchedUser(currentUser, matchFound);

          _isUserAvailable = false;

          _printMatchValues(currentUser, matchFound, isUserClaimed);

          await sendFCMNotification(
              'Match Found',
              'Come back to talk to ${currentUser.username}!',
              matchFound.fcmToken);

          _goToMatchFoundScreen(currentUser, matchFound, isUserClaimed);
        }
        break;

      case true:
        final matchFound = PotentialMatches.fromRTDB(
            potentialMatchesMap![currentUser.matchUid]);

        _isUserAvailable = false;

        _printMatchValues(currentUser, matchFound, isUserClaimed);

        await sendFCMNotification(
              'Match Found',
              'Come back to talk to ${currentUser.username}!',
              matchFound.fcmToken);

        _goToMatchFoundScreen(currentUser, matchFound, isUserClaimed);
        break;

      default:
        break;
    }
  }

bool _checkIfClaimed(final PotentialMatches currentUser) {
    if (currentUser.matchUid == '') {
      return false;
    } else {
      return true;
    }
  }

 Future<void> _claimMatchedUser(final PotentialMatches currentUser,
      final PotentialMatches matchFound) async {
    DatabaseReference matchFoundRef =
        FirebaseDatabase.instance.ref('potentialMatches/${matchFound.uid}');
    DatabaseReference currentUserRef =
        FirebaseDatabase.instance.ref('potentialMatches/${currentUser.uid}');

    await matchFoundRef.update({"matchUid": currentUser.uid});
    await currentUserRef.update({"matchUid": matchFound.uid});
  }

const PotentialMatches({
    required this.genderPreference,
    required this.agePreference,
    required this.matchUid,
    required this.username,
    required this.uid,
    required this.userAge,
    required this.userGender,
    required this.userLocation,
    required this.locationPreference,
    required this.matchDistance,
    required this.fcmToken,
  });

  factory PotentialMatches.fromRTDB(Map<dynamic, dynamic> data) {
    return PotentialMatches(
      genderPreference: data['genderPreference'] as String,
      locationPreference: data['locationPreference'] as String,
      agePreference: data['agePreference'] as Map<dynamic, dynamic>?,
      matchDistance: data['matchDistance'] as int,
      matchUid: data['matchUid'] as String,
      username: data['username'] as String,
      uid: data['uid'] as String,
      userAge: data['age'] as int,
      userGender: data['gender'] as String,
      userLocation: data['location'] as Map<dynamic, dynamic>?,
      fcmToken: data['fcmToken'] as String,
    );
  }

PotentialMatches? getRandomUser(
    PotentialMatches currentUser, List<PotentialMatches> potentialMatches) {
  final matchedUsers = potentialMatches.where((otherUser) {
    final currentUserAgePreference =
        MinMaxAgePreference.fromAgePreference(currentUser.agePreference!);
    final otherUserAgePreference =
        MinMaxAgePreference.fromAgePreference(otherUser.agePreference!);
    final currentUserLocation = LatLng.fromLocation(currentUser.userLocation!);
    final otherUserLocation = LatLng.fromLocation(otherUser.userLocation!);
    final distanceInMiles =
        calculateDistance(currentUserLocation, otherUserLocation);

    debugPrint(
        'Distance between current user and other user: $distanceInMiles miles');

    final currentUserMatchDistance = currentUser.locationPreference == 'nearby'
        ? currentUser.matchDistance
        : double.infinity;

    final otherUserMatchDistance = otherUser.locationPreference == 'nearby'
        ? otherUser.matchDistance
        : double.infinity;

    final effectiveMatchDistance =
        min(currentUserMatchDistance, otherUserMatchDistance);

    final isNearby = (currentUser.locationPreference == 'nearby' ||
            otherUser.locationPreference == 'nearby') &&
        distanceInMiles <= effectiveMatchDistance;

    final isAnywhere = (currentUser.locationPreference == 'anywhere' &&
            otherUser.locationPreference == 'anywhere') ||
        (currentUser.locationPreference == 'anywhere' && isNearby) ||
        (otherUser.locationPreference == 'anywhere' && isNearby);

    return currentUser.uid != otherUser.uid &&
        (currentUser.genderPreference == otherUser.userGender ||
            currentUser.genderPreference == 'Any' ||
            otherUser.userGender == 'Non-Binary') &&
        (currentUser.userGender == otherUser.genderPreference ||
            otherUser.genderPreference == 'Any' ||
            currentUser.userGender == 'Non-Binary') &&
        (currentUserAgePreference.max >= otherUser.userAge &&
            currentUserAgePreference.min <= otherUser.userAge) &&
        (currentUser.userAge <= otherUserAgePreference.max &&
            currentUser.userAge >= otherUserAgePreference.min) &&
        (currentUser.matchUid == '' && otherUser.matchUid == '') &&
        (isNearby || isAnywhere);
  }).toList();

  return matchedUsers.isNotEmpty
      ? matchedUsers[Random().nextInt(matchedUsers.length)]
      : null;
}

解决方案

1. 使用Firebase事务确保原子性

当前_claimMatchedUser是两个独立的update操作,存在并发修改风险——多个用户可能同时修改同一个目标用户的matchUid。使用事务可以确保只有当目标用户的matchUid为空时才进行更新,避免竞态。

修改后的_claimMatchedUser:

Future<void> _claimMatchedUser(final PotentialMatches currentUser, final PotentialMatches matchFound) async {
  DatabaseReference matchFoundRef = FirebaseDatabase.instance.ref('potentialMatches/${matchFound.uid}');
  
  // 原子性锁定匹配对象的matchUid
  await matchFoundRef.runTransaction((mutableData) async {
    Map<String, dynamic>? data = mutableData.value as Map<String, dynamic>?;
    // 仅当matchUid为空时允许更新
    if (data?['matchUid'] == '') {
      mutableData.value = {...data!, 'matchUid': currentUser.uid};
      return Transaction.success(mutableData);
    } else {
      // 目标已被其他用户匹配,终止事务
      return Transaction.abort();
    }
  }).then((transactionResult) async {
    if (transactionResult.committed) {
      // 匹配成功,更新当前用户的matchUid
      await FirebaseDatabase.instance.ref('potentialMatches/${currentUser.uid}').update({"matchUid": matchFound.uid});
      _isUserAvailable = false;
      // 触发后续聊天流程
      _printMatchValues(currentUser, matchFound, false);
      await sendFCMNotification(
          'Match Found',
          'Come back to talk to ${currentUser.username}!',
          matchFound.fcmToken);
      _goToMatchFoundScreen(currentUser, matchFound, false);
    } else {
      // 匹配失败,重新开启搜索
      _isUserAvailable = true;
    }
  });
}

2. 优化监听器逻辑,减少无效触发

当前监听整个potentialMatches节点的onValue事件,任何用户的变化都会触发处理,容易导致基于旧数据的重复操作。改为监听当前用户自身节点的变化,仅在自身matchUid更新时触发逻辑。

修改后的_activateSearchMode:

void _activateSearchMode() async {
  String currentUid = currentUserData.currentUser!.uid;
  DatabaseReference currentUserRef = FirebaseDatabase.instance.ref('potentialMatches/$currentUid');
  
  // 仅监听当前用户自身节点的变化
  potentialMatchesStream = currentUserRef.onValue.listen((DatabaseEvent event) {
    final currentUserFromDB = PotentialMatches.fromRTDB(event.snapshot.value as Map<dynamic, dynamic>);
    
    // 检查是否已被匹配
    if (currentUserFromDB.matchUid != '') {
      _handleMatchedState(currentUserFromDB);
      return;
    }
    
    // 未被匹配时,查询潜在匹配对象
    if (_isUserAvailable) {
      _queryPotentialMatches(currentUserFromDB);
    }
  });
}

// 查询符合条件的潜在匹配对象
Future<void> _queryPotentialMatches(PotentialMatches currentUser) async {
  DatabaseReference potentialMatchesRef = FirebaseDatabase.instance.ref('potentialMatches');
  // 过滤出matchUid为空的用户,减少无效数据传输
  Query query = potentialMatchesRef.orderByChild('matchUid').equalTo('');
  
  DatabaseEvent event = await query.once();
  List<PotentialMatches> potentialMatchesList = [];
  
  final potentialMatchesMap = event.snapshot.value as Map<dynamic, dynamic>?;
  if (potentialMatchesMap != null) {
    for (var key in potentialMatchesMap.keys) {
      if (key != currentUser.uid) {
        potentialMatchesList.add(PotentialMatches.fromRTDB(potentialMatchesMap[key]));
      }
    }
  }
  
  if (potentialMatchesList.isNotEmpty) {
    final matchFound = getRandomUser(currentUser, potentialMatchesList);
    if (matchFound != null) {
      await _claimMatchedUser(currentUser, matchFound);
    }
  }
}

// 处理被匹配后的逻辑
void _handleMatchedState(PotentialMatches currentUser) {
  _isUserAvailable = false;
  // 获取匹配对象信息
  FirebaseDatabase.instance.ref('potentialMatches/${currentUser.matchUid}').once().then((event) {
    PotentialMatches matchFound = PotentialMatches.fromRTDB(event.snapshot.value as Map<dynamic, dynamic>);
    _printMatchValues(currentUser, matchFound, true);
    sendFCMNotification(
        'Match Found',
        'Come back to talk to ${currentUser.username}!',
        matchFound.fcmToken);
    _goToMatchFoundScreen(currentUser, matchFound, true);
  });
}

3. 移除本地状态依赖,以数据库状态为准

删除本地_isUserAvailable变量的直接依赖,每次操作前从数据库读取当前用户的matchUid状态,确保逻辑基于最新数据。

方案说明

  • 事务机制:确保匹配过程的原子性,避免多个用户同时抢占同一个匹配对象
  • 定向监听:减少无效的数据流触发,降低客户端处理压力,确保只响应自身状态变化
  • 数据驱动:以数据库中的真实状态作为逻辑判断依据,避免本地状态与远程数据不一致导致的问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 00:17:02