Azure Function中Cosmos DB输出绑定的并发如何管理?
问题背景
现有Azure Functions应用部署架构如下:
- Function1:HTTP触发器,接收HTTP调用后将请求payload发送至Service Bus队列即结束运行
- Function2:Service Bus队列触发器,监听到队列新消息后执行逻辑:查询Cosmos DB中对应数据,若数据已存在则更新对应字段,不存在则新建文档
- 故障现象:同时向HTTP触发器发起2个并发请求时,最终仅1个请求的操作能正常生效
- 当前Cosmos DB写入实现:通过输出绑定完成更新,核心代码如下
context.bindings.outputDocument = updatedDocument
问题根因
更新丢失本质是并发场景下的「读-改-写」竞态:
- 两个并发请求对应的Service Bus消息几乎同时被两个Function2实例拉取
- 两个实例同时查询Cosmos DB,拿到同一份旧版本的目标文档
- 两个实例各自基于旧版本修改字段,先后通过输出绑定写入
- 默认配置下的Cosmos DB输出绑定未开启版本校验,后写入的文档要么直接覆盖先写入的内容,要么因版本冲突被静默丢弃,最终表现为仅1个请求的操作生效
并发处理方案
方案1:开启乐观并发控制,配合Service Bus重试机制
改造成本最低,适合更新逻辑复杂、需要基于文档全量内容做计算的场景:
- 给Cosmos DB输出绑定明确配置与文档匹配的
partitionKey,确保同一条逻辑数据的写入落到同一分区 - 查询Cosmos DB获取现有文档时,必须保留文档自带的系统字段
_etag,构造updatedDocument时不要删除该字段 - 开启输出绑定的ETag校验逻辑,写入时只有当传入文档的
_etag与库中当前版本一致时才允许写入,版本不匹配时会抛出412 Precondition Failed错误 - 不要在代码中捕获412错误后直接返回,让异常正常抛出给Functions运行时,触发Service Bus的内置消息重试机制。重试时Function2会重新读取最新版本的文档,在新版本基础上修改后再写入,避免更新覆盖
方案2:替换输出绑定为Cosmos DB原子操作,从根源消除竞态
性能最优,适合高并发场景、更新逻辑为固定字段操作的场景:
- 弃用先查询再写入的输出绑定逻辑,直接在Function2中调用Cosmos DB SDK的Patch API做服务端原子更新,无需提前读取文档
- 对于「不存在则新建」的需求,给Patch请求配置
isUpsert: true即可,整个更新过程在Cosmos DB服务端原子执行,不存在客户端并发读带来的版本不一致问题 - 核心代码示例:
const { CosmosClient } = require("@azure/cosmos"); const client = new CosmosClient(process.env.CosmosDBConnectionString); const container = client.database("你的数据库名").container("你的容器名"); await container.items.patch( targetDocumentId, targetPartitionKeyValue, [ // 按需配置更新操作,例如设置指定字段值 { op: "set", path: "/需要更新的字段路径", value: 对应更新值 } ], { isUpsert: true } );
方案3:调整触发器并发配置,从源头降低冲突概率
适合并发量不高的场景,可搭配前两个方案使用:
- 调低Service Bus触发器的
maxConcurrentCalls参数(例如设为1),控制单实例Function2的并发处理数,减少同一条文档被同时拉取修改的概率,缺点是会降低整体消息处理吞吐量 - 更优的调整方式是:Function1往Service Bus发送消息时,以目标文档的唯一标识作为SessionId,Service Bus触发器会自动保证同SessionId的消息顺序串行处理,既不会出现同文档并发修改的问题,对整体吞吐量的影响也远小于全局调低并发数
选型建议
- 业务并发量低、更新逻辑需要依赖文档现有字段做复杂计算:选择方案1+方案3的组合,改造成本最低
- 业务并发量高、更新逻辑为固定的字段赋值/数值增减/数组操作:优先选择方案2,无重试开销,稳定性最高
内容的提问来源于stack exchange,提问作者Utkarsh
相关产品推荐
相关产品推荐

