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做乐观并发:
- 读实体的时候顺便拿到
_etag:var response = await _container.ReadItemAsync<Foo>(id, "myPartitionKey"); var foo = response.Resource; var etag = response.ETag; - 执行补丁的时候带上
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
相关产品推荐
相关产品推荐

