4.3 批量写入与乐观并发控制


文档摘要

4.3 批量写入与乐观并发控制 本节摘要:批量接口(Bulk API)把成百上千条操作打进一个请求,靠换行分隔的报文与分片亲和的分组榨取吞吐;乐观并发控制(OCC)用写入响应里的序列号给并发更新装上护栏——谁手里的版本旧,谁的写入被拒。前者解决"搬得快",后者解决"改不乱",数据管道与双向同步的两块基石都在这一节。 当一千条文档同时进站 批量请求的报文长什么样 单条写入一次 HTTP 往返,一千条就是一千次——网络与线程开销把引擎压到每秒几百条。批量接口把操作流拼进一个请求体,格式是严格的两行一组、换行分隔: 每行必须是一个完整 JSON,行与行之间靠换行分隔,整个请求体末尾也要有换行。

4.3 批量写入与乐观并发控制

本节摘要:批量接口(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 的合规批量请求体
部分失败 从响应里找出被拒条目并说出退避重试的姿势
尺寸甜区 说出单请求的经验区间与起步调参方法
指纹护栏 演示一次序列号冲突被拒的更新及重读重试闭环
版本方案选型 按负载在序列号与外部版本间做选择并说明理由

易错点补充

  • 批量体末尾漏换行:最常见的四百报错来源,构造请求时逐行检查这一字符。
  • 想要"存在即报错"却用了 index 动作:该语义属于 create 动作,混用会把重复数据悄悄覆盖掉。
  • 重试不带退避:被拒的条目立刻原样重发,队列刚松一口气又被顶满,指数退避加抖动才是礼貌的重试。

两块基石就位

  • 批量靠报文格式与分片分组榨吞吐,单请求 5 到 15 MB 是经验甜区。
  • 批量响应必须逐条核对,429 拒绝靠退避重试消化,不靠蛮力。
  • 序列号指纹给读改写装护栏,外部版本给同步流保单调,按场景选型。

写入端齐活。下一章换一个视角:查询请求的旅程——它如何扇出、如何打分、如何把最相关的三条顶到首页。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U