_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 语义不同,不要混用 |
重试逻辑里不能在失败后直接复用旧版本号,每次重试都要重新读取最新版本,否则会一直冲突直到放弃。
条件写只保证"这次写入基于我读到的版本",并不能让两个并发写自动合并;需要严格串行时还得在业务层排队或改用原子脚本。