第六阶段 52 · 并发控制与乐观锁(并发读写下 ES 怎么保证一致)

52 · 并发控制与乐观锁(并发读写下 ES 怎么保证一致)

阶段:第六阶段 / 进阶专题

ES:_seq_no / _primary_termversionretry_on_conflict、NRT | PostgreSQL:MVCC、行锁、事务、乐观锁


1. 概念:ES 没有事务和行锁,用"乐观并发控制"

从 PG 转过来最大的心智差异:

维度 PostgreSQL Elasticsearch
多语句事务 ✅ BEGIN/COMMIT ❌ 没有跨文档事务
原子性粒度 行 + 事务 单篇文档(doc 级原子)
并发控制 MVCC + 行锁(悲观为主) 乐观并发控制(OCC),冲突让应用重试
读写互斥 读一般不阻塞写(MVCC) 读写互不加锁(段不可变)
可见性 提交即可见(读己所写) 近实时,写完约 1s 后可搜

核心:ES 不加锁等待,而是"乐观地写,冲突了报错让你重试"。


2. 写并发:_seq_no + _primary_term 乐观锁

每篇文档都有两个并发控制元数据:

  • _seq_no:该分片上写操作的序号(每次写自增)。
  • _primary_term:主分片的"任期",主分片切换时递增。

流程("比对再写"):

复制代码
# 1) 先读,拿到当前 _seq_no / _primary_term
GET orders_idx/_doc/order-1
# => "_seq_no": 12, "_primary_term": 3

# 2) 带着这两个值去写:只有没被别人改过才成功
PUT orders_idx/_doc/order-1?if_seq_no=12&if_primary_term=3
{ "order_no": "SO-1", "status": "PAID" }

若期间别人改过(_seq_no 变了),返回 409 version_conflict_engine_exception

本次写失败------由应用决定重读后重试,而不是阻塞等待。

旧写法 ?version=N&version_type=internal 已被 _seq_no/_primary_term 取代,新代码用后者。


3. _update 自动重试 & 批量冲突策略

3.1 单文档:retry_on_conflict

_update(读-改-写一体)可让 ES 自动重读重试,省去手写循环:

复制代码
POST orders_idx/_update/order-1?retry_on_conflict=3
{ "script": { "source": "ctx._source.view_count += 1" } }

冲突时最多自动重试 3 次,适合计数器这类高并发自增。

3.2 批量:conflicts=proceed

update_by_query / delete_by_query 遇到版本冲突默认中止

想"跳过冲突项继续跑完"用 conflicts=proceed

复制代码
POST orders_idx/_update_by_query?conflicts=proceed
{ "script": {...}, "query": {...} }

4. external version(用外部系统的版本号)

数据权威在别处(如 PG 的 updated_at/递增版本)时,用外部版本,让 ES 只接受"更新的版本":

复制代码
PUT orders_idx/_doc/order-1?version=170000&version_type=external
{ ... }

只有传入 version 大于 当前值才写入------天然实现"旧数据不覆盖新数据",

非常适合 DB → ES 同步(乱序消息也不会用旧值盖新值)。


5. 读并发:读不阻塞写、近实时可见

  • 段不可变:Lucene 写入生成新段,旧段继续服务查询;读写互不加锁(类似 MVCC)。
  • 近实时(NRT) :写入先进内存 buffer + translog,默认每 refresh_interval(~1s)
    才 refresh 成可搜索段。所以"写完立刻查不到"是正常的(第 08 篇)。
  • 持久性:translog 保证宕机不丢已确认的写;flush 把段落盘。
  • 遍历期间一致 :一次 search 看当次段快照;长时间分页/导出用 PIT (第 18/40 篇)
    锁定快照,避免翻页途中被并发写"串动"。

6. Spring Boot 实现

java 复制代码
@Component
public class Doc52Concurrency {

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    /** 乐观锁更新:带 if_seq_no/if_primary_term,冲突则由调用方重试 */
    public boolean updateWithOcc(String index, String id, Map<String, Object> doc,
                                 long seqNo, long primaryTerm) throws IOException {
        try {
            elasticsearchClient.index(i -> i
                    .index(index)
                    .id(id)
                    .ifSeqNo(seqNo)
                    .ifPrimaryTerm(primaryTerm)
                    .document(doc));
            return true;
        } catch (ElasticsearchException e) {
            if (e.status() == 409) {         // version_conflict_engine_exception
                log.warn("并发冲突,需重读重试 id={}", id);
                return false;                // 交给上层重读最新 _seq_no 后重试
            }
            throw e;
        }
    }

    /** 读时拿到 _seq_no / _primary_term,供后续乐观锁写入使用 */
    public GetResult<Map> getForUpdate(String index, String id) throws IOException {
        GetResponse<Map> resp = elasticsearchClient.get(g -> g.index(index).id(id), Map.class);
        // resp.seqNo()、resp.primaryTerm()、resp.source()
        return new GetResult<>(resp.source(), resp.seqNo(), resp.primaryTerm());
    }

    public record GetResult<T>(T source, Long seqNo, Long primaryTerm) {}
}

import:co.elastic.clients.elasticsearch._types.ElasticsearchException

co.elastic.clients.elasticsearch.core.GetResponse。计数器类高并发优先用 _update + retry_on_conflict


7. 坑与最佳实践

  1. 没有跨文档事务:需要多文档"要么全成"时,在应用层用幂等 + 补偿,别指望 ES 事务。
  2. 默认 last-write-wins:不带版本号并发写,后到的覆盖前者;要防覆盖必须用 OCC 或 external version。
  3. 409 是正常信号不是故障:说明有并发,重读最新版本再写即可(有限次退避重试)。
  4. 计数器/累加用 _update + retry_on_conflict,别自己读-改-写留下竞态窗口。
  5. DB→ES 同步用 external version:以源库版本为准,天然防乱序旧值覆盖新值。
  6. 读己所写要注意 NRT :刚写完就要查到,测试可 refresh=true(生产别滥用,伤性能)。
  7. 长导出用 PIT:并发写不影响你这次遍历的一致视图(第 40 篇)。

相关

  • 近实时可见性:08-刷新与可见性(refresh与NRT).md
  • 批量更新/删除与冲突:32-update_by_query-delete_by_query.md
  • 一致性快照遍历(PIT):40-深分页与性能.md

相关推荐
Elastic 中国社区官方博客5 小时前
搜索倍增器:推动收入、生产力和 AI 实现规模化
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
wjkjpcba5 小时前
机器人控制板PCBA怎么加工?从SMT贴片到BGA焊接解析机器人电子制造工艺
大数据·人工智能
Capricorn19887 小时前
解决全网抄 Karpathy 导致的 LLM Wiki 污染?基于知芽 Notebook Skill 的 raw/ 笔记法排障指南
大数据·论文阅读·人工智能·笔记·论文笔记
临沂GEO8 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
人民新视野8 小时前
党建引领促交流 互学互鉴共提升 —— 赴启信创格(北京)营销科技有限公司交流学习活动总结
大数据·人工智能·科技
Elasticsearch8 小时前
Microsoft Foundry 的 AI agent 可观测性:两个环境变量,无需采集器
elasticsearch
亦皓ai9 小时前
AI时代后端工程(二):AI把代码写得越来越快,我却越来越不敢让它直接开工了
人工智能·算法·机器学习·搜索引擎·transformer
中科高级技工学校10 小时前
乌鲁木齐中医康复学习经历分享
大数据·学习
ManageEngineITSM10 小时前
什么是CMDB?配置管理数据库的定义、作用与建设方法一文讲清
大数据·数据库·人工智能·资产管理·变更管理