`

es局部更新与版本控制

阅读更多
更新文档中的一部分
在《更新》一章中,我们讲到了要是想更新一个文档,那么就需要去取回数据,更改数据然后将整个文档进行重新索引。当然,你还可以通过使用更新API来做部分更新,比如增加一个计数器。

正如我们提到的,文档不能被修改,它们只能被替换掉。更新API也必须遵循这一法则。从表面看来,貌似是文档被替换了。对内而言,它必须按照找回-修改-索引的流程来进行操作与管理。不同之处在于这个流程是在一个片(shard) 中完成的,因此可以节省多个请求所带来的网络开销。除了节省了步骤,同时我们也能减少多个进程造成冲突的可能性。

使用更新请求最简单的一种用途就是添加新数据。新的数据会被合并到现有数据中,而如果存在相同的字段,就会被新的数据所替换。例如我们可以为我们的博客添加tags和views字段:

POST /website/blog/1/_update
{
   "doc" : {
      "tags" : [ "testing" ],
      "views": 0
   }
}
如果请求成功,我们就会收到一个类似于索引时返回的内容:

{
   "_index" :   "website",
   "_id" :      "1",
   "_type" :    "blog",
   "_version" : 3
}
再次取回数据,你可以在_source中看到更新的结果:

{
   "_index":    "website",
   "_type":     "blog",
   "_id":       "1",
   "_version":  3,
   "found":     true,
   "_source": {
      "title":  "My first blog entry",
      "text":   "Starting to get the hang of this...",
      "tags": [ "testing" ], <1>
      "views":  0 <1>
   }
}
新的数据已经添加到了字段_source中。
使用脚本进行更新

我们将会在《脚本》一章中学习更详细的内容,我们现在只需要了解一些在Elasticsearch中使用API无法直接完成的自定义行为。默认的脚本语言叫做MVEL,但是Elasticsearch也支持JavaScript, Groovy 以及 Python。

MVEL是一个简单高效的JAVA基础动态脚本语言,它的语法类似于Javascript。你可以在Elasticsearch scripting docs 以及 MVEL website了解更多关于MVEL的信息。

脚本语言可以在更新API中被用来修改_source中的内容,而它在脚本中被称为ctx._source。例如,我们可以使用脚本来增加博文中views的数字:

POST /website/blog/1/_update
{
   "script" : "ctx._source.views+=1"
}
我们同样可以使用脚本在tags数组中添加新的tag。在这个例子中,我们把新的tag声明为一个变量,而不是将他写死在脚本中。这样Elasticsearch就可以重新使用这个脚本进行tag的添加,而不用再次重新编写脚本了:

POST /website/blog/1/_update
{
   "script" : "ctx._source.tags+=new_tag",
   "params" : {
      "new_tag" : "search"
   }
}
获取文档,后两项发生了变化:

{
   "_index":    "website",
   "_type":     "blog",
   "_id":       "1",
   "_version":  5,
   "found":     true,
   "_source": {
      "title":  "My first blog entry",
      "text":   "Starting to get the hang of this...",
      "tags":  ["testing", "search"], <1>
      "views":  1 <2>
   }
}
tags数组中出现了search。
views字段增加了。
我们甚至可以使用ctx.op来根据内容选择是否删除一个文档:

POST /website/blog/1/_update
{
   "script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'",
    "params" : {
        "count": 1
    }
}
更新一篇可能不存在的文档

想象一下,我们可能需要在Elasticsearch中存储一个页面计数器。每次用户访问这个页面,我们就增加一下当前页面的计数器。但是如果这是个新的页面,我们不能确保这个计数器已经存在。如果我们试着去更新一个不存在的文档,更新操作就会失败。

为了防止上述情况的发生,我们可以使用upsert参数来设定文档不存在时,它应该被创建:

POST /website/pageviews/1/_update
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 1
   }
}
首次运行这个请求时,upsert的内容会被索引成新的文档,它将views字段初始化为1。当之后再请求时,文档已经存在,所以脚本更新就会被执行,views计数器就会增加。

更新和冲突

在本节的开篇我们提到了当取回与重新索引两个步骤间的时间越少,发生改变冲突的可能性就越小。但它并不能被完全消除,在更新的过程中还可能存在另一个进程进行重新索引的可能性。

为了避免丢失数据,更新API会在获取步骤中获取当前文档中的_version,然后将其传递给重新索引步骤中的索引请求。如果其他的进程在这两部之间修改了这个文档,那么_version就会不同,这样更新就会失败。

对于很多的局部更新来说,文档有没有发生变化实际上是不重要的。例如,两个进程都要增加页面浏览的计数器,谁先谁后其实并不重要 —— 发生冲突时只需要重新来过即可。

你可以通过设定retry_on_conflict参数来设置自动完成这项请求的次数,它的默认值是0。

POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 0
   }
}
失败前重新尝试5次
这个参数非常适用于类似于增加计数器这种无关顺序的请求,但是还有些情况的顺序就是很重要的。例如上一节提到的情况,你可以参考乐观并发控制以及悲观并发控制来设定文档的版本号。



当你使用索引API来更新一个文档时,我们先看到了原始文档,然后修改它,最后一次性地将整个新文档进行再次索引处理。Elasticsearch会根据请求发出的顺序来选择出最新的一个文档进行保存。但是,如果在你修改文档的同时其他人也发出了指令,那么他们的修改将会丢失。

很长时间以来,这其实都不是什么大问题。或许我们的主要数据还是存储在一个关系数据库中,而我们只是将为了可以搜索,才将这些数据拷贝到Elasticsearch中。或许发生多个人同时修改一个文件的概率很小,又或者这些偶然的数据丢失并不会影响到我们的正常使用。

但是有些时候如果我们丢失了数据就会出大问题。想象一下,如果我们使用Elasticsearch来存储一个网店的商品数量。每当我们卖出一件,我们就会将这个数量减少一个。

突然有一天,老板决定来个大促销。瞬间,每秒就产生了多笔交易。并行处理,多个进程来处理交易:

无并发控制的后果

web_1中库存量的变化丢失的原因是web_2并不知道它所得到的库存量数据是是过期的。这样就会导致我们误认为还有很多货存,最终顾客就会对我们的行为感到失望。

当我们对数据修改得越频繁,或者在读取和更新数据间有越长的空闲时间,我们就越容易丢失掉我们的数据。

以下是两种能避免在并发更新时丢失数据的方法:

悲观并发控制(PCC)

这一点在关系数据库中被广泛使用。假设这种情况很容易发生,我们就可以阻止对这一资源的访问。典型的例子就是当我们在读取一个数据前先锁定这一行,然后确保只有读取到数据的这个线程可以修改这一行数据。

乐观并发控制(OCC)

Elasticsearch所使用的。假设这种情况并不会经常发生,也不会去阻止某一数据的访问。然而,如果基础数据在我们读取和写入的间隔中发生了变化,更新就会失败。这时候就由程序来决定如何处理这个冲突。例如,它可以重新读取新数据来进行更新,又或者它可以将这一情况直接反馈给用户。

乐观并发控制

Elasticsearch是分布式的。当文档被创建、更新或者删除时,新版本的文档就会被复制到集群中的其他节点上。Elasticsearch即是同步的又是异步的,也就是说复制的请求被平行发送出去,然后可能会混乱地到达目的地。这就需要一种方法能够保证新的数据不会被旧数据所覆盖。

我们在上文提到每当有索引、put和删除的操作时,无论文档有没有变化,它的_version都会增加。Elasticsearch使用_version来确保所有的改变操作都被正确排序。如果一个旧的版本出现在新版本之后,它就会被忽略掉。

我们可以利用_version的优点来确保我们程序修改的数据冲突不会造成数据丢失。我们可以按照我们的想法来指定_version的数字。如果数字错误,请求就是失败。

我们来创建一个新的博文:

PUT /website/blog/1/_create
{
  "title": "My first blog entry",
  "text":  "Just trying this out..."
}
反馈告诉我们这是一个新建的文档,它的_version是1。假设我们要编辑它,把这个数据加载到网页表单中,修改完毕然后保存新版本。

首先我们先要得到文档:

GET /website/blog/1
返回结果显示_version为1:

{
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "1",
  "_version" : 1,
  "found" :    true,
  "_source" :  {
      "title": "My first blog entry",
      "text":  "Just trying this out..."
  }
}
现在,我们试着重新索引文档以保存变化,我们这样指定了version的数字:

PUT /website/blog/1?version=1 <1>
{
  "title": "My first blog entry",
  "text":  "Starting to get the hang of this..."
}
我们只希望当索引中文档的_version是1时,更新才生效。
请求成功相应,返回内容告诉我们_version已经变成了2:

{
  "_index":   "website",
  "_type":    "blog",
  "_id":      "1",
  "_version": 2
  "created":  false
}
然而,当我们再执行同样的索引请求,并依旧指定version=1时,Elasticsearch就会返回一个409 Conflict的响应码,返回内容如下:

{
  "error" : "VersionConflictEngineException[[website][2] [blog][1]:
             version conflict, current [2], provided [1]]",
  "status" : 409
}
这里面指出了文档当前的_version数字是2,而我们要求的数字是1。

我们需要做什么取决于我们程序的需求。比如我们可以告知用户已经有其它人修改了这个文档,你应该再保存之前看一下变化。而对于上文提到的库存量问题,我们可能需要重新读取一下最新的文档,然后显示新的数据。

所有的有关于更新或者删除文档的API都支持version这个参数,有了它你就通过修改你的程序来使用乐观并发控制。

使用外部系统的版本

还有一种常见的情况就是我们还是使用其他的数据库来存储数据,而Elasticsearch只是帮我们检索数据。这也就意味着主数据库只要发生的变更,就需要将其拷贝到Elasticsearch中。如果多个进程同时发生,就会产生上文提到的那些并发问题。

如果你的数据库已经存在了版本号码,或者也可以代表版本的时间戳。这是你就可以在Elasticsearch的查询字符串后面添加version_type=external来使用这些号码。版本号码必须要是大于零小于9.2e+18(Java中long的最大正值)的整数。

Elasticsearch在处理外部版本号时会与对内部版本号的处理有些不同。它不再是检查_version是否与请求中指定的数值相同,而是检查当前的_version是否比指定的数值小。如果请求成功,那么外部的版本号就会被存储到文档中的_version中。

外部版本号不仅可以在索引和删除请求时使用,还可以在创建时使用。

例如,创建一篇使用外部版本号为5的博文,我们可以这样操作:

PUT /website/blog/2?version=5&version_type=external
{
  "title": "My first external blog entry",
  "text":  "Starting to get the hang of this..."
}
在返回结果中,我们可以发现_version是5:

{
  "_index":   "website",
  "_type":    "blog",
  "_id":      "2",
  "_version": 5,
  "created":  true
}
现在我们更新这个文档,并指定version为10:

PUT /website/blog/2?version=10&version_type=external
{
  "title": "My first external blog entry",
  "text":  "This is a piece of cake..."
}
请求被成功执行并且version也变成了10:

{
  "_index":   "website",
  "_type":    "blog",
  "_id":      "2",
  "_version": 10,
  "created":  false
}
如果你再次执行这个命令,你会得到之前的错误提示信息,因为你所指定的版本号并没有大于当前Elasticsearch中的版本号。
分享到:
评论

相关推荐

    elasticsearch权威指南高清中文pdf

    Elasticsearch权威指南中文(最新版)pdf,高清,1. Introduction 2. 入门 i. 是什么 ii. 安装 iii. API iv. 文档 v. 索引 vi. 搜索 vii. 聚合 viii. 小结 ix. 分布式 x. 结语 3. 分布式集群 i. 空集群 ii. 集群健康 ...

    LearnElasticSearch.pdf

    5. Elasticsearch索引操作:Elasticsearch中的索引操作包括创建索引、删除索引、索引映射以及文档的Get、存在检查、更新、创建、删除、版本控制和局部更新等。 6. Elasticsearch搜索功能:Elasticsearch的搜索功能...

    ES入门文档

    Elasticsearch(简称ES)是一款基于Lucene的开源搜索引擎。它为开发者提供了高效、可靠的搜索和数据分析能力,支持多种数据类型的实时索引与搜索。 **1.2 入门指南** - **1.2.1 初识ES** - Elasticsearch是一个...

    opengles_shading_language.pdf

    Khronos Group向当前的推动者(Promoter)、贡献者(Contributor)或采用者(Adopter)成员明确授予复制和重新分发未修改版本的规范的权利,前提是不为此规范收费,并尽可能使用最新可用的规范更新版本。 #### 三、OpenGL...

    全局优化算法CMA-ES.rar

    全局优化算法是解决多维度复杂优化问题的一种方法,它旨在找到一个函数的全局最优解,而非局部最优解。在各种工程、科学计算以及机器学习领域,全局优化算法扮演着至关重要的角色。CMA-ES(Covariance Matrix ...

    Android OpenGl ES绘制圆点与优化圆点锯齿

    在Android平台上,OpenGL ES是一种广泛使用的图形库,用于在移动设备上实现高性能的2D和3D图形渲染。本文将深入探讨如何使用OpenGL ES来绘制圆点,并介绍一种方法来优化圆点边缘的锯齿问题,提升视觉效果。 首先,...

    基于opengl es 的显示gif的例子

    OpenGL ES(OpenGL for Embedded Systems)是OpenGL的一个精简版本,专为嵌入式设备如智能手机、平板电脑等设计,用于处理2D、3D图形渲染。在移动设备上,OpenGL ES是开发图形应用的重要工具,它允许开发者创建复杂...

    OpenGLES demo - 16. 蒙板 Stencil

    4. **重复与更新模板值**:我们可以多次执行上述步骤,每次改变模板测试条件或模板值,以实现更复杂的渲染效果。例如,我们可以先绘制一次背景,然后在特定区域上覆盖前景,从而达到局部遮罩的效果。 5. **关闭模板...

    The OpenGL ES Shading Language

    在Unity引擎中,GLSL ES被用于编写Shader脚本,这些脚本可以与Unity的图形管线紧密结合,创建出丰富的视觉效果。开发者可以通过Unity的 ShaderLab 语法,结合GLSL ES的内核着色器代码,实现定制的图形效果。 总之,...

    NEC-V850-CAN控制器中文翻译.doc

    NEC-V850-CAN控制器中文翻译版本: 该产品专有一个片上1通道的CAN(控制器局部网)控制器,它遵守ISO 11898中规定的CAN协议

    西门子ET200L 6 ES7_133_1BL01接线端子.zip

    在这些系统中,ET200L提供了将现场设备与中央控制系统连接的能力,以实现高效的生产过程监控和控制。 在接线端子方面,6 ES7_133_1BL01模块配备了多种类型的端子,以适应不同类型的信号输入和输出。这些端子可能...

    OpenGLES demo - 9. 矩阵变换

    OpenGLES中,变换通常是按照“先局部后全局”的顺序进行的,即首先对物体进行缩放,然后旋转,最后平移。这意味着矩阵的乘法顺序是相反的:首先计算平移矩阵,然后是旋转矩阵,最后是缩放矩阵。 在iOS开发中,我们...

    Android下Opengl ES

    OpenGL ES(Embedded Systems)是OpenGL的轻量级版本,特别针对嵌入式设备如智能手机和平板电脑设计。在Android中,使用OpenGL ES进行图形渲染主要通过GLSurfaceView和GLSurfaceView.Renderer来实现。 **...

Global site tag (gtag.js) - Google Analytics