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
相关产品推荐
相关产品推荐

