首页/数据/NoSQL 速查/_seq_no 与 _primary_term 乐观并发控制
Elasticsearch89 条

_seq_no 与 _primary_term 乐观并发控制

文档的 _seq_no 随每次写入递增、_primary_term 标识主分片任期;写入或更新时带上 if_seq_no 与 if_primary_term,可避免覆盖别人刚写的版本。

NoSQL 速查数据更新于 2026-10-13

_seq_no 与 _primary_term 是什么

每篇文档都带两个元数据:_seq_no 是写入序号,随每次写操作递增;_primary_term 是主分片任期号,主分片切换时会变化。写请求带上 if_seq_no 与 if_primary_term 后就变成带条件的更新,只有版本仍然匹配才执行,用来避免覆盖别人刚写入的内容。

乐观锁的工作方式

这是乐观并发控制:先读文档拿到版本号,提交时带着版本号,冲突返回 409 后重新读取再重试,而不是提前把文档锁住。它适合读多写少、冲突概率低的场景;高并发的计数类更新应改用分片端原子执行的脚本(在 _update 里对字段做自增),而不是先读后写。

基本信息

读取版本GET /<index>/_doc/<id> 响应里带 _seq_no 与 _primary_term
条件写PUT /<index>/_doc/<id>?if_seq_no=N&if_primary_term=M
部分更新POST /<index>/_update/<id>?if_seq_no=N&if_primary_term=M
冲突结果版本不匹配返回 409 version_conflict_engine_exception
相关字段_version 是文档内部版本号,语义与 seq_no 不同

示例

示例
# 1 先读取当前版本号
GET /my-index/_doc/1

# 2 带条件写入:版本不匹配返回 409
PUT /my-index/_doc/1?if_seq_no=5&if_primary_term=2
{ "title": "updated with conflict check" }

# 3 部分更新同样支持条件参数
POST /my-index/_update/1?if_seq_no=6&if_primary_term=2
{ "doc": { "views": 15 } }

# 4 计数场景改用分片端原子累加,不需要先读版本
POST /my-index/_update/1
{ "script": { "source": "ctx._source.views += params.n", "params": { "n": 1 } } }

典型使用场景

  • 并发编辑:同一文档可能被多个入口修改时避免互相覆盖
  • 配置类文档更新:先读后改,提交时校验版本
  • 状态流转:确保状态按预期版本推进,冲突时重试或报错
  • 计数累加:用原子脚本而不是条件写
  • 批量修复:修复脚本用条件写,避免覆盖同时进行的业务写入

字段与参数读法

字段或参数含义注意
_seq_no文档写入序号,随每次写递增条件写与顺序判断的依据
_primary_term主分片任期号主分片切换后变化,防止旧主分片上的写生效
if_seq_no要求匹配的写入序号必须与 if_primary_term 成对使用
409 冲突条件不满足属于正常结果,应重读后重试
_version文档内部版本号与 seq_no 语义不同,不要混用
重试逻辑里不能在失败后直接复用旧版本号,每次重试都要重新读取最新版本,否则会一直冲突直到放弃。
条件写只保证"这次写入基于我读到的版本",并不能让两个并发写自动合并;需要严格串行时还得在业务层排队或改用原子脚本。

易错点

  • 重试时复用旧版本号会一直失败,每次重试都要重新读取最新版本
  • if_seq_no 与 if_primary_term 必须成对出现,只给其中一个会因参数不合法直接报错
  • 把 409 当成系统故障去告警会制造噪声,它是并发控制的正常返回值,应单独统计冲突率
  • 高并发计数场景用先读后写的条件更新会大量冲突,应改用分片端原子脚本

数据来源与参考

  • Elasticsearch Guide: Optimistic concurrency control
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌