Flutter项目Firebase Firestore事务并发操作时数据偶发覆盖丢失问题
问题根因
你遇到的并发修改丢失问题核心是Firestore事务的使用方法错误:
- 事务内部的文档读取操作必须通过
transaction.get()执行,不能直接调用DocumentReference.get()。只有通过事务对象读取的文档才会被纳入冲突检测范围,当你读取后文档被其他请求修改时,事务会自动重试保证原子性,你当前的写法跳过了冲突检测步骤,并发场景下后提交的事务会直接覆盖前序修改。 saveTask方法的set操作没有开启merge,直接全量覆盖文档,进一步放大了修改丢失的概率。
解决方案
1. 修复事务读取逻辑
将两个事务内部的文档读取全部替换为transaction.get(),开启冲突检测。
2. 给saveTask的set操作添加merge配置
避免全量覆盖文档,仅更新修改过的字段。
3. 校验toJson序列化逻辑
确保StudentGroup.toJson()不会将未修改的字段序列化为null或默认值,避免merge时意外覆盖现有有效数据。
4. 可选优化(高并发场景建议)
Firestore单文档最佳写入频率为≤1次/秒,如果你后续操作频率升高,可以将计时器、任务数据拆分到不同的子文档存储,降低单文档并发冲突概率。
修改后代码示例
saveTask 修复版
Future<Result> saveTask({required String sessionId, required String studentGroupId, required Task task}) async { print("trying to save task <$task>."); try { return await _firebaseFirestore.runTransaction((transaction) async { final studentGroupRef = _getStudentGroupDocumentReference( sessionId: sessionId, studentGroupId: studentGroupId ); // 替换为transaction.get() 纳入冲突检测 final sessionGroupDoc = await transaction.get(studentGroupRef); if (!sessionGroupDoc.exists) { return Result.error("student group not exists"); } final sessionGroup = StudentGroup.fromSnapshot(sessionGroupDoc); sessionGroup.game.saveTask(task); // 添加merge配置 transaction.set( studentGroupRef, sessionGroup.toJson(), SetOptions(merge: true) ); }) .then((value) => taskFunction(true)) .catchError((error) => taskFunction(false)); } catch (error) { return Result.error("Error couldn't save task"); } }
updateTaskTimer 修复版
Future<Result> updateTaskTimer({required String sessionId, required String studentGroupId, required Duration duration}) async { print("trying to update first task timer"); try { return await _firebaseFirestore.runTransaction((transaction) async { final studentGroupRef = _getStudentGroupDocumentReference( sessionId: sessionId, studentGroupId: studentGroupId ); // 替换为transaction.get() 纳入冲突检测 final sessionGroupDoc = await transaction.get(studentGroupRef); if (!sessionGroupDoc.exists) { return Result.error("student group not exists"); } final sessionGroup = StudentGroup.fromSnapshot(sessionGroupDoc); switch (sessionGroup.game.gameStage) { case GameStage.First: sessionGroup.game.stages.first.duration = duration.inSeconds; break; case GameStage.Second: sessionGroup.game.stages[1].duration = duration.inSeconds; break; case GameStage.Third: sessionGroup.game.stages[2].duration = duration.inSeconds; break; case GameStage.Fourth: sessionGroup.game.stages[3].duration = duration.inSeconds; break; case GameStage.Fifth: sessionGroup.game.stages[4].duration = duration.inSeconds; break; } transaction.set( studentGroupRef, sessionGroup.toJson(), SetOptions(merge: true) ); print("Did I finish task 4? ${sessionGroup.game.stages.first.tasks[3].isFinished}"); }) .then((value) => timerFunction(true)) .catchError((error) => timerFunction(false)); } catch (error) { return Result.error("Error couldn't update task timer"); } }
内容的提问来源于stack exchange,提问作者ShadeToD
相关产品推荐
相关产品推荐

