Node.js环境下如何检测Firestore集合中是否有新文档正在写入
实现方案
1. 新增互斥锁集合做写入控制
专门创建一个名为write_lock的Firestore集合,集合内只保留一个固定ID的锁文档,比如文档ID设为a_collection_write_lock,文档包含三个字段:is_locking(布尔值,标记是否有写入操作正在执行)、lock_owner(字符串,标记当前持有锁的接口标识,可选)、expire_at(时间戳,锁自动过期时间,避免异常场景下锁永远不释放)。
2. 统一接口执行流程
endpoint_A1和endpoint_A2两个接口收到请求后,都按照以下流程执行:
- 第一步:通过Firestore事务抢占锁,事务内先读取锁文档,判断
is_locking为false或者当前时间已经超过expire_at。如果满足条件,就更新锁文档为持有状态:设置is_locking = true,lock_owner为当前接口标识,expire_at为当前时间加30秒(可根据业务写入最长耗时调整)。如果抢锁失败就等待100ms后重试,最多重试5次,超过重试次数直接返回繁忙提示。 - 第二步:拿到锁之后,先校验另一个集合的最近写入情况,比如
endpoint_A1就查询CollectionA2的最新文档创建时间,确认锁持有期间没有新的写入记录,避免抢锁前刚好有未完成的写入操作。 - 第三步:执行当前接口自身的集合写入逻辑。
- 第四步:写入完成后更新锁文档,把
is_locking改回false,完成整个流程。
3. 核心代码示例(Node.js)
const { Firestore } = require('@google-cloud/firestore'); const db = new Firestore(); const LOCK_DOC_PATH = 'write_lock/a_collection_write_lock'; // 封装抢锁逻辑 async function acquireLock(ownerFlag) { const maxRetries = 5; let retryCount = 0; while(retryCount < maxRetries) { try { const lockResult = await db.runTransaction(async (transaction) => { const lockDoc = await transaction.get(db.doc(LOCK_DOC_PATH)); const now = Date.now(); // 锁不存在/未被持有/已过期 都可以抢占 if (!lockDoc.exists || !lockDoc.data().is_locking || lockDoc.data().expire_at < now) { transaction.set(db.doc(LOCK_DOC_PATH), { is_locking: true, lock_owner: ownerFlag, expire_at: now + 30 * 1000 }, { merge: true }); return true; } return false; }); if (lockResult) return true; } catch (err) { console.error('抢锁异常', err); } retryCount++; // 指数退避等待 await new Promise(resolve => setTimeout(resolve, 100 * retryCount)); } return false; } // 封装释放锁逻辑 async function releaseLock() { await db.doc(LOCK_DOC_PATH).update({ is_locking: false }); } // endpoint_A1接口示例逻辑 async function endpointA1Handler(req, res) { const gotLock = await acquireLock('A1'); if (!gotLock) { return res.status(503).send('服务繁忙,请稍后重试'); } try { // 校验CollectionA2最近10秒无写入 const a2LatestSnapshot = await db.collection('CollectionA2') .orderBy('create_time', 'desc') .limit(1) .get(); if (!a2LatestSnapshot.empty) { const latestA2WriteTime = a2LatestSnapshot.docs[0].data().create_time.toMillis(); if (Date.now() - latestA2WriteTime < 10 * 1000) { await releaseLock(); return res.status(409).send('存在并发写入,请稍后重试'); } } // 执行CollectionA1写入逻辑 await db.collection('CollectionA1').add({ // 你的业务字段 create_time: Firestore.FieldValue.serverTimestamp() }); // 其余后续业务逻辑 res.send('操作成功'); } catch (err) { console.error('A1接口执行异常', err); res.status(500).send('服务内部异常'); } finally { await releaseLock(); } }
注意事项
- 锁文档的操作必须用Firestore事务执行,避免并发抢锁时出现竞态问题
- 锁必须设置超时时间,避免接口异常崩溃导致锁永远不释放,后续请求全部卡住
- 两个集合的所有写入入口都必须接入这套锁逻辑,不能有遗漏,否则互斥控制会失效
内容的提问来源于stack exchange,提问作者KK2491
相关产品推荐
相关产品推荐

