Firestore中如何执行批量事务?一致性保障方案咨询
Firestore 事务与统计数据一致性解决方案
关于批量事务的疑问
Firestore 并非不支持批量事务,它提供两种原子性操作方式:
- 批量写入(Batch Write):适合无需读取现有数据、直接执行多个写入/更新/删除操作的场景,所有操作原子性执行,要么全成功要么全失败。但它无法基于现有数据计算后再更新,所以不适合你的统计更新场景。
- 事务(Transaction):支持先读取文档数据,基于读取结果计算后再执行写入/更新,整个过程同样是原子性的,完全能满足你同时处理三个操作并保证一致性的需求。
保证收支统计一致性的实现方式
你的场景需要先读取当前总统计、月度统计的数值,加上新交易的金额后再更新,必须用事务来包裹所有操作。示例代码(以JavaScript为例):
const db = firebase.firestore(); // 假设新交易数据 const newTransaction = { amount: 100, type: 'income', // 收入/支出 date: new Date(), // 其他字段... }; // 执行事务 db.runTransaction(async (transaction) => { // 1. 获取总统计文档 const totalStatsRef = db.collection('stats').doc('total'); const totalStatsDoc = await transaction.get(totalStatsRef); if (!totalStatsDoc.exists) { // 若不存在则初始化 transaction.set(totalStatsRef, { income: 0, expense: 0 }); } const currentTotal = totalStatsDoc.data() || { income: 0, expense: 0 }; // 2. 获取月度统计文档(假设以YYYY-MM为文档ID) const monthKey = `${newTransaction.date.getFullYear()}-${String(newTransaction.date.getMonth() + 1).padStart(2, '0')}`; const monthlyStatsRef = db.collection('stats').doc(monthKey); const monthlyStatsDoc = await transaction.get(monthlyStatsRef); if (!monthlyStatsDoc.exists) { transaction.set(monthlyStatsRef, { income: 0, expense: 0 }); } const currentMonthly = monthlyStatsDoc.data() || { income: 0, expense: 0 }; // 3. 计算更新后的值 const updatedTotal = { income: currentTotal.income + (newTransaction.type === 'income' ? newTransaction.amount : 0), expense: currentTotal.expense + (newTransaction.type === 'expense' ? newTransaction.amount : 0) }; const updatedMonthly = { income: currentMonthly.income + (newTransaction.type === 'income' ? newTransaction.amount : 0), expense: currentMonthly.expense + (newTransaction.type === 'expense' ? newTransaction.amount : 0) }; // 执行三个操作:添加交易、更新总统计、更新月度统计 transaction.set(db.collection('transactions').doc(), newTransaction); transaction.update(totalStatsRef, updatedTotal); transaction.update(monthlyStatsRef, updatedMonthly); }) .then(() => { console.log('事务执行成功'); }) .catch((error) => { console.error('事务失败:', error); });
替代方案:云函数自动触发
如果不想在客户端直接处理事务,也可以用Firebase云函数监听transactions集合的新增文档事件,在云函数内部用事务更新统计数据。这种方式把逻辑放在服务端,避免客户端直接操作统计文档,同时同样依赖事务保证一致性:
// 云函数示例(Node.js) const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); exports.updateStatsOnTransaction = functions.firestore .document('transactions/{transactionId}') .onCreate(async (snap, context) => { const transactionData = snap.data(); const db = admin.firestore(); return db.runTransaction(async (transaction) => { // 以下逻辑和客户端事务一致:读取总统计、月度统计,计算后更新 const totalStatsRef = db.collection('stats').doc('total'); const totalStatsDoc = await transaction.get(totalStatsRef); const currentTotal = totalStatsDoc.data() || { income: 0, expense: 0 }; const monthKey = `${transactionData.date.toDate().getFullYear()}-${String(transactionData.date.toDate().getMonth() + 1).padStart(2, '0')}`; const monthlyStatsRef = db.collection('stats').doc(monthKey); const monthlyStatsDoc = await transaction.get(monthlyStatsRef); const currentMonthly = monthlyStatsDoc.data() || { income: 0, expense: 0 }; const updatedTotal = { income: currentTotal.income + (transactionData.type === 'income' ? transactionData.amount : 0), expense: currentTotal.expense + (transactionData.type === 'expense' ? transactionData.amount : 0) }; const updatedMonthly = { income: currentMonthly.income + (transactionData.type === 'income' ? transactionData.amount : 0), expense: currentMonthly.expense + (transactionData.type === 'expense' ? transactionData.amount : 0) }; transaction.update(totalStatsRef, updatedTotal); transaction.update(monthlyStatsRef, updatedMonthly); }); });
需要注意的是,云函数触发时也要用事务,避免多个并发的交易触发云函数时,统计数据出现竞态条件(比如两个交易同时读取同一个统计值,导致更新后的值不正确)。
内容的提问来源于stack exchange,提问作者Laczkó Örs
相关产品推荐
相关产品推荐

