如何在Cloud Function触发器中实现API调用幂等性,避免重复请求
问题结论
onCreate类型Cloud Function采用至少一次交付机制,确实存在多次触发、并发执行的竞态条件,你当前的顾虑完全成立:现有代码将不可重试的外部API调用放在Firestore事务内部,一旦事务因为并发修改、网络波动重试,就会重复发起请求,触发业务风险。
错误根因
Firestore事务内的所有逻辑都会随事务重试重复执行,而调用外部第三方API生成面单属于不可重试的非幂等操作,绝对不能放在事务内部执行。
修正方案
核心逻辑是把「锁抢占」和「API调用」完全拆分,用eventId作为唯一锁ID,确保只有抢到锁的实例能执行API调用:
- 先执行第一个纯数据库事务,仅做锁抢占:以eventId为文档ID尝试写入状态为
processing的初始化记录,仅当文档不存在时写入才会成功,否则直接终止流程 - 只有锁抢占成功的实例,才会执行外部API调用,此时不会有其他并发实例能进入API调用环节
- API调用完成后,再执行第二个事务更新文档数据为最终的面单信息
修正后代码示例
let ship_ref = admin.firestore().collection('shipments').doc(event_id) // 第一步:事务抢占锁,不包含任何外部调用 return admin.firestore().runTransaction(async transaction => { const ship_doc = await transaction.get(ship_ref) if (ship_doc.exists) { // 已有记录,直接抛出特定错误终止后续逻辑 throw new Error('DUPLICATE_EVENT') } // 写入初始处理中标记,占住唯一eventId的坑 transaction.create(ship_ref, { status: 'processing', createTime: admin.firestore.FieldValue.serverTimestamp(), completed_sale_ref: snap.ref }) return null }) .then(async () => { // 锁抢占成功,才执行外部API调用,此时不会有并发重复执行的问题 const label_response = await axios({ method: 'post', // 其余axios配置 }) const label_data = label_response.data // 第二步:写入最终面单数据 await admin.firestore().runTransaction(transaction => { return transaction.get(ship_ref).then(ship_doc => { if (!ship_doc.exists) return transaction.update(ship_ref, { status: 'completed', ...sale_data, label_data, tracking_url_data }) return null }) }) }) .catch(e => { if (e.message === 'DUPLICATE_EVENT') { console.log('label already generated or processing') return null } // 其他错误处理,可更新文档状态为failed方便后续重试 return admin.firestore().doc(ship_ref).update({ status: 'failed', error: e.message }).catch(() => {}) })
额外优化点
- 可给
processing状态加超时机制,避免函数执行中途崩溃导致锁永久占用,比如定时任务扫描超过10分钟仍为processing状态的记录,删除后允许重试 - 若外部API本身支持幂等键,可在调用时额外传入eventId作为幂等参数,做双层保障
内容的提问来源于stack exchange,提问作者Grant Singleton
相关产品推荐
相关产品推荐

