Firebase Firestore并发写入致数据异常的原因排查与方案咨询
问题分析与解决方案
1. 问题根源:确实是并发写入导致的竞态条件
你的推测完全正确,出现手机号重复9次(远超用户3次操作限制)的问题,是由两处非原子的读-改-写操作在并发场景下引发的竞态条件导致:
(1)addPhoneNumber的竞态
当前逻辑是先读取Firestore文档中的numbers数组,在本地添加手机号后再全量写回。当多个用户(或同一用户的多次并发请求)同时执行这一操作时:
- 多个请求会同时读取到同一个
numbers数组的状态(比如数组长度为79999) - 每个请求都在本地数组中添加同一个手机号
- 最后各自将修改后的数组写回Firestore,导致同一个手机号被多次追加,覆盖彼此的修改
(2)canCheckIn的竞态
canCheckIn中先读取用户的checkins字段,减1后再写回的逻辑同样是非原子的:
- 同一用户的多个并发请求会同时读取到
checkins=3的状态 - 每个请求都将
checkins减为2并写回,最终checkins的值只会变成2,而不是0 - 这就导致用户实际获得了远超3次的操作机会,再叠加
addPhoneNumber的并发重复,最终出现手机号重复9次的情况
2. 事务是可行的解决方案
Firestore的事务可以确保读-改-写操作的原子性:事务执行期间,文档会被逻辑锁定,如果期间文档被其他请求修改,事务会自动重试,直到成功或达到重试上限。这能彻底解决并发竞态问题。
(1)修复addPhoneNumber方法
将数组的读取、检查长度、追加、写回逻辑包裹在事务中:
Future addPhoneNumber(String phoneNumber) async { _loadingDialog("Checking In..."); final FirebaseFirestore _firestore = FirebaseFirestore.instance; final List<String> documentNames = ['list1', 'list2', 'list3', 'list4', 'list5']; const int maxNumbersPerDoc = 80000; try { bool addedSuccessfully = false; for (String docName in documentNames) { await _firestore.runTransaction((transaction) async { final docRef = _firestore.collection('contest').doc(docName); final docSnap = await transaction.get(docRef); List<dynamic> numbers = docSnap.exists ? (docSnap.data()?['numbers'] ?? []) : []; if (numbers.length < maxNumbersPerDoc) { numbers.add(phoneNumber); transaction.update(docRef, {'numbers': numbers}); addedSuccessfully = true; } }); if (addedSuccessfully) break; } if (!addedSuccessfully) throw Exception('All documents are full.'); if (context.mounted) Navigator.of(context).pop(); _vibrateAndPlaySound(); _showDialog(); _showInterstitialAd(); loadCheckIns(); } catch (e) { toast_msg('Error adding phone number: $e'); } }
(2)修复canCheckIn方法
同样用事务确保checkins的减1操作是原子性的,避免并发下多扣次数:
Future<bool> canCheckIn() async { _loadingDialog("Validating"); if (Manager.userId == null) { if (mounted) Navigator.of(context).pop(); return false; } final userRef = FirebaseFirestore.instance.collection('users').doc(Manager.userId); bool isValid = false; try { await FirebaseFirestore.instance.runTransaction((transaction) async { final userSnap = await transaction.get(userRef); if (!userSnap.exists) return; final userData = userSnap.data() as Map<String, dynamic>?; int checkins = userData?['checkins'] ?? 0; if (checkins <= 0) return; transaction.update(userRef, {'checkins': checkins - 1}); local_checkin--; isValid = true; }); } catch (e) { print('Error validating check-in: $e'); } if (mounted) Navigator.of(context).pop(); return isValid; }
3. 额外优化建议
- 如果需要避免同一手机号被重复添加(即使是同一用户的多次操作),可以在事务中追加手机号前,先检查
numbers数组中是否已存在该号码,再执行添加操作。 - Firestore事务有默认5次的重试次数限制,如果并发量极大,可以考虑增加重试次数,或在代码中添加自定义重试逻辑。
内容的提问来源于stack exchange,提问作者A.J
相关产品推荐
相关产品推荐

