Elasticsearch 数据更新流程完全解析:从 update 到版本控制的 8 步深度揭秘
作者:IT之一小佬
发布日期:2025年10月22日
阅读时间:25分钟
适合人群:Elasticsearch 开发者、运维、SRE、数据平台负责人
🌟 引言:Elasticsearch 的“更新”其实是“重建”
当你执行:
curl -X POST "localhost:9200/logs/_update/1" -H 'Content-Type: application/json' -d'
{
"doc": { "status": "processed" }
}'
你可能以为 Elasticsearch 在原地修改了文档。但事实是:
🔥 Elasticsearch 并不支持真正的“原地更新”!
它的更新机制本质是:删除旧文档 + 写入新文档,并保证原子性与版本控制。
本教程将带你深入 Elasticsearch 更新流程的 8 个核心步骤,涵盖普通更新、脚本更新、乐观并发控制等高级特性。
一、更新流程全景图
[客户端]
↓ (1. 接收 update 请求)
[协调节点]
↓ (2. 路由到主分片)
[主分片]
↓ (3. 获取旧文档 _source)
↓ (4. 应用变更(doc 或 script))
↓ (5. 标记旧文档为删除 + 写入新文档)
↓ (6. 同步复制到副本)
[协调节点]
↓ (7. 返回结果)
[后台]
↓ (8. 段合并时物理删除)
二、Step 1 & 2:请求接收与路由
流程
- 客户端发送
_update请求 - 任意节点作为协调节点接收
- 根据
_id计算路由,确定目标主分片shard_num = hash(_id) % primary_shards
✅ 与写入流程相同,
_update请求必须先到达主分片。
三、Step 3:获取旧文档(Get Source)
关键操作
- 主分片从 Lucene 存储中读取旧文档的
_source _source是原始 JSON 字符串,必须启用(默认启用)
配置说明
PUT /logs
{
"mappings": {
"_source": { "enabled": true } // 必须为 true 才能 update
}
}
❌ 如果
_source: false,则无法使用_updateAPI!
四、Step 4:应用变更(Apply Changes)
Elasticsearch 支持两种更新方式:
方式 1:doc 更新(部分字段更新)
POST /logs/_update/1
{
"doc": { "status": "completed", "duration": 120 }
}
- 将
doc中的字段合并到旧文档 - 类似于
Object.assign(oldDoc, doc) - 简单高效,推荐用于常规更新
方式 2:script 更新(复杂逻辑)
POST /logs/_update/1
{
"script": {
"source": "ctx._source.retry_count += 1; if (ctx._source.retry_count > 3) { ctx.op = 'delete' }",
"lang": "painless"
}
}
- 使用 Painless 脚本执行复杂逻辑
- 可访问
ctx._source(旧数据) - 可修改
ctx.op:"index"(默认):更新"delete":删除该文档"noop":无操作
⚠️ 警告:脚本性能较低,避免复杂循环。
五、Step 5:删除旧文档 + 写入新文档
这是更新的核心原子操作。
具体步骤
- 标记旧文档为“已删除”
- 在 Lucene 中,旧文档被加入 “已删除文档列表”(.liv 文件)
- 并非立即物理删除
- 写入新文档
- 新文档进入 In-Memory Buffer
- 写入 Translog
- 版本号
_version自增
版本控制(Versioning)
{
"_index": "logs",
"_id": "1",
"_version": 2,
"result": "updated"
}
_version从 1 → 2- 用于乐观并发控制
六、Step 6:同步复制到副本
与写入流程一致:
- 主分片将“删除+写入”操作发送给所有副本分片
- 副本分片执行相同操作:
- 标记旧文档删除
- 写入新文档到 Buffer 和 Translog
- 副本返回 ACK
- 主分片确认后向协调节点返回成功
✅ 确保主副本数据一致性。
七、Step 7:返回结果
成功响应
{
"_index": "logs",
"_id": "1",
"_version": 2,
"result": "updated", // or "noop", "deleted"
"shards": { "total": 3, "successful": 2, "failed": 0 }
}
特殊情况
"result": "noop":脚本中设置了ctx.op = 'noop'"result": "deleted":脚本中设置了ctx.op = 'delete'
八、Step 8:后台段合并(Merge)— 物理删除
何时发生?
- 当 Lucene 进行 段合并(Segment Merge) 时
- 合并不会包含已被标记删除的文档
效果
- 释放磁盘空间
- 提升查询性能(减少无效文档扫描)
🔄 这是一个异步过程,不是实时的。
九、高级更新模式
1. 乐观并发控制(Optimistic Locking)
防止并发更新覆盖问题。
使用 _version
# 第一次读取
GET /logs/_doc/1 → "_version": 1
# 更新时指定版本
POST /logs/_update/1?if_seq_no=1&if_primary_term=1
{ "doc": { "status": "done" } }
- 如果期间有其他更新,
seq_no已变为 2,则本次更新失败(409 Conflict)
使用外部版本
PUT /logs/_update/1?version=5&version_type=external
{ "doc": { "status": "done" } }
- 使用业务系统的版本号
2. Upsert(Update or Insert)
如果文档不存在,则创建。
POST /logs/_update/1
{
"doc": { "status": "created" },
"doc_as_upsert": true
}
- 避免先
GET再判断是否存在
3. 批量更新(Bulk Update)
POST /_bulk
{ "update" : { "_index" : "logs", "_id" : "1" } }
{ "doc" : { "status" : "processed" } }
{ "update" : { "_index" : "logs", "_id" : "2" } }
{ "doc" : { "status" : "failed" } }
- 减少网络开销,提升吞吐
十、性能影响与优化
1. 更新的性能成本
| 操作 | 相对成本 |
|---|---|
index(新建) | 1x |
_update | ~2-3x (需读取 + 删除 + 写入) |
delete + index | ~2x |
💡 建议:频繁更新的场景,考虑是否可用
counter字段替代。
2. 减少更新频率
- 使用 批量更新(Bulk)
- 合并多次小更新为一次大更新
3. 避免脚本更新
- 脚本解释执行,性能较差
- 复杂逻辑尽量在应用层处理
十一、常见错误与排查
错误 1:DocumentMissingException
- 原因:文档不存在且未设置
doc_as_upsert - 解决:使用
upsert或先创建文档
错误 2:VersionConflictEngineException (409)
- 原因:版本冲突(乐观锁失败)
- 解决:
- 重试机制
- 使用
retry_on_conflict=3
POST /logs/_update/1?retry_on_conflict=3
{ "doc": { "count": 1 } }
错误 3:脚本编译失败
- 原因:Painless 语法错误
- 解决:使用 Kibana Dev Tools 调试脚本
🔚 结语
你已经完整掌握了 Elasticsearch 数据更新流程的 8 个步骤:
- 接收 → 2. 路由 → 3. 获取旧文档 → 4. 应用变更
- 删除+写入 → 6. 复制 → 7. 响应 → 8. 后台合并
关键要点:
- ✅ 更新 = 删除旧 + 写入新(逻辑删除)
- ✅
_source必须启用 - ✅ 使用
if_seq_no/if_primary_term实现乐观锁 - ✅
upsert和bulk提升效率
理解这个流程,你就能:
- 正确设计更新逻辑
- 避免并发更新问题
- 优化更新性能
现在,检查你的代码:
- 是否在频繁调用单条
_update?→ 改用bulk - 是否有并发更新风险?→ 加上
retry_on_conflict - 是否在用脚本做简单更新?→ 改用
doc更新
立即优化,让你的更新操作更安全、更高效!
💬 评论区互动:你在项目中如何处理高并发更新?遇到过哪些坑?欢迎分享你的实战经验!
340

被折叠的 条评论
为什么被折叠?



