Cosmos DB(SQL API)线程安全分配可用文档的实现方案咨询
Cosmos DB 航班座位分配并发问题的标准实现方案
你当前考虑的基于ETag的乐观并发重试方案是符合Cosmos DB并发安全逻辑的,正确性有保障,但不是性能最优的实现,这类"原子抢占唯一资源"的场景,Cosmos DB SQL API有原生的标准模式可以用,实现更简单、开销更低。
现有ETag方案的优缺点
- 优势:完全利用Cosmos DB原生的乐观并发控制机制,只要更新时携带读取到的ETag做校验,就不会出现超卖、重复分配的问题,逻辑容易理解
- 劣势:
- 每次分配需要先发起查询请求读未分配座位,再发更新请求,两次网络往返延迟高
- 高并发场景下,大量请求会同时查询到同一个未分配座位,再扎堆发起带ETag的更新,冲突率极高,会触发大量重试,甚至引发重试风暴拉高整体延迟
- 客户端需要维护读、校验、重试的整套逻辑,代码复杂度高
最优标准实现:服务端原子条件Patch(部分更新)
这是Cosmos DB官方针对这类原子抢占场景推荐的标准写法,核心是把"查找符合条件的未分配座位"和"标记为已分配"两个动作合并成单次服务端侧原子执行的请求,完全不需要客户端提前读文档、传ETag校验。
具体实现逻辑:
- 所有请求必须明确携带分区键值,即对应航班的
FlightNumber_DepartureDateTime,所有操作限定在单分区内执行,避免跨分区带来的额外开销和一致性问题 - 直接发起Patch(部分文档更新)请求,在请求中直接写入匹配条件和更新操作:
- 匹配谓词(predicate)设置为
c.allocated = false,可根据需要增加座位排序规则(比如按座位号升序、排除特殊座席等) - 更新操作直接指定两个字段的赋值:设置
allocated = true,allocatedTo = <当前请求的ticketReference>
- 匹配谓词(predicate)设置为
- 当请求返回更新成功时,座位分配完成;如果返回"无匹配文档",说明当前查询范围内的未分配座位已经被其他并发请求抢占,直接重试发起下一次Patch请求即可。
这个方案相比你最初的ETag方案有几个明显优势:
- 单次请求完成全流程,比先查后改少一次网络往返,延迟降低50%左右
- 匹配判断和更新在服务端原子执行,同一时间只会有一个请求成功抢占到目标座位,不会出现多个客户端读到同一个座位再抢改的情况,冲突概率大幅降低
- 不需要客户端处理ETag的读取、传递、校验逻辑,代码量更少,出错概率更低
高并发场景的优化建议
如果遇到航班值机高峰、并发请求量极高的情况,可以加几个简单优化进一步降低冲突率:
- 不要每次都固定匹配排序第一的未分配座位,可以在匹配条件里增加随机范围,比如每次从序号前20的未分配座位里随机选一个匹配,避免所有请求扎堆抢同一个座位
- 重试时增加10~50ms的随机短退避,避免瞬时并发下的连续碰撞
- 如果航班座位量很大,可以提前把座位按区域拆分成不同的逻辑块,不同的请求轮询指向不同区块做匹配,进一步降低碰撞概率
不推荐的实现避坑
- 不要用存储过程实现占座逻辑:虽然存储过程也能在单分区内实现原子操作,但存储过程的RU开销更高,遇到分区限流时失败率远高于原生Patch,维护成本也更高
- 不要做跨分区的座位查询:跨分区查询的延迟和RU成本是单分区的数倍,完全没必要,座位分配本身就是单航班(单分区)内的操作
- 不要对座位列表做全量扫描再在客户端筛选:点查、带条件的单分区Patch的RU消耗是全量扫描的几十分之一,性能差距极大
内容的提问来源于stack exchange,提问作者lahsrah
相关产品推荐
相关产品推荐

