Elasticsearch 更新和删除操作的底层原理是什么?
Elasticsearch 底层基于 Lucene,而 Lucene 的段(segment)是不可变的。这意味着已写入段的文档无法原地修改或删除。Elasticsearch 的更新和删除操作都建立在这一约束之上,通过标记删除 + 重新索引的方式实现,再由段合并完成物理清理。
更新操作:标记删除 + 重新索引
Elasticsearch 的更新并不是原地修改文档。当你更新一个文档时,实际发生的是两步操作:
- 旧文档在
.del文件中被标记为 deleted - 新文档被索引到一个新的段中
也就是说,更新 = 删除旧版本 + 插入新版本。这是由倒排索引的不可变性决定的——段一旦写入就无法修改,只能追加。
jsonPUT /products/_doc/1 { "name": "MacBook Pro", "price": 14999, "updated_at": "2025-01-15" }
上述请求如果文档 ID=1 已存在,旧文档会被标记删除,新文档写入新段。如果不指定 ID,则直接作为新文档插入。
部分更新(Partial Update)
全量替换需要发送完整文档,网络开销大。部分更新通过 _update API 只修改指定字段,但底层仍然是标记删除 + 重新索引——只是服务端帮你完成了合并旧文档和新字段的步骤:
jsonPOST /products/_update/1 { "doc": { "price": 12999 } }
脚本更新
对于需要动态计算的场景,可以用脚本更新:
jsonPOST /products/_update/1 { "script": { "source": "ctx._source.price += params.delta", "params": { "delta": 500 } } }
upsert 操作
当不确定文档是否存在时,upsert 可以在文档不存在时插入、存在时更新:
jsonPOST /products/_update/1 { "doc": { "price": 12999 }, "upsert": { "name": "MacBook Pro", "price": 12999 } }
删除操作:逻辑删除与段合并清理
删除文档时,Elasticsearch 不会立即从磁盘移除数据。而是在 .del 文件中标记该文档为 deleted 状态。被标记的文档仍然存在于段中,但查询时会被过滤掉。
jsonDELETE /products/_doc/1
物理删除何时发生?
物理删除发生在段合并(segment merge)过程中。Lucene 后台会定期将多个小段合并为大段,此时被标记为 deleted 的文档不会被写入新段,从而实现真正的磁盘空间回收。
你也可以手动触发合并清理:
jsonPOST /products/_forcemerge?only_expunge_deletes=true
only_expunge_deletes=true 表示只合并含有删除文档的段,不影响无删除标记的段。
按条件批量删除
对于需要按查询条件删除的场景,使用 delete_by_query:
jsonPOST /products/_delete_by_query { "query": { "range": { "price": { "lte": 100 } } } }
注意:delete_by_query 是先扫描再逐个标记删除,大数量下耗时长,建议在低峰期执行并设置 wait_for_completion=false 异步执行。
版本控制与乐观并发
_version 字段
每个文档都有一个 _version 字段,每次写操作(index、update、delete)都会使版本号递增。这用于防止旧版本覆盖新版本——如果一个更新请求基于的版本号已过期,操作会被拒绝。
乐观并发控制
Elasticsearch 使用 if_seq_no 和 if_primary_term 实现乐观并发控制(OCC)。在读取文档时获取当前的 seq_no 和 primary_term,更新时带上这两个值,如果文档已被其他操作修改(seq_no 已变),则返回 409 冲突:
jsonPUT /products/_doc/1?if_seq_no=5&if_primary_term=1 { "name": "MacBook Pro", "price": 13999 }
如果不做并发控制,两个请求同时更新同一文档,后到的请求会覆盖先到的结果——这在电商库存扣减等场景下是严重问题。
近实时搜索与 refresh 机制
文档写入后并不是立即可搜索。Elasticsearch 的写入流程是:
- 文档先写入内存缓冲区(index buffer)
- 同时写入 translog(事务日志,保证持久性)
- 每隔
refresh_interval(默认 1s)执行一次 refresh,将内存缓冲区的数据写入新段,文档变为可搜索
这意味着更新和删除操作也有近一秒的延迟才对搜索可见。生产环境中,可以适当调大 refresh_interval(如 30s)来提升写入吞吐量,代价是搜索可见延迟增加。
性能优化要点
更新场景:
- 优先使用部分更新而非全量替换,减少网络传输和 _source 重写开销
- 高频更新使用 Bulk API 批量提交
- 避免在热索引上频繁单条更新,考虑异步队列聚合后批量写入
删除场景:
- 大批量删除用
delete_by_query而非逐条 DELETE - 删除后若段膨胀明显,执行
force_merge回收空间(只对只读索引执行,否则可能产生超大段) - 删除大量数据后关注磁盘水位,段合并需要额外磁盘空间
通用建议:
- 监控
GET /_nodes/stats/indexing中的索引吞吐和删除计数 - 调整
index.merge.policy控制段合并策略和频率 - 更新和删除都会产生 translog 和段碎片,定期评估索引是否需要 reindex
面试中回答这个问题,核心要讲清楚三点:段不可变导致更新是删除+插入、删除是逻辑标记物理清理靠段合并、并发控制靠 seq_no/primary_term 实现乐观锁。理解这三层,就能应对追问。