Elasticsearch 文档版本控制实验手册

核心思想:在分布式系统中,并发写操作必须有控制机制。Elasticsearch 提供了两种互补方案:

  • 原子脚本更新(用于计数、累加等简单操作)
  • 序列号乐观锁 (用于全量文档替换,如表单编辑)
    本实验将带你亲手验证两者的适用边界。

▶ 实验目标

  1. 创建用于实验的索引。
  2. 插入初始数据。
  3. 对比演示
    • 无并发控制时的数据丢失问题;
    • 计数类操作的正确实现方式(update + 脚本);
    • 复杂文档编辑的正确实现方式(PUT + if_seq_no)。
  1. 理解何时该用哪种机制。

3.1 创建索引(简化环境)

bash 复制代码
PUT /demo_index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "refresh_interval": "1s"
  },
  "mappings": {
    "properties": {
      "design": { "type": "keyword" },
      "votes": { "type": "integer" },
      "title": { "type": "text" },
      "bio": { "type": "text" }
    }
  }
}

📌 说明:单分片 + 无副本,排除分片/副本干扰,专注并发逻辑。


3.2 测试数据

文档 ID 类型 内容示例
1 T 恤投票 { "design": "Galaxy Print" }
2 用户资料 { "title": "张三", "bio": "学生" }

3.3 插入初始数据

json 复制代码
// 投票文档(votes 字段可不存在)
PUT /demo_index/_doc/1
{ "design": "Galaxy Print" }

// 用户资料
PUT /demo_index/_doc/2
{ "title": "张三", "bio": "学生" }

3.4 问题演示:无并发控制的危险操作

场景:两人同时给 T 恤投票(错误做法)

json 复制代码
// 两人同时读取(都看到 votes 不存在或为 null)
GET /demo_index/_doc/1

// 各自计算 votes = 1,并全量写回
PUT /demo_index/_doc/1
{ "design": "Galaxy Print", "votes": 1 }

结果 :最终 votes = 1(应为 2),一次投票丢失

💥 结论:直接"先读再全量写"在并发下必然出错!

⚠️ 注意:此演示仅用于说明问题本质绝非推荐做法


3.5 ✅ 正确方案一:计数类操作 → 用 update + 脚本

适用场景

  • 投票、点赞、收藏、库存扣减、访问量统计等基于当前值的简单算术操作

操作方式

bash 复制代码
POST /demo_index/_update/1
{
  "script": {
    "source": "ctx._source.votes = (ctx._source.votes ?: 0) + params.inc",
    "params": { "inc": 1 }
  }
}

优势

  • 原子性:ES 在主分片上串行执行脚本,天然防并发冲突。
  • 高效:单次请求完成,无需先 GET。
  • 安全 :即使 1000 并发,结果也精确为 初始值 + 1000
  • 免重试 :永不返回 409 Conflict

高并发测试(复制执行 5 次)

bash 复制代码
POST /demo_index/_update/1
{ "script": { "source": "ctx._source.votes = (ctx._source.votes ?: 0) + 1" } }

最终结果votes = 5,无丢失!

📌 最佳实践
对于所有计数类操作,请永远优先使用 update + 脚本,而不是"先读再全量写"。


3.6 ✅ 正确方案二:复杂文档编辑 → 用 PUT + if_seq_no

适用场景

  • 用户资料、商品详情、文章草稿、配置项等结构复杂、需全量提交的表单数据
  • 多人可能同时编辑同一文档,且不能覆盖他人修改

操作流程

步骤 1:加载当前文档(获取元数据)

json 复制代码
GET /demo_index/_doc/2
// 返回:
{
  "_seq_no": 0,
  "_primary_term": 1,
  "_source": { "title": "张三", "bio": "学生" }
}

步骤 2:用户修改后提交(带版本校验)

json 复制代码
PUT /demo_index/_doc/2?if_seq_no=0&if_primary_term=1
{
  "title": "张三",
  "bio": "研究生"   // ← 修改点
}

步骤 3:结果判断

  • ✅ 若期间无人修改 → 更新成功,_seq_no 变为 1。
  • ❌ 若另一用户已保存(_seq_no 变为 1)→ 返回 409 Conflict

步骤 4:冲突处理

  • 提示用户:"资料已被更新,请刷新后重新编辑。"
  • 前端重新加载最新数据,让用户决定是否合并更改。

为什么这里必须用版本校验?

  • 更新的是整个 _source,不是单个字段。
  • 无法预知用户改了哪些字段 → 不能用 doc 局部更新(会清空未传字段)。
  • 修改内容是任意文本 → 无法用固定脚本表达
  • 必须确保"所见即所存"

📌 最佳实践
只有当你需要替换整个文档时,才使用 if_seq_no + if_primary_term


3.7 综合实验:构建两个防并发系统

实验 A:高可靠投票系统(用脚本)

ini 复制代码
# 后端 API
def vote(item_id):
    es.update(
        index="demo_index",
        id=item_id,
        body={
            "script": {
                "source": "ctx._source.votes = (ctx._source.votes ?: 0) + 1"
            }
        }
    )
    return "success"

✅ 无需版本管理,天然并发安全。

实验 B:协同编辑用户资料(用版本校验)

ini 复制代码
# 后端 API
def update_profile(user_id, new_data):
    current = es.get(index="demo_index", id=user_id)
    try:
        es.index(
            index="demo_index",
            id=user_id,
            body=new_data,  # 全量表单数据
            if_seq_no=current["_seq_no"],
            if_primary_term=current["_primary_term"]
        )
        return "success"
    except VersionConflict:
        return "conflict: 请刷新页面后重试"

✅ 防止多人编辑互相覆盖。


❓ 思考题

1. 为什么 ES 7.0+ 推荐 if_seq_no + if_primary_term 而非旧版 version

_seq_no 是严格递增的操作日志序号,_primary_term 能识别主分片切换,两者组合可精准判断文档状态,避免 _version 在分片恢复时的歧义。

2. 如果投票系统要求"每人只能投一次",还能用脚本吗?

:可以,但需结合外部去重(如 Redis 记录用户ID)。ES 脚本本身不解决业务幂等,只解决并发原子性。


🧭 决策指南:如何选择?

问题 update + 脚本 PUT + if_seq_no
操作是数值增减(+1, -1, ×2)?
更新整个复杂文档(如表单)?
无法预知修改了哪些字段?
追求极致写入吞吐?
必须防止任何人覆盖我的修改? ❌(脚本无此语义)

✅ 总结

  • 计数类操作 → 用 POST /_update + 脚本(原子、高效、免版本)。
  • 全量文档替换 → 用 PUT /_doc + if_seq_no + if_primary_term(防覆盖、保一致)。
  • 不要混淆两者:用解决"文档编辑"的方案去做"计数",是典型的反模式。

记住

"在分布式系统中,没有并发控制的写操作,都是在赌博。"

但更重要的是------用对工具,才能赢得漂亮

❌ 什么情况下绝对不要用 ES 做库存?

  1. 涉及金钱交易:如支付、转账、余额变动。
  2. 法律/合规要求强一致:如金融、医疗记录。
  3. 无法承受任何超卖:如限量版球鞋、演唱会门票。
  4. 需要复杂业务规则:如"买 A 送 B"、"满减叠加",这些逻辑在 ES 脚本中难以维护。

简单来说:对于核心业务(如金融、电商订单、真实库存),强烈推荐"业务层实现分布式锁或事务控制";对于非核心、可容忍轻微误差的场景(如点赞、浏览量),可以使用"ES Script Update"。

下面从多个维度进行详细对比和分析:

1. 数据一致性模型

  • ES Script 方案
    • Elasticsearch 不是强一致性数据库 ,它是一个近实时(NRT)的搜索引擎
    • 脚本执行虽然是原子的,但不提供跨文档、跨索引的事务
    • 如果你的库存扣减需要同时更新订单表、用户积分表,ES 无法保证这些操作的原子性。
    • 风险:极端情况下(如节点故障、脚本 bug),可能出现数据不一致。
  • DB 事务方案
    • 关系型数据库(如 MySQL, PostgreSQL)提供 ACID 事务
    • 可以在一个事务中完成:扣库存 + 创建订单 + 记录流水
    • 保证:要么全部成功,要么全部回滚,数据绝对一致。

结论:涉及资金、核心资产的场景,必须用 DB 事务。

2. 可靠性与可维护性

  • ES Script 方案
    • Painless 脚本调试困难,错误日志不友好。
    • 脚本逻辑散落在各个 update 请求中,难以统一管理和版本控制。
    • 如果业务逻辑变更(如增加风控规则),需要修改脚本并重新部署。
  • DB 事务方案
    • 业务逻辑集中在应用代码或存储过程中,易于测试、调试和维护。
    • 可以利用成熟的 ORM、事务管理器(如 Spring @Transactional)。
    • 审计、回滚、补偿机制更完善。

结论:长期可维护性,DB 方案胜出。

🛠 混合架构:最佳实践(推荐!)

在真实的大型系统中,两者并非互斥,而是互补。最健壮的架构通常是:

scss 复制代码
[用户请求]
    │
    ▼
[应用服务] ------(1)------> [MySQL: 扣真实库存 + 创建订单] (强一致)
    │
    ▼
(2) 异步消息 (Kafka/RabbitMQ)
    │
    ▼
[ES 同步服务] ------(3)------> [Elasticsearch: 更新搜索库存] (最终一致)

工作流程:

  1. 写操作 :所有核心写入(扣库存、下单)都在 MySQL 事务中完成。
  2. 读操作 :搜索、列表页等读多写少 的场景,从 ES 读取(包含一个"搜索库存"字段)。
  3. 同步 :通过 Binlog 监听(如 Canal)或 MQ,将 DB 的变更异步同步到 ES

优势:

  • 强一致性:核心数据由 DB 保证。
  • 高性能:ES 承担高并发读,DB 压力小。
  • 解耦:ES 出现问题不影响下单主流程。
  • 容错 :即使 ES 同步延迟,用户看到的"搜索库存"可能略高于真实库存,但下单时会以 DB 为准,不会超卖。

💡 例如:你在淘宝看到某商品"库存 100+",点进去下单时才真正校验库存。这就是混合架构的体现。

最佳实践建议

  • 新手/中小项目 :优先用 DB 事务,简单、可靠、不易出错。
  • 高并发非核心场景 :可以用 ES Script 提升性能。
  • 大型系统 :采用 "DB 为主 + ES 为辅" 的混合架构,兼顾一致性与性能。

记住:技术选型的本质,是在"一致性"、"可用性"和"复杂性"之间做权衡。 永远把核心资产的安全放在第一位。

# Elasticsearch8.X实战速学:核心工作原理之文档更新、删除原理

相关推荐
摇滚侠3 小时前
自动补全 黑马 Elasticsearch 全套教程,黑马旅游网案例
大数据·elasticsearch·搜索引擎
逸Y 仙X3 小时前
文章二十一:ElasticSearch 词项查询与调度查询实战
java·大数据·数据库·elasticsearch·搜索引擎
摇滚侠3 小时前
数据聚合 黑马 Elasticsearch 全套教程,黑马旅游网案例
大数据·elasticsearch·搜索引擎
Elasticsearch4 小时前
如何衡量和提升 Elasticsearch 搜索召回率:通过 混合搜索 从 0.43 提升到 0.75
elasticsearch
历程里程碑7 小时前
MySQL数据类型全解析 + 代码实操讲解
大数据·开发语言·数据库·sql·mysql·elasticsearch·搜索引擎
绘梨衣5477 小时前
django-elasticsearch-dsl-drf 搜索服务搭建教学文档
python·elasticsearch·django
Adolf_19938 小时前
Mac 配置Homebrew + Oh My Zsh + npm全局权限问题
大数据·elasticsearch·搜索引擎
二哈赛车手1 天前
新人笔记---ES和kibana启动问题以及一些常用的linux的错误排查方法,以及ES,数据库泄密解决方案[超详细]
java·linux·数据库·spring boot·笔记·elasticsearch