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

Firestore事务锁失效:如何让并发修改触发事务失败?

Firestore文档锁机制优化问题

我的pick_doc函数目标是为Firestore文档添加锁,Python程序部署在Cloud Functions上,必须确保没有两个函数实例同时锁定/使用同一文档。

我尝试用Firestore Async Client的事务实现锁机制,但读取文档和将doc_lock从False改为True的写入操作之间,仍有多个其他函数对同一文档加锁。函数逻辑是:从Firestore拉取10个未锁定的文档,筛选后随机选一个,只为该文档加锁。目前的异步事务方案中,其他客户端在读写间隙修改文档时,事务仍会执行更新,我需要实现:当锁字段被其他客户端修改时,事务直接失败。

现有代码如下:

class FirestoreClient:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._client = None
        return cls._instance

    async def init_client(self, cred_file):
        try:
            self._client = AsyncClient.from_service_account_json(cred_file)
            print(self._client)
        except Exception as e:
            logging.error(f"Error initializing Firestore client: {e}")

    async def get_client(self):
        if self._client is None:
            raise ValueError("Firestore client not initialized")
        return self._client

    async def close_client(self):
        if self._client is not None:
            print(self._client)
            self._client.close()
            self._client = None
            print("Closed client")


    @firestore.async_transactional
    async def get_doc_lock(transaction, self):
        client = await self.get_client()

        docs_ref = client.collection('docs').where('doc_lock', '==', False)
        docs = await docs_ref.limit(10).get(transaction=transaction)

        if len(docs) == 0:
            return None

        #... logic for filtering the 10 docs goes here

        if len(docs) == 0:
            return None
        
        # pick a random doc from the filtered list
        doc = random.choice(docs)
        doc_ref = client.collection('docs').document(doc.id)
        dock_lock_update = {"doc_lock": True}
        transaction.update(doc_ref, dock_lock_update)
        return doc

    async def pick_doc(self):
        client = await self.get_client()
        tries = 0
        while True:
            tries += 1
            transaction = client.transaction()
            doc = await self.get_doc_lock(transaction, self)
            if doc is not None:
                break
            if tries > 10:
                return None
            else:
                await asyncio.sleep(tries * 3)
                print(f"Doc is None, trying again. Try number: {tries}")
                continue
        return doc
解决方案

问题核心在于:事务中首次查询获取的文档是快照,后续筛选、随机选择的过程中,目标文档可能已被其他客户端锁定,但当前事务仍会执行更新,没有校验最新状态。

需要做两个关键修改:

  • 在更新目标文档前,重新通过事务读取该文档的最新状态,确认doc_lock仍然是False
  • 利用Firestore事务的冲突检测机制,确保只有当文档状态未被修改时才执行更新

修改后的get_doc_lock函数如下:

@firestore.async_transactional
async def get_doc_lock(transaction, self):
    client = await self.get_client()

    docs_ref = client.collection('docs').where('doc_lock', '==', False)
    docs = await docs_ref.limit(10).get(transaction=transaction)

    if len(docs) == 0:
        return None

    #... 保留你的筛选逻辑不变

    if len(docs) == 0:
        return None
    
    # 随机选择一个待锁定的文档
    doc = random.choice(docs)
    doc_ref = client.collection('docs').document(doc.id)

    # 关键:在事务中重新读取该文档的最新状态
    fresh_doc = await doc_ref.get(transaction=transaction)
    if not fresh_doc.exists or fresh_doc.get('doc_lock'):
        # 文档已被锁定,返回None触发事务重试
        return None

    # 执行更新:Firestore事务会自动检测文档状态变化,冲突则失败重试
    transaction.update(doc_ref, {"doc_lock": True})
    return doc

逻辑说明

  • 重新读取文档:首次查询的10个文档状态可能已过期,重新读取目标文档能确保我们操作的是当前未锁定的实例。
  • 事务冲突检测:Firestore事务会监控所有读取的文档,如果在事务执行期间有其他客户端修改了目标文档,事务会自动失败并触发重试(由@firestore.async_transactional装饰器处理)。
  • 双重校验:先读取确认锁状态,再执行更新,即使极端情况下读取后更新前被修改,事务的冲突检测也会阻止无效更新。

另外,pick_doc中的循环重试逻辑可以保留,建议根据实际业务场景调整重试次数和间隔,避免过度等待。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 04:27:11