Flutter+Firebase实时数据库匹配系统竞态问题求助
Flutter一对一聊天匹配APP竞态条件问题解决
问题概述
开发的Flutter一对一聊天匹配APP核心功能为随机匹配聊天对象,但当前匹配流程存在竞态条件,约40%场景下双方无法进入同一聊天室。
技术栈与匹配逻辑
- 技术栈:Flutter + Firebase Realtime Database
- 存储结构:待匹配用户存储于
potentialMatches节点,每个用户节点包含年龄、性别、位置偏好、matchUid(初始为空字符串,匹配后存储对方UID)等字段 - 匹配流程:
- 用户进入搜索模式后加入
potentialMatches节点 - 系统筛选符合年龄、性别、位置偏好的潜在匹配对象,随机选择目标用户
- 将当前用户UID写入目标用户的
matchUid节点,同时更新自身matchUid为目标用户UID - 双方通过数据库监听器感知
matchUid更新,进入聊天室
- 用户进入搜索模式后加入
核心问题
部分场景下被匹配用户无法检测到matchUid更新,导致双方无法同步进入聊天室。已尝试Future.delay、StreamController、节流/防抖、Timer、Completer及额外数据库查询等方案,均未解决。
数据库结构

核心代码
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
相关产品推荐
相关产品推荐

