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

本篇主要介绍一下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.rhkb.cn/news/408514.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

“Docker中部署Kibana:步骤与指南“

博主这篇文章是跟Elasticsearch那篇文章是有关系的,建议大家先去看: 轻松上手:Docker部署Elasticsearch,高效构建搜索引擎环境_docker 启动 es-CSDN博客 这篇博文,还有镜像下载不下来的情况,大家可以去看…

pikachu-ssrf_redis

目录 SSRF 1、SSRF漏洞介绍: 2、SSRF漏洞原理: 3、SSRF漏洞利用手段: 4、SSRF漏洞绕过方法: SSRF(curl)用法 1、通过网址访问链接 2、利用file协议查看本地文件 3、dict协议扫描内网主机开放端口 4.gopher:威…

计算机网络基础 - 应用层(1)

计算机网络基础 应用层网络应用的体系结构C/S 体系结构P2P 体系结构C/S 和 P2P 体系结构的混合体 进程通信概述套接字(Socket)TCP socketUDP socket 应用层协议应用层需要传输层提供的服务Web 与 HTTP概念非持续连接和持续连接HTTP报文格式请求报文响应报…

vs code中编写html的配置,插件安装

首先安装vs code 插件安装下面三个: 功能分别是: html css support :就是支持html环境,因为vs code就是一个文本编辑器 live server:自动更新编写的文件在浏览器刷新 auto rename tag:自动修改另一半标签…

Postman接口自动化测试:从入门到实践!

前言 在软件开发过程中,接口测试是确保软件各组件之间正确交互的关键环节。Postman作为一款强大的API开发工具,不仅支持接口请求的发送与调试,还提供了丰富的自动化测试功能,使得接口自动化测试变得更加高效和便捷。本文将从Post…

音频采集spring_ws_webrtc (html采集麦克风转gb711并发送广播播放)完整案例

下载地址:http://www.gxcode.top/code 项目说明 springbootwebscoektwebrtc 项目通过前端webrtc采集麦克风声音,通过websocket发送后台,然后处理成g711-alaw字节数据发生给广播UDP并播放。 后台处理项目使用线程池(5个线程)接受webrtc数据并…

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《极端冰雪天气下计及孤岛划分与融合的配电网故障恢复》

本专栏栏目提供文章与程序复现思路,具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

【计算机三级-数据库技术】操作题大题(第八套)

第46题 (1) 1 (2) create table ( a1 int, a6 int, a10 int, Primary Key(a1, a6), Constraint fk_PerOrders Foreign Key (a1) References T1(a1), Constraint fk_PerOrders Foreign Key (a6) References T3(a6) ) 第47题 答案: [1]Pro…

给Go+Sciter开发的桌面客户端软件添加系统托盘图标

在桌面端软件开发中,系统托盘图标是提升用户体验的重要元素。托盘图标不仅能提供直观的状态反馈,还能让软件在后台运行时依然保持与用户的交互。通过托盘图标,用户可以轻松最小化软件、退出程序,甚至弹出通知,从而避免…

PG数据库导致断电/重启无法正常启动

一、问题 数据库断电后,启动PG数据库后无法正常启动,报”psql: could not connect to server: No such file or directory”的错误,错误图片如下: 二、背景分析 数据库是单机版,使用k8s进行部署运行在指定节点&#…

K8S系列——(二)、K8S部署RocketMQ集群

1、环境准备 要将RocketMQ部署到K8S上,首先你需要提前准备一个K8S集群环境,如图我已经准备好了一个版本为 v1.28.13 的 K8S 集群(其他版本也没问题): 角色IPMaster192.168.6.220Node-1192.168.6.221Node-2192.168.6.…

Vue中的methods方法与computed计算属性的区别

在创建的 Vue 应用程序实例中,可以通过 methods 选项定义方法。应用程序实例本身会代理 methods 选项中的所有方法,因此可以像访问 data 数据那样来调用方法。在模板中绑定表达式只能用于简单的运算。如果运算比较复杂,可以使用 Vue.js 提供的…

科技在日常生活中的革新

在科技日新月异的今天,‌我们的生活正经历着前所未有的变革。‌从智能家居到可穿戴设备,‌科技已经渗透到我们生活的每一个角落,‌深刻地影响着我们的生活方式和社会经济的发展。‌ 智能家居系统的出现,‌无疑是科技改变生活的典…

C语言函数介绍(上)

函数概念库函数标准库和头文件库函数的使用方法头文件包含库函数文档的一般格式 自定义函数函数的语法形式函数例子 形参和实参实参形参实参和形参的关系 return 语句数组做函数参数 函数概念 数学中我们其实就见过函数的概念,比如:一次函数 ykxb &…

【Python进阶(十)】——Matplotlib基础可视化

🍉CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一|统计学|干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项,参与研究经费10w、40w级横向 文…

IOS 11 通用Base控制器封装

整体规划 BaseController:把viewDidLoad逻辑拆分为三个方法,方便管理。 BaseCommonController:不同项目可以复用的逻辑,例如:设置背景颜色方法等 BaseLogicController:本项目的通用逻辑,主要…

使用css如何获取最后一行的元素?使用css解决双边框问题

一、项目场景: 在小程序上需要实现一个如下图的ui效果图 需要满足以下条件 一行放不下 自动换行最后一行或者只有一行时,文字底部不能有线 二、初版实现 按照上面的要求,最开是的实现代码如下 我是给每一个元素都添加了一个下边框&#x…

测量 Redis 服务器的固有延迟

redis-cli --intrinsic-latency redis-cli --intrinsic-latency 命令用于测量 Redis 服务器的固有延迟。 固有延迟指的是 Redis 服务器处理一个命令所需的最短时间,不包括网络延迟。通过这个测量,我们可以了解 Redis 服务器本身的性能,而不…

[Algorithm][综合训练][mari和shiny][重排字符串]详细讲解

目录 1.mari和shiny1.题目链接2.算法原理详解 && 代码实现 2.重排字符串1.题目链接2.算法原理详解 && 代码实现 1.mari和shiny 1.题目链接 mari和shiny 2.算法原理详解 && 代码实现 自己的版本:三层循环暴力枚举 --> 超时 --> 40% …

ssrf漏洞复现

一、环境搭建 这里选用的平台是pikachu 地址:GitHub - zhuifengshaonianhanlu/pikachu: 一个好玩的Web安全-漏洞测试平台 可能遇到的问题——MySQL连接问题 mysql> ALTER USER root IDENTIFIED WITH mysql_native_password BY root; Query OK, 0 rows affect…