You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何利用Firebase Firestore实现悲观锁处理并发更新?

Firestore实现悲观锁方案

核心思路

以Firestore文档作为锁的载体,每个目标记录的唯一ID对应一个锁文档,通过事务+乐观锁模拟悲观锁效果,结合轮询重试机制,确保同一记录的函数调用串行执行,避免第三方数据库的更新覆盖问题。

锁文档结构

在Firestore中创建locks集合,每个锁文档ID与目标记录ID一一对应,文档包含以下字段:

  • locked: boolean类型,标记锁是否被占用
  • expiresAt: 时间戳,设置锁的过期时间(防止函数崩溃导致锁永久无法释放)
  • owner: 字符串(可选),标记持有锁的函数实例ID,用于问题排查

具体实现步骤

1. 获取锁(步骤0)

编写获取锁的函数,利用Firestore事务实现原子性的锁抢占逻辑,抢占失败则进入轮询重试:

  • 事务内先读取对应锁文档,若文档不存在则直接创建并标记为锁定状态
  • 若文档已存在,检查锁是否处于未占用状态,或是否已过期
  • 满足抢占条件则更新锁状态为已占用,并设置新的过期时间(建议根据函数执行耗时调整,比如5分钟)
  • 事务执行成功则返回锁获取成功;失败则等待指定间隔后重试,直到超时

示例代码(Node.js):

const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');

initializeApp();
const db = getFirestore();

async function acquireLock(recordId, timeoutMs = 30000, retryIntervalMs = 1000) {
  const lockRef = db.collection('locks').doc(recordId);
  const startTime = Date.now();

  while (Date.now() - startTime < timeoutMs) {
    try {
      const success = await db.runTransaction(async (tx) => {
        const lockDoc = await tx.get(lockRef);
        const now = new Date();

        // 锁不存在、已解锁或已过期时,抢占锁
        if (!lockDoc.exists || !lockDoc.data().locked || lockDoc.data().expiresAt.toDate() < now) {
          tx.set(lockRef, {
            locked: true,
            expiresAt: new Date(now.getTime() + 5 * 60 * 1000), // 5分钟过期
            owner: process.env.FUNCTION_INSTANCE_ID || 'unknown'
          }, { merge: true });
          return true;
        }
        return false;
      });

      if (success) {
        console.log(`锁获取成功:${recordId}`);
        return true;
      }
    } catch (err) {
      console.error(`锁获取失败,重试中:${err.message}`);
    }

    // 等待后重试
    await new Promise(resolve => setTimeout(resolve, retryIntervalMs));
  }

  console.error(`锁获取超时:${recordId}`);
  return false;
}

2. 释放锁(步骤4)

函数执行完成(无论成功或失败),必须释放锁,同样通过事务确保原子性操作:

  • 事务内读取锁文档,将locked字段设为false;也可直接删除锁文档(无需保留锁历史时使用)

示例代码:

async function releaseLock(recordId) {
  const lockRef = db.collection('locks').doc(recordId);
  try {
    await db.runTransaction(async (tx) => {
      const lockDoc = await tx.get(lockRef);
      if (lockDoc.exists) {
        tx.update(lockRef, { locked: false });
        // 若无需保留锁记录,可替换为:tx.delete(lockRef);
      }
    });
    console.log(`锁释放成功:${recordId}`);
  } catch (err) {
    console.error(`锁释放失败:${err.message}`);
  }
}

3. 主函数整合

将锁逻辑嵌入原有业务流程,确保无论执行结果如何都能释放锁:

exports.processRecord = async (req, res) => {
  const { recordId, partialPayload } = req.body;

  // 步骤0:获取锁
  const lockAcquired = await acquireLock(recordId);
  if (!lockAcquired) {
    return res.status(503).send('无法获取锁,请稍后重试');
  }

  try {
    // 步骤2:从第三方读取记录
    const thirdPartyRecord = await fetchThirdPartyRecord(recordId);
    // 步骤3:修补记录并存储回第三方
    const updatedRecord = { ...thirdPartyRecord, ...partialPayload };
    await updateThirdPartyRecord(recordId, updatedRecord);

    res.status(200).send('记录更新成功');
  } catch (err) {
    console.error(`记录处理失败:${err.message}`);
    res.status(500).send('记录处理失败');
  } finally {
    // 步骤4:释放锁,无论成功失败都执行
    await releaseLock(recordId);
  }
};

关键注意事项

  • 过期时间配置:需根据函数实际执行耗时设置,确保锁有效期覆盖整个处理流程,同时避免过期时间过长导致死锁
  • 事务原子性:Firestore事务确保锁的读取和更新操作原子化,防止多个函数实例同时修改锁状态
  • 轮询策略:并发量不超10次的场景下,1秒间隔重试足够,超时时间可根据业务需求调整(比如30秒)
  • 异常兜底:必须在finally块中执行锁释放逻辑,避免函数崩溃导致锁永久占用
  • 锁文档清理:可定期清理过期且未锁定的锁文档,或在释放锁时直接删除文档,减少无用数据占用

内容的提问来源于stack exchange,提问作者david_adler

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 13:30:42