2100 字
约 7 分钟
18
S13-A25 · 写后立读:为什么"刚改完就搜不到"

一个后台"改完商品立刻刷新前台搜索",用户看到"还是旧的"。查下来不是缓存——是 Elasticsearch 的"近实时":写入后要等 refresh(默认 1 秒)才能被搜到。而那次"改完立刻查",恰好落在"1 秒"里。

ES 的"写后立读"是个经典问题,而它的解法有三档:等待 refresh、用 GET 而非 search、改 refresh 策略。这三档的"选择标准"不是"哪个最快",而是"哪一处需要实时"——因为"全局实时"的代价(吞吐)会由"所有写入"承担.
三档的代价完全不同。

1. 三种"读"的可见性

读方式 是否受 refresh 影响
GET /index/_doc/id 否(实时,读 translog)
search / msearch 是(要等 refresh)
count / 聚合 是

第一项是"关键区别":按 ID 取文档是"实时的"第一项"实时"的原因很具体:GET 直接读"translog"(写入日志)——而"搜索"读的是"已经刷新成段"的数据;两者读的"不是同一份存储".
(它从 translog 读);而"搜索"要等"段"被刷新.

"按 ID 读"这一条,恰好是"改完立刻显示"的最省解法:

// 三段"改写后"的读法(同一商品)
1 详情页(改完立刻看):GET /products/_doc/123        → 实时
2 列表页(1 秒后可搜):search products where ...      → 近实时
3 兜底(读不到时):读主库 / 读缓存                    → 一定实时
// 判据:需要"立刻看到"的那一处,不要用 search

2. "写后立读"的三种解法

解法 代价
写完 refresh=wait_for 写入变慢(等 1 次刷新)
前端"延迟 1 秒再查" 体验差、不可靠
关键字段"回主库读" 多一次查询(但实时)

第一项的"代价"很实在:每次这样的写要"等一次刷新"——默认 1 秒,于是"写接口"的 P99 可能变 1 秒;高写入量时"更明显".
第一项是"精确"的:?refresh=wait_for 让"这次写"等到"可搜"再返回——于是"写完之后搜得到".

第三项是"最稳"的:"改完立刻要显示"的那一处,直接读主库/DB——而"搜索列表"仍走 ES(容 1 秒延迟).

"批量导入"是"手动 refresh"的典型场景,而它常被漏:

// 三步"批量导入"(避免"边导边刷")
1 导入期:refresh_interval=-1(关闭自动刷新)
2 导入:_bulk 分批(每批 1000–5000)
3 收尾:POST /my_index/_refresh(一次刷新)
// 判据:导入完成后"立刻可搜",而导入期间"写入吞吐最高"

3. "refresh 策略"的三档

策略 影响
默认 1s 写吞吐好,延迟 1s
refresh=false(手动刷) 写快,读要"自己刷"
refresh=true(每次写刷) 实时,但"段"激增、变慢

第三项是"最贵"的:**每次写都刷新会"产生大量小段"**第三项的"连锁"是"段合并压力":每秒 1000 次写 → 每秒 1000 个小段——而合并(merge)会吃 IO 与 CPU,于是"搜索延迟"上升;这就是"实时换吞吐".
——于是"合并压力"上升,搜索变慢.

"写入成功的判定"要区分"已写入"与"已可搜",否则验收标准会错:

// 两种"成功"(同一写请求)
1 写入成功:主分片写入 + 副本确认(_shards.successful = 2)
2 可搜:段被刷新(refresh)
// 判据:接口返回 201 只代表 1;要 2 就加 refresh=wait_for

4. "为什么有的查询看起来实时"的三种情形

情形 原因
按 ID 查 走 GET(实时)
等了 1 秒后查 已刷新
数据量小、写入少 刷新几乎"立刻"

第三项的"陷阱"在于"验证方式":你在测试环境"改完立刻搜",搜到了,于是"以为没问题"——**而线上"1 秒"的窗口,恰好是"用户点刷新"的时机".
第三项是"假象":在"测试环境"里数据少、写入稀疏——于是"看起来实时",上线后"1 秒延迟"就显形.

"refresh=wait_for"要"只在需要的接口上开",而不是"全局":

// 一个"需要实时"的写入(改标题后立刻要能搜到)
PUT /products/_doc/123?refresh=wait_for
{ "title": "新标题", "updated_at": "2026-11-11T00:00:00Z" }
// 对照:批量同步任务用 refresh=false(或默认),末尾统一刷
// 判据:每 1000 次写里,"等刷新"的不超过"用户触发的那几次"

5. "写后立读"的三种验收

验收 做法
写入后立即可 GET 是(应通过)
写入后立即可 search 否(正常)
refresh=wait_for 后可 search 是

第二项"应该"是"搜不到"——如果它总是能搜到,说明"refresh 被设成了 true"(代价被隐藏).

"偶发 1 秒"要"能证明",否则会被当成"随机故障":

// 三步"证明是 refresh"
1 同一文档,写后 t=0 用 search 查:无;t=1.2s 再查:有
2 用 GET 在 t=0 查:有(证明"写入成功,只是没刷新")
3 看索引的 refresh_interval 设置(默认 1s)
// 判据:三条一致 → 就是 refresh,不必再查"数据丢了"

6. 排错:五种"写后读不到"

现象 成因 处理方向
改完刷新是旧的 未刷新 wait_for 或读主库
偶发(1 秒后就好) refresh 延迟 正常/加 wait_for
测试环境"实时" 数据少 别以此为准
批量导入后搜不到 手动 refresh 未调 导入末尾 _refresh
搜索变慢 refresh=true 改回 1s

7. 边界:什么情况"不必管写后立读"

场景 为什么 做法
搜索是"异步展示"(列表页) 容 1 秒 不必
只做"分析/报表"(T+1) 无实时要求 不必
写入极少(每天几次) 撞不上 不必
关键路径已"读主库" 已实时 不必

最后一条与"体验"有关的边界:"读主库"这档也可以是"只读那一处":"详情页"读主库、"列表页"走 ES——于是"实时性"只花在"最需要的地方",而"列表的 1 秒"用户无感.
判断口径一句话:"用户改完之后,会不会"立刻再看搜索结果""。会(后台改价、改标题、上架),就要"那一处读主库"或"refresh=wait_for";不会(数据同步、报表),1 秒延迟无感——而"为了省事把 refresh 设成 true"是最常见的"用吞吐换实时".

再补一个"运维"视角:"段变多"是"用实时换吞吐"的账单——当 _cat/segments 里"每分片段数"到几百,搜索就会明显变慢,而"原因"往往写在一个月前的某次"把 refresh 设成 true"的提交里。所以"实时性"应当"按接口"配置,而不是"按集群"配置:"后台改价要实时"和"数据同步不急"是两件事,不该共用一个 refresh 策略.

最后一个"选择"建议:"写后立读"的解法优先级是"读主库 → GET → wait_for → refresh=true"——因为前两档"只影响那一处",后两档"影响所有写入"。而"把最贵的那档当默认",几乎总会在"写入量上来"之后被反噬:搜索变慢、段暴涨、合并吃满 IO——而那时候"改回去"要"重启索引策略 + 等段合并".

而"改回去"的成本,永远高于"一开始就只开在那一处"。

所以"按接口配实时",是这件事唯一省钱的顺序。

8. 自测

  1. GET 与 search 在可见性上有什么差别?为什么 GET 是实时的?
  2. "写后立读"的三种解法各有什么代价?哪一种"最稳"?
  3. refresh=true 为什么"最贵"?它带来什么连锁影响?
  4. "测试环境看起来实时"是什么原因?为什么上线后会显形?
  5. "写入后立即可 search"如果总成立,说明什么?
  6. 判断"是否需要处理写后立读"的那个问题是什么?
S13-A25 · 写后立读:为什么"刚改完就搜不到"
https://www.clxhxhhr.top/posts/6683/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。