Firestore防重复消费最佳实践:并发更新安全与字段锁定咨询
Firestore 并发修改与重复消费防护最佳实践
场景背景
拥有Employees与Departments两个集合,通过Cloud Functions在员工创建/删除时同步更新对应部门的员工计数,删除操作的实现代码如下:
import * as functions from "firebase-functions"; import admin = require("firebase-admin"); export const handleEmployeeDeletion = functions.firestore .document('Employee/{employeeId}') .onDelete((snapshot, context) => { const departmentRef = admin.firestore() .collection("Department").doc(snapshot.data().departmentId); return departmentRef.update({ employeesCount: admin.firestore.FieldValue.increment(-1) }) })
问题解答
问题1:若两个员工文档被同时删除,使用firestore.FieldValue.increment而非先读取当前值到Cloud Function内存再写入更新值,是否能避免重复消费?
是的,完全可以避免。FieldValue.increment是Firestore提供的原子操作,所有计算逻辑在Firestore服务器端执行,而非本地函数内存中。
当两个删除事件同时触发函数时,两次increment(-1)请求会被Firestore串行化处理:服务器会基于字段的最新实时值完成增减计算,最终employeesCount会正确减少2,不会出现因并发读取旧值导致的"重复消费"(即只减少1的错误)。
如果采用先读再写的方式(先拉取当前employeesCount到本地,计算后再写入),就可能出现两个函数实例同时读取到同一个旧值,各自写入旧值-1的结果,最终只完成一次减1操作,这就是典型的并发修改冲突。
问题2:若字段修改涉及更复杂的计算而非简单增减,如何确保多个会话不会读取相同旧值后并发修改该字段?
这种场景下需要使用Firestore的**事务(Transactions)**来实现原子性的读写操作:
- 事务会先在服务器端锁定目标文档,读取其最新值;
- 基于该值完成复杂计算后,尝试写入更新结果;
- 如果在事务执行期间,有其他操作修改了目标文档,Firestore会自动重试整个事务流程,直到成功或达到重试上限。
举个示例,假设需要根据员工的薪资等级调整部门的平均薪资(复杂计算场景),事务实现逻辑如下:
import * as functions from "firebase-functions"; import admin = require("firebase-admin"); export const handleEmployeeDeletion = functions.firestore .document('Employee/{employeeId}') .onDelete(async (snapshot, context) => { const employeeData = snapshot.data(); const departmentRef = admin.firestore().collection("Department").doc(employeeData.departmentId); return admin.firestore().runTransaction(async (transaction) => { // 读取部门最新数据 const departmentDoc = await transaction.get(departmentRef); if (!departmentDoc.exists) { throw new Error("部门文档不存在"); } const deptData = departmentDoc.data(); // 执行复杂计算:调整平均薪资 const totalSalary = deptData.totalSalary - employeeData.salary; const newEmployeeCount = deptData.employeesCount - 1; const newAvgSalary = newEmployeeCount > 0 ? totalSalary / newEmployeeCount : 0; // 提交更新 transaction.update(departmentRef, { totalSalary, employeesCount: newEmployeeCount, avgSalary: newAvgSalary }); }); });
需要注意:事务中的操作必须是幂等的,避免重试时产生意外副作用;同时事务有重试次数限制,极端高并发场景下需考虑失败后的兜底逻辑。
内容的提问来源于stack exchange,提问作者Menas
相关产品推荐
相关产品推荐

