Firebase定时后台任务同步方案:基于Firestore锁的长耗时任务防重逻辑校验
现有锁实现的评价
你选择用Firestore做分布式锁的核心思路完全正确。Cloud Functions的定时触发器是无状态多实例运行的,靠进程内的变量锁根本无法跨实例阻挡并发触发,必须依赖外部持久化存储做锁判定。但当前的实现存在三个会导致锁失效的明显问题:
- 存在并发竞态漏洞:当前流程是「先查询锁状态、再写入锁标记」,两个步骤不是原子操作。如果两个函数实例在同一时间触发,都可能读到
locked: false的状态,进而同时进入执行流程,完全起不到互斥作用。 - 没有死锁兜底机制:如果任务执行过程中,函数实例被平台冷回收、运行时抛出未捕获的错误,代码就永远走不到
.then分支的解锁逻辑,锁会永久卡在locked: true状态,后续所有定时触发都会被跳过,整个定时任务直接永久瘫痪。 - 锁状态信息缺失:仅存储
locked: true的布尔值,无法区分当前的锁是正在执行任务的有效锁,还是之前崩溃残留的无效死锁。
适配需求的最佳实现方案
针对你“任务运行超过1分钟,自动跳过下一次触发”的需求,结合Firestore的原生能力做改造,最终实现的逻辑要满足三个核心要求:无竞态、不死锁、精准跳过长任务后的触发,具体改造点如下:
- 把「先查再写」的非原子抢锁流程,换成Firestore原生的原子操作能力:用文档
create()方法抢锁,这个方法天生保证“只有文档不存在时才会写入成功”,从根源上杜绝并发抢锁的竞态问题。 - 给锁增加有效期元数据:写锁时同步写入锁的过期时间,过期时长设置为比调度周期更长的冗余值(比如调度是1分钟,就设置锁2分钟过期)。每次抢锁时如果发现已有锁的有效期早于当前时间,就直接判定为残留死锁,覆盖写入新锁,自动清理异常状态。
- 用
try/finally包裹整个业务执行流程,无论任务执行成功还是抛出异常,最终都执行删除锁的操作,保证锁不会被错误卡住。
核心改造后的关键代码片段:
export const pickWinnerBackgroundJob = functions.pubsub.schedule('every 1 minutes').onRun(async (context) => { const lockRef = admin.firestore() .collection(LOCK_COLLECTION_NAME) .doc(BG_JOB_LOCK_DOCUMENT_ID) // 原子抢锁流程 try { // 仅当锁文档不存在时才能创建成功 await lockRef.create({ locked: true, expireAt: admin.firestore.Timestamp.fromMillis(Date.now() + 2 * 60 * 1000) }) } catch (lockExistError) { // 锁文档已存在,检查是否为过期死锁 const lockSnapshot = await lockRef.get() const lockData = lockSnapshot.data() if (lockData.expireAt.toMillis() < Date.now()) { // 死锁则覆盖写入新锁 await lockRef.set({ locked: true, expireAt: admin.firestore.Timestamp.fromMillis(Date.now() + 2 * 60 * 1000) }) } else { // 有效锁存在,直接跳过本次触发 return } } try { // 原有业务逻辑:执行所有Firestore文档更新操作 var promises: Promise<any>; return Promise.all(promises) } finally { // 无论执行成功还是失败,最终释放锁 await lockRef.delete() } })
这种实现可以完全覆盖你的需求:如果某次任务执行超过1分钟,下一次定时触发时会读到还在有效期内的有效锁,直接跳过本次执行;同时靠过期机制自动清理异常崩溃留下的死锁,靠原子操作保证不会出现多实例同时执行的问题。
内容的提问来源于stack exchange,提问作者aryaxt
相关产品推荐
相关产品推荐

