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

Cosmos SDK 3:可空列表字段并发初始化的解决方案咨询

Cosmos DB可空列表字段的并发安全处理方案

问题背景

我定义了这样一个实体类:

class Foo {
   public string id { get; set; }
   public List<int>? items { get; set; }
}

现在要处理items这个可空列表的元素添加操作,遇到了麻烦:

  • 当items已经存在时,正常用追加操作:
    // 场景A:追加元素
    var po = PatchOperation.Add("/items/-", newItem); // 注意路径里的/-
    
    await _container.PatchItemAsync<Foo>(
       foo.id,
       "myPartitionKey",
       new[] { po },
       RequestOptions.DontReturnDataOnPatch);
    
  • 但如果items是null,第一次执行上面的代码会直接抛CosmosException(错误请求),这时候得先初始化列表:
    // 场景B:初始化列表
    var items = new List<int>() { newItem };
    var po = PatchOperation.Add("/items", items); // 路径没有/-
    
    await _container.PatchItemAsync<Foo>(
       foo.id,
       "myPartitionKey",
       new[] { po },
       RequestOptions.DontReturnDataOnPatch);
    

常规做法是先读一遍实体,判断items是否为null再选操作:

var foo = await _container.ReadItemAsync(
   id,
   "myPartitionKey");

if (foo.items == null) {
   // 走场景B初始化
   //...
} else {
   // 走场景A追加
   // ...
}

核心痛点:并发冲突

Cosmos SDK不像EF那样有状态跟踪,从读实体到执行补丁的这段时间里,可能有其他请求(比如Web API的别的调用)已经修改了items的值。这会导致:

  • 要是别的请求已经把items初始化了,我还执行场景B,直接就把已有列表覆盖了;
  • 要是别的请求把items改成null了,我执行场景A就会抛错误请求。

可行解决方案

1. 用条件补丁一步搞定(推荐)

Cosmos的补丁操作支持加条件判断,直接在同一个请求里处理两种情况,不用提前读实体:

var patchOperations = new List<PatchOperation>();

// 只有当items不存在时,才初始化列表
patchOperations.Add(PatchOperation.Add("/items", new List<int>{newItem}, condition: "NOT EXISTS(/items)"));
// 只有当items存在时,才追加元素
patchOperations.Add(PatchOperation.Add("/items/-", newItem, condition: "EXISTS(/items)"));

await _container.PatchItemAsync<Foo>(
   id,
   "myPartitionKey",
   patchOperations,
   RequestOptions.DontReturnDataOnPatch);

这两个操作是互斥的,只会有一个生效,全程不需要读实体,从根源上避免了并发间隙的问题。

2. 让列表默认不为null

直接修改实体类,把items默认初始化为空列表,而不是null:

class Foo {
   public string id { get; set; }
   public List<int> items { get; set; } = new List<int>();
}

这样不管什么时候,都直接用场景A的追加操作就行,不用再处理null的情况,彻底绕开这个问题。如果业务上确实需要把列表置为null,再单独处理清空的逻辑就行。

3. 乐观并发控制

如果必须保留items的可空性,可以用Cosmos的_etag做乐观并发:

  1. 读实体的时候顺便拿到_etag:
    var response = await _container.ReadItemAsync<Foo>(id, "myPartitionKey");
    var foo = response.Resource;
    var etag = response.ETag;
    
  2. 执行补丁的时候带上IfMatchEtag参数,确保操作基于最新的实体状态:
    var requestOptions = new PatchItemRequestOptions { IfMatchEtag = etag };
    PatchOperation po;
    
    if (foo.items == null) {
        po = PatchOperation.Add("/items", new List<int>{newItem});
    } else {
        po = PatchOperation.Add("/items/-", newItem);
    }
    
    try {
        await _container.PatchItemAsync<Foo>(
            id,
            "myPartitionKey",
            new[] { po },
            requestOptions);
    } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed) {
        // 碰到并发冲突了,重新读实体再重试就行
        // 可以加个重试逻辑,比如最多重试3次
    }
    

要是这段时间里有别的请求修改了实体,会抛出412错误,这时候重试一次就能拿到最新状态再操作。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 16:30:08