ES 支持乐观锁吗?如何实现的?

news2024/9/20 23:31:34

本篇主要介绍一下Elasticsearch的并发控制和乐观锁的实现原理,列举常见的电商场景,关系型数据库的并发控制、ES的并发控制实践。

并发场景

不论是关系型数据库的应用,还是使用Elasticsearch做搜索加速的场景,只要有数据更新,并发控制是永恒的话题。

当我们使用ES更新document的时候,先读取原始文档,做修改,然后把document重新索引,如果有多人同时在做相同的操作,不做并发控制的话,就极有可能会发生修改丢失的。可能有些场景,丢失一两条数据不要紧(比如文章阅读数量统计,评论数量统计),但有些场景对数据严谨性要求极高,丢失一条可能会导致很严重的生产问题,比如电商系统中商品的库存数量,丢失一次更新,可能会导致超卖的现象。

我们还是以电商系统的下单环节举例,某商品库存100个,两个用户下单购买,都包含这件商品,常规下单扣库存的实现步骤

  1. 客户端完成订单数据校验,准备执行下单事务。

  2. 客户端从ES中获取商品的库存数量。

  3. 客户端提交订单事务,并将库存数量扣减。

  4. 客户端将更新后的库存数量写回到ES。

示例流程图如下:

如果没有并发控制,这件商品的库存就会更新成99(实际正确的值是98),这样就会导致超卖现象。假定http-1比http-2先一步执行,出现这个问题的原因是http-2在获取库存数据时,http-1还未完成下单扣减库存后,更新到ES的环节,导致http-2获取的数据已经是过期数据,后续的更新肯定也是错的。

上述的场景,如果更新操作越是频繁,并发数越多,读取到更新这一段的耗时越长,数据出错的概率就越大。

常用的锁方案

并发控制尤为重要,有两种通用的方案可以确保数据在并发更新时的正确性。

悲观并发控制

悲观锁的含义:我认为每次更新都有冲突的可能,并发更新这种操作特别不靠谱,我只相信只有严格按我定义的粒度进行串行更新,才是最安全的,一个线程更新时,其他的线程等着,前一个线程更新完成后,下一个线程再上。

关系型数据库中广泛使用该方案,常见的表锁、行锁、读锁、写锁,依赖redis或memcache等实现的分布式锁,都属于悲观锁的范畴。明显的特征是后续的线程会被挂起等待,性能一般来说比较低,不过自行实现的分布式锁,粒度可以自行控制(按行记录、按客户、按业务类型等),在数据正确性与并发性能方面也能找到很好的折衷点。

乐观并发控制

乐观锁的含义:我认为冲突不经常发生,我想提高并发的性能,如果真有冲突,被冲突的线程重新再尝试几次就好了。

在使用关系型数据库的应用,也经常会自行实现乐观锁的方案,有性能优势,方案实现也不难,还是挺吸引人的。

Elasticsearch默认使用的是乐观锁方案,前面介绍的_version字段,记录的就是每次更新的版本号,只有拿到最新版本号的更新操作,才能更新成功,其他拿到过期数据的更新失败,由客户端程序决定失败后的处理方案,一般是重试。

ES的乐观锁方案

我们还是以上面的案例为背景,若http-2向ES提交更新数据时,ES会判断提交过来的版本号与当前document版本号,document版本号单调递增,如果提交过来的版本号比document版本号小,则说明是过期数据,更新请求将提示错误,过程图如下:

使用外部_version实战乐观锁控制效果 

虽然ElasticSearch的最新本版已经不再支持直接使用version字段实现乐观锁,但仍然允许利用外部版本号version实现乐观锁。

所谓外部版本号version,意思就是version字段的值不是由ElasticSearch自动生成的,而是在创建或修改文档时由应用程序在请求参数中明确指定的。

例如在生产实践中,我们通常使用关系型数据库作为主数据源,将数据从DB同步到ElasticSearch以提供数据搜索服务。而DB中的每行记录都会有update_time字段表示数据最后被更新的时间戳。我们可以利用这个时间戳作为外部版本号,以确保从DB同步数据给ElasticSearch的数据一致性。

删除之前的索引,执行如下命令重新创建一个新的文档;


PUT /book_store/_doc/97876381260?version=1638430097013&version_type=external
{
  "title": "零基础学Java",
  "ISBN": "97876381260",
  "price": 66.88, 
  "stock": 100
}

version_type参数的值是external,明确指定使用外部版本号作为文档version字段的值。

version参数的值是笔者写这篇文章时的时间戳,会作为文档的version字段的值。

该命令与普通的Index API类似,也是“先尝试创建一个新的文档,如果对应的文档已存在就更新那个文档”。但区别是当且仅当request中的version参数的值大于该文档中version字段的值时才会更新成功,否则就会报错表示并发冲突。

ElasticSearch收到该请求后返回如下结果;可以看到文档version字段的值就是创建文档时提供的外部版本号。


{
  "_index" : "book_store",
  "_type" : "_doc",
  "_id" : "97876381260",
  "_version" : 1638430097013,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

再执行一次与上面完全相同的命令,ElasticSearch会返回如下错误结果;


{
  "error" : {
    "root_cause" : [
      {
        "type" : "version_conflict_engine_exception",
        "reason" : "[97876381260]: version conflict, current version [1638430097013] is higher or equal to the one provided [1638430097013]",
        "index_uuid" : "NUfx6zIrQFGWcgh5NNwP4g",
        "shard" : "0",
        "index" : "book_store"
      }
    ],
    "type" : "version_conflict_engine_exception",
    "reason" : "[97876381260]: version conflict, current version [1638430097013] is higher or equal to the one provided [1638430097013]",
    "index_uuid" : "NUfx6zIrQFGWcgh5NNwP4g",
    "shard" : "0",
    "index" : "book_store"
  },
  "status" : 409
}

从错误原因可以看出,由于提供的版本号参数不大于文档中的版本号,所以导致了并发冲突的异常。

如果我们要更新这个文档,则必须用一个新的外部版本号,且这个外部版本号必须大于文档当前的version字段的值。

使用了一个新的时间戳构造文档更新请求;


PUT /book_store/_doc/97876381260?version=1638435446074&version_type=external
{
  "doc": {
    "stock": 99
  }
}

ElasticSearch收到该请求后返回如下结果;更新成功,且文档version字段的值已经被更新成新的外部版本号了。

{
  "_index" : "book_store",
  "_type" : "_doc",
  "_id" : "97876381260",
  "_version" : 1638435446074,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

 _seq_no & _primary_term

_seq_no 和 _primary_term 是用来并发控制,和 _version不同,_version属于当前文档,而 _seq_no属于整个index。

_seq_no & _primary_term

  • _seq_no:索引级别的版本号,索引中所有文档共享一个 _seq_no 。

  • _primary_term:primary_term是一个整数,每当Primary Shard发生重新分配时,比如节点重启,Primary选举或重新分配等primary_term会递增1。主要作用是用来恢复数据时处理当多个文档的_seq_no 一样时的冲突,避免 Primary Shard 上的数据写入被覆盖。

if_seq_no & if_primary_term

在Elasticsearch中,if_seq_no 和 if_primary_term 是用于乐观锁并发控制的参数,用于确保对文档的操作不会与其他操作产生冲突。

if_seq_no 参数用于指定期望的文档序列号(seq_no),而 if_primary_term 参数用于指定期望的 primary term。这两个参数一起作为条件,如果提供的条件与实际存储的文档序列号和主要项匹配,则操作成功执行;否则,操作将失败并返回版本冲突的错误。

假设我们有一个名为 my_index 的索引,其中包含 _id 为 1 的文档。当前文档的 seq_no 是 10primary_term 是 1

示例 1:更新文档

PUT my_index/_doc/1?if_seq_no=10&if_primary_term=1
{
  "foo": "bar"
}

输出:

{
  "_index": "my_index",
  "_type": "_doc",
  "_id": "1",
  "_version": 11,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

在这个示例中,通过提供正确的 if_seq_no 和 if_primary_term 条件,操作成功地更新了文档,并返回了更新后的版本号 _version

示例 2:更新文档,但条件不匹配

PUT my_index/_doc/1?if_seq_no=11&if_primary_term=1
{
  "foo": "bar"
}

输出:

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, current version [11], provided version [11]",
        "index_uuid": "xxxxxxxxxxxxx",
        "shard": "0",
        "index": "my_index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, current version [11], provided version [11]",
    "index_uuid": "xxxxxxxxxxxxx",
    "shard": "0",
    "index": "my_index"
  },
  "status": 409
}

在这个示例中,由于提供的 if_seq_no 和 if_primary_term 条件与实际存储的文档序列号和主要项不匹配,操作失败并返回版本冲突的错误。

通过使用 if_seq_no 和 if_primary_term 参数,我们可以精确控制对文档的并发操作,并避免冲突。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2064108.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java导出分类到Excel

需求 在一般需求中点击导出按钮可以把所有的分类导出到Excel文件中。 技术方案 使用EasyExcel实现Excel的导出操作。 https://github.com/alibaba/easyexcel https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81…

【计算机方向】三本中科院SCI宝刊,国人占比高,发文友好,门槛低,毕业靠它了!

本期将为您带来五本计算机SCI 妥妥毕业神刊! ARTIFICIAL INTELLIGENCE REVIEW AUTONOMOUS ROBOTS Cognitive Computation 期刊名称:ARTIFICIAL INTELLIGENCE REVIEW 期刊简介: 发布对该领域的应用、技术和算法的批判性评估。 为研究人员…

HDU1159——通用子序列,HDU1160——FatMouse的速度、HDU1165——艾迪的研究 II

HDU1159——通用子序列 题目描述 问题 - 1159 (hdu.edu.cn) 问题描述 给定序列的子序列是给定的序列&#xff0c;其中遗漏了一些元素&#xff08;可能没有&#xff09;。给定一个序列 X <x1&#xff0c; x2&#xff0c; ...&#xff0c; xm>如果存在一个严格递增的 X …

【C++ Primer Plus习题】2.7

问题: 解答: #include <iostream> using namespace std;void print(int hour, int minute) {cout << "Time:" << hour << ":" << minute << endl; }int main() {int hour0;int minute 0;cout << "请输入…

NumExpr加速计算(numpy表达式)

文章目录 一、简介二、安装三、函数详解四、性能评估 Python 性能优化&#xff1a;NumExpr Numba CuPy 一、简介 numexpr&#xff08;全称&#xff1a;numpy expression&#xff09;&#xff1a;用于在 NumPy 表达式上快速执行元素级运算的 Python 加速库。 优势&#xff1…

软考高级科目怎么选?

首先上图 从图片中可以看出来&#xff0c;在软件开发中考试方向为程序员-软件设计师-系统架构师或者系统分析师。 系统分析师与系统架构师工作内容&#xff1a; 系统分析师&#xff1a;在信息系统项目开发过程中负责制定信息系统需求规格说明书和项目开发计划、指导和协调信息…

在网站文章中,‌<br>标签对SEO的影响及优化策略

在网页设计和内容创作中&#xff0c;‌<br>标签常被用于实现文本的换行显示。‌然而&#xff0c;‌对于关注SEO&#xff08;‌搜索引擎优化&#xff09;‌的网站管理员和内容创作者来说&#xff0c;‌<br>标签的使用却需要更加谨慎。‌这是因为<br>标签对SEO…

Linux系统编程全面学习

应用层&#xff1a;写一个QT可执行程序、一个C程序 驱动层&#xff1a;写一个LED、蜂鸣器、pwm驱动 硬件层&#xff1a;焊接、layout Linux系统介于应用层和驱动层之间&#xff0c;Linux系统会向应用层提供接口&#xff0c;学习使用的基本是Linux内核向用户提供的接口或者可以…

理解Tomcat的IP绑定与访问控制

在使用Spring Boot开发应用时&#xff0c;内置的Tomcat容器提供了灵活的网络配置选项。特别是&#xff0c;当计算机上有多个网卡时&#xff0c;如何配置server.address属性显得尤为重要。本文将详细探讨不同IP配置对Tomcat服务访问的影响。 多网卡环境下的IP配置 假设你的计算…

java 8种基础数据类型

1、数据范围 2、各个类型转换 实线转换&#xff1a;无信息丢失的自动转换&#xff0c;反方向需要强制类型转换&#xff0c;如&#xff08;int) 虚线转换&#xff1a;可能存在精度丢失 精度丢失示例如下&#xff1a; long l 123456787654321L; float f l; System.out.prin…

框架漏洞大全【万字总结】

文章目录 常见语言开发框架&#xff1a;Thinkphp远程代码执行5.0.23 rce介绍影响版本复现 CNVD-2018-24942介绍影响版本复现 任意文件包含包含日志-3.2x介绍影响版本复现 包含语言&#xff08;QVD-2022-46174&#xff09;介绍影响版本复现 sql注入漏洞(5.0.x)介绍影响版本复现 …

太上老君的“三味真火”也可以提升3D NAND可靠性!

《西游记》中孙悟空因在太上老君的炼丹炉中历经九九八十一难&#xff0c;最终炼就了一双能够洞察一切妖魔鬼怪真身的“火眼金睛”。这双神奇的眼睛&#xff0c;仿佛预示着一种古老的智慧——通过火的考验&#xff0c;可以淬炼出更加坚韧的灵魂。 而在现代科技的洪流中&#xff…

软件测试-自动化测试

自动化测试 测试人员编写自动化测试脚本&#xff0c;维护并解决自动化脚本问题 自动化的主要目的就是用来进行回归测试 回归测试 常见面试题 ⾃动化测试能够取代人工测试吗&#xff1f; ⾃动化测试不⼀定⽐人工测试更能保障系统的可靠性&#xff0c;⾃动化测试是测试⼈员手…

vue2项目从0到1记录

脚手架需要安装完 npm install -g vue/cli1. 使用脚手架创建项目 vue create 项目名2. 引入样式重置normalize.css插件 // 统一浏览器样式 安装&#xff1a;npm install --save normalize.css 引入&#xff1a;import normalize.css/normalize.css3. 根据部署环境判断是否要…

MyBatis[进阶]

大纲: 动态SQL查询 留言板 1. 动态SQL 1.1 <if> 我们都注册过一些信息,有的信息是非必填项,改如何实现呢? 这个时候就需要使⽤动态标签来判断了 ⽐如添加的时候性别gender为⾮必填字段&#xff0c;具体实现如 下&#xff1a; 注解: 如果性别为空: 如果性别不为空:…

你真的会用大模型吗,探索提示词工程的魅力

相信在这一两年内&#xff0c;每个人都尝试使用了各种大模型。不知大家有没有发现&#xff0c;它们的质量参差不齐&#xff0c;回答的内容也不一定准确。随着人工智能技术的快速发展&#xff0c;越来越多的模型被开发出来并用于各种应用&#xff0c;但并非所有模型都能够提供可…

Leetcode每日刷题之904.水果成篮

1.题目解析 本题的题目要求较长&#xff0c;不过理解起来较为简单&#xff0c;就是在给定数组内找出最长子数组&#xff0c;并且该最长子数组只能有两种数字&#xff0c;最后返回该符合条件的最长子数组的长度即可 题目来源&#xff1a;904.水果成篮 2.算法原理 本题的核心是找…

组件提前渲染

问题&#xff1a; 组件正常引入并使用的过程中&#xff0c;出现组件第一次渲染不显示&#xff0c;只有再次刷新页面才显示的问题 <el-table-column label"图纸规定" align"center" prop"tzgd" v-if"mbform.zbzd.tzgd" width"…

动手实现基于Reactor模型的高并发Web服务器(一):epoll+多线程版本

系统流程概览 main函数 对于一个服务器程序来说&#xff0c;因为要为外部的客户端程序提供网络服务&#xff0c;也就是进行数据的读写&#xff0c;这就必然需要一个 socket 文件描述符&#xff0c;只有拥有了文件描述符 C/S 两端才能通过 socket 套接字进行网络通信&#xff0…

【深海王国】小学生都能玩的单片机!番外2:Arduino控制其他元器件

Hi٩(๑ ^ o ^ ๑)۶, 各位深海王国的同志们&#xff0c;早上下午晚上凌晨好呀~辛勤工作的你今天也辛苦啦 (o゜▽゜)o☆ 今天大都督为大家带来单片机的新番外系列——小学生都能玩的单片机&#xff01;番外2&#xff1a;Arduino控制其他元器件&#xff0c;带你学习如何使用Ard…