4.3 批量写入与乐观并发控制 本节摘要:批量接口(Bulk API)把成百上千条操作打进一个请求,靠换行分隔的报文与分片亲和的分组榨取吞吐;乐观并发控制(OCC)用写入响应里的序列号给并发更新装上护栏——谁手里的版本旧,谁的写入被拒。前者解决"搬得快",后者解决"改不乱",数据管道与双向同步的两块基石都在这一节。 当一千条文档同时进站 批量请求的报文长什么样 单条写入一次 HTTP 往返,一千条就是一千次——网络与线程开销把引擎压到每秒几百条。批量接口把操作流拼进一个请求体,格式是严格的两行一组、换行分隔: 每行必须是一个完整 JSON,行与行之间靠换行分隔,整个请求体末尾也要有换行。
本节摘要:批量接口(Bulk API)把成百上千条操作打进一个请求,靠换行分隔的报文与分片亲和的分组榨取吞吐;乐观并发控制(OCC)用写入响应里的序列号给并发更新装上护栏——谁手里的版本旧,谁的写入被拒。前者解决"搬得快",后者解决"改不乱",数据管道与双向同步的两块基石都在这一节。
单条写入一次 HTTP 往返,一千条就是一千次——网络与线程开销把引擎压到每秒几百条。批量接口把操作流拼进一个请求体,格式是严格的两行一组、换行分隔:
POST _bulk { "index": { "_index": "tickets", "_id": "doc-1001" } } { "title": "用户申请退款,处理速度太慢", "status": "pending" } { "index": { "_index": "tickets", "_id": "doc-1002" } } { "title": "退款已到账", "status": "resolved" } { "delete": { "_index": "tickets", "_id": "doc-1003" } }
每行必须是一个完整 JSON,行与行之间靠换行分隔,整个请求体末尾也要有换行。操作类型不止 index:create(存在即失败)、update(可带脚本)、delete(只要一行)可以混排在同一请求里。引擎收到后按目标索引与分片分组,每组一车皮发往对应分片——这正是编组站的批量理货。

批量响应整体 200 不代表条条成功,必须逐条核对:
{ "took": 48, "errors": true, "items": [ { "index": { "_id": "doc-1001", "status": 201, "result": "created" } }, { "index": { "_id": "doc-1002", "status": 429, "error": { "type": "es_rejected_execution_exception" } } } ] }
errors 为 true 时,items 里标出每条的去向。429 是"分片忙不过来"的拒绝信号,正确姿势是退避重试(指数退避加抖动),不是加大并发硬闯——那只会把队列顶得更满。
背景:老工单库要整体迁入搜索引擎,总量约八十万条、单条平均 2 KB,迁移窗口两小时,期间老库仍在写入。操作:导出端按主键分页扫老库,攒到 5 MB 或两千条就发一个批量请求;请求带路由无关的自动 id 吗——不,工单有天然主键,指定 id;响应里 429 的条目单独收集,退避两百毫秒后并入下一批重试;每完成十万条记录一次进度位点,中断可续。结果:约五十分钟迁完,中途出现三批拒绝共六千余条,全部重试成功;迁移结束后用计数接口核对两边文档数一致,再按时间戳补扫增量。解读:吞吐来自批量尺寸与背压自律的组合——尺寸吃满网络与分片的批量收益,遇到拒绝就收手让引擎消化,比蛮力并发稳得多。变式:窗口极短时改双写(老库与新索引同时写),存量慢慢搬;数据有 deleted 标记时,扫库跳过已删行,免得搬一堆墓碑进来。
4.1 的响应里有一对字段:_seq_no 与 _primary_term,它们组成文档的版本指纹。更新请求带上"我读到的指纹",引擎只在指纹仍是最新时受理:
GET tickets/_doc/doc-1001 # 取到 _seq_no: 12 _primary_term: 3
POST tickets/_update/doc-1001?if_seq_no=12&if_primary_term=3 { "doc": { "status": "resolved" } }
# 若期间另一更新已把指纹推进到13 这条请求返回409版本冲突 拒绝执行
这就是乐观并发:不加锁,先干活,提交时对指纹,对不上就失败重来。读-改-写的循环是:读文档拿指纹、本地算好修改、带指纹提交、冲突则重读再来。上游数据库本身有版本号时,还有第二条路——外部版本:写入时带 version 与 external 版本类型,引擎只接受比已存版本号更大的写入,天然屏蔽乱序与回放。
| 场景 | 要不要 OCC | 原因 |
|---|---|---|
| 单服务顺序写同一文档 | 不必 | 无并发方 |
| 两个服务都会改同一工单 | 要 | 最后写入胜出会丢语义 |
| 数据库同步到搜索引擎 | 要(外部版本) | 同步任务重跑与乱序 arrival 常见 |
| 纯日志追加 | 不必 | 自动 id 天然无冲突 |
⚠️ 常见坑:给批量请求里的每条更新都带严格指纹,在高频同步流里冲突率飙升、吞吐雪崩。同步类负载优先用外部版本(天然单调),交互类负载才用序列号指纹加重试。
💡 关键直觉:乐观并发的成本结构与锁相反——冲突少时几乎零开销,冲突多时全是重试。选它之前先估计冲突概率,正如编组站不会给每节车厢都派专人押运。
| 考核点 | 达标标准 |
|---|---|
| 报文格式 | 手写一个含 index 与 delete 的合规批量请求体 |
| 部分失败 | 从响应里找出被拒条目并说出退避重试的姿势 |
| 尺寸甜区 | 说出单请求的经验区间与起步调参方法 |
| 指纹护栏 | 演示一次序列号冲突被拒的更新及重读重试闭环 |
| 版本方案选型 | 按负载在序列号与外部版本间做选择并说明理由 |
写入端齐活。下一章换一个视角:查询请求的旅程——它如何扇出、如何打分、如何把最相关的三条顶到首页。