
1.4 Elasticsearch-CRUD 全实战:PUT/POST/GET/DELETE/_bulk
——从“写一条”到“写一亿条”的 production-ready 路径
- 一条文档的“身份证”:_id 与 _source
在 Elasticsearch(后文简称 ES)里,文档(document)等价于关系型数据库里的一行记录,却自带一张“身份证”:
_index/_type/_id(7.0 之后 _type 统一为 _doc)。
CRUD 的所有动作,本质上就是围绕这张身份证做原子操作。牢记下面两条铁律,可以避开 80% 的线上事故:
- 写操作默认 1 秒 refresh 后才可见;实时搜索需
?refresh=true,但会击穿 segment 缓存。 - 任何写操作都会先写 translog,再写内存 buffer,因此 bulk 再大也不会丢数据,但 translog 过大时会触发 flush,引起 IO 尖刺。
- PUT vs POST:幂等、指定 ID、自动生成 ID
场景 1:指定 ID 全量替换
PUT /shop/_doc/1001
{“title”:“ES 实战指南”,“price”:69.9}
特点:幂等、版本号 _version 自增、全字段覆盖(缺失字段会被删)。
场景 2:不指定 ID,让 ES 自动生成
POST /shop/_doc
{“title”:“ES 实战指南”,“price”:69.9}
返回 _id: u7wQk4sB7y5vfQqF8p4H,24 位 UUID;非幂等,同一行代码执行两次得到两条记录。
场景 3:PUT 带 _create,拒绝覆盖
PUT /shop/_doc/1001/_create
{“title”:“ES 实战指南”}
若 1001 已存在,返回 409 Conflict,防止“误刷”线上数据。
- GET:实时读取与“伪实时”
GET /shop/_doc/1001
返回结果包在 _source 里;实时可读,不走 searcher,直接查 translog + versionMap,因此 RT 保证 <1 ms。
只想看部分字段:
GET /shop/_doc/1001?_source_includes=title,price
或者只要存储字段(stored field):
GET /shop/_doc/1001?stored_fields=title
- DELETE:逻辑删除与“墓碑”
DELETE /shop/_doc/1001
ES 只在 segment 里写一条 .del 记录,真正的物理删除要等段合并(merge)。
批量删除用 _delete_by_query,会启动一个 scroll + bulk delete 任务,返回 taskId 可异步轮询。
线上大索引删除技巧:
- 先设置
index.refresh_interval=-1关闭自动 refresh, - 再跑 delete-by-query,
- 最后打开 refresh 并手动
_forcemerge?max_num_segments=1回收空间。
- UPDATE:脚本更新、upsert、并发控制
POST /shop/_update/1001
{
“script”: {
“source”: “ctx._source.price += params.delta”,
“params”: {“delta”: 10}
},
“upsert”: {“price”: 79.9}
}
脚本在 ES 端执行,避免“先 GET 再 PUT”的 race condition;
upsert 表示文档不存在时插入初始值。
并发版本控制:
- 内部版本号:
if_seq_no=12&if_primary_term=1,利用 sequence number 实现乐观锁; - 外部版本号:
?version=20&version_type=external,适合 CDC(MySQL binlog)场景,版本号由外部产生。
- _bulk:一次 RTT 干 10000 件事
bulk 是 ES 写入的“大杀器”,协议极简:
POST /_bulk
{ “index” : { “_index” : “shop”, “_id” : “2001” } }
{“title”:“Python 进阶”}
{ “update” : { “_index” : “shop”, “_id” : “1001” } }
{“doc”:{“price”:89.9}}
{ “delete” : { “_index” : “shop”, “_id” : “5001” } }
注意:
- 两行一条指令,不能换行少一行,否则整包 400;
- 末尾必须
\n结尾; - 单条失败不影响整包,返回体里逐条标记
{"error":...}; - 官方建议 5–15 MB/包,或 3000–5000 条/包;
- 开 gzip,设置
Content-Encoding: gzip,网络传输可省 70% 流量; - 客户端层做“拒绝重试”:429 只重试写,502/503 幂等操作才重试,避免 update 脚本重复执行。
- 生产级 bulk 模板(Java HighLevel Client 示例)
BulkRequest bulk = new BulkRequest();
bulk.setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL); // 等本次 bulk 可见
bulk.timeout(TimeValue.timeValueSeconds(30));
bulk.pipeline(“timestamp-pipeline”); // 预置 ingest pipeline 自动加 @timestamp
for (Sku sku : list) {
IndexRequest req = new IndexRequest(“shop”)
.id(sku.getId())
.source(JSON.toJSONString(sku), XContentType.JSON);
bulk.add(req);
}
BulkResponse resp = client.bulk(bulk, RequestOptions.DEFAULT);
if (resp.hasFailures()) {
for (BulkItemResponse item : resp.getItems()) {
if (item.isFailed()) {
log.error(“fail id:{} msg:{}”, item.getId(), item.getFailureMessage());
}
}
}
- 性能调优 checklist
- 关闭不必要的 _source 存储,省 30% 磁盘;
- 把 “string 仅排序/聚合” 字段设
"index":false,"doc_values":true,避免倒排; - 写前把 number_of_replicas 设 0,写完再改回,副本拷贝走 recovery 而非 bulk;
- 机械盘务必
index.translog.durability: async+index.translog.sync_interval: 30s,SSD 可默认; - 用 Elasticsearch 官方 Rally 压测,track 选
nyc_taxis,target-throughput 设 100k docs/s,观察 CPU iowait 与 JVM old 区; - 超过 3 亿条/天的业务,直接上 data stream + ILM,按大小 50 GB 滚索引,避免单 shard 过 TB 后 merge 炸盘。
- 常见报错速查表
| 现象 | 根因 | 解法 |
| 409 VersionConflict | 并发 update | 用 seq_no+primary_term 重试 |
| 429 EsRejectedEx | 写队列满 | client 退避 + 限流,或扩容 data 节点 |
| 400 illegal_argument | bulk 包格式错 | 检查换行、json 缺括号 |
| 403 cluster_block_exception | 磁盘 95% 只读 | 删冷索引、加磁盘、调 watermark |
| 504 gateway timeout | 负载均衡超时 | 把 client 超时调到 120 s,或直连 data 节点 |
- 小结
PUT 是“身份证写”,POST 是“匿名生”,GET 是“毫秒读”,DELETE 是“逻辑埋”,_bulk 是“万箭齐发”。
把 version/seq_no 当乐观锁、把 refresh_interval 当性能开关、把 translog 当数据生命线,
你就拥有了在千亿级文档里“指哪打哪”的 CRUD 能力。
更多技术文章见公众号: 大城市小农民

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



