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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 13:14:58