Elasticsearch系列---增量更新原理及优点

概要

本篇主要介绍增量更新(partial update,也叫局部更新)的核心原理,介绍6.3.1版本的Elasticsearch脚本使用实例和增量更新的优点。前端

增量更新过程与原理

简单回顾

前文咱们有简单介绍过增量的语法,简单回顾一下请求示例:node

POST /music/children/1/_update
{
  "doc": {
    "length": "76"
  }
}复制代码

通常从客户端到Elasticsearch,完整的应用请求流程基本是这样的:缓存

  • 客户端先发起GET请求,获取到document信息,展示到前端页面上,供用户进行编辑。
  • 用户编辑完数据后,点击提交。
  • 后台系统处理修改后的数据,并组装好完整的document报文。
  • 发送PUT请求到ES,进行全量替换。
  • ES将老的document标记为deleted,而后从新建立一个新的document。

Elasticsearch的document是基于不可变模式设计的,全部的document更新,其实都建立了一个新的document出来,再把老的document标记为deleted,增量更新也不例外,只是GET全量document数据,整合新的document,替换老的document这三步操做全在一个shard里完成,毫秒级完成。安全

增量更新分片之间的交互

12

增量更新document的步骤:markdown

  1. Java客户端向ES集群发送更新请求。
  2. Coodinate Node收到请求,但该document不在当前node上,它将请求转发到Node2节点的P0 shard上。
  3. Node 2检索document,修改source下的JSON,而且从新索引该document,若是有其余线程修改过该document,有版本冲突的话,会从新尝试更新document,最大重试retryon_conflict次,超出重试次数后放弃。
  4. 若是步骤3操做成功,Node2会将该document的完整内容异步转发到Node1和Node3的replica shard,从新创建索引。一旦全部replica都返回成功,Node2返回成功消息给Coodinate Node。
  5. Coodinate Node响应更新成功消息给客户端,此时ES集群内primary shard和replica shard都已经更新完成。
注意几点:
  • primary shard向replica shard进行document数据同步时,发送的是document的完整信息,由于异步请求不保证有序,若是发增量信息的话,顺序错乱会致使document内容错误。
  • 只要Coodinate Node向Java客户端响应成功,就表示全部的primary shard向replica shard都完成了更新操做,此时ES集群内的数据是一致的,更新是安全的。
  • retry策略,ES再次获取document数据和最新版本号,成功就更新,失败再试,最大次数能够设置,如5次:retryonconflict=5
  • retry策略在增量操做无关顺序的场景更适用,好比说计数操做,谁先执行谁后执行,关系不大,最终结果是对的就行。其余的一些场景,如库存的变化,帐户余额的变化,直接更新成指定数值的,确定不能使用retry策略,但能够转化成加减法,以下单时由直接更新库存数量的逻辑改为“当前可用库存数量=库存数量-订单商品数量”,帐户余额的更新加减变化的金额,这样能够在必定程度上,把顺序有关转化成顺序无关,就能够更方便的使用retry策略解决冲突的问题。

增量更新的优势

  1. 全部的查询、修改和回写操做,都是在ES内部完成的,减少了网络数据传输开销(2次),提高了性能。
  2. 相比全量替换的时间间隔(秒级以上),缩短了查询和修改的时间间隔(毫秒级),能够有效下降并发冲突的状况。

使用脚本实现增量更新

Elasticsearch支持使用脚本实现更为灵活的逻辑,6.0版本之后,默认支持的脚本是painless,而且再也不支持Groovy,由于Groovy编译有必定几率会出现内存不释放,最终致使Full GC的问题。网络

咱们以英文儿歌的案例为背景,假设document的数据是这样:架构

{
  "_index": "music",
  "_type": "children",
  "_id": "2",
  "_version": 6,
  "found": true,
  "_source": {
    "name": "wake me, shark me",
    "content": "don't let me sleep too late, gonna get up brightly early in the morning",
    "language": "english",
    "length": "55",
    "likes": 0
  }
}复制代码

内置脚本

如今有这样一个需求:每当有人点击播放一次歌曲时,该document的likes field就自增1,咱们能够用简单的脚原本实现:并发

POST /music/children/2/_update
{
   "script" : "ctx._source.likes++"
}复制代码

执行一次后,再查询该document,发现likes变成1,每执行一次,likes都自增1,结果符合预期。less

外部脚本

对刚刚那个自增需求作一些改动,支持批量更新播放量,自增的数量由参数传入,脚本也能够经过导入的方式,预先编译存储在ES中,使用的时候调用便可。异步

建立脚本
POST _scripts/music-likes
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.likes += params.new_likes" 
  }
}复制代码

脚本ID为music-likes,参数为new_likes,是能够在调用时传入的。

使用脚本

咱们更新时,执行以下请求,就能够调用刚刚建立的脚本

POST /music/children/2/_update
{
  "script": {
    "id": "music-likes", 
    "params": {
      "new_likes": 2
    }
  }
}复制代码

id即建立脚本时的music-likes,params是固定写法,里面的参数为new_likes,执行后再查看document信息,能够看到likes field的值按传入的值进行累加,结果符合预期。

查看脚本

命令:

GET _scripts/music-likes复制代码

斜杠后面的参数即脚本ID

删除脚本

命令:

DELETE _scripts/music-likes复制代码

斜杠后面的参数即脚本ID

脚本注意事项

  • ES检测到新脚本时,会执行脚本编译,并将它存储在缓存中,编译比较耗时。
  • 脚本的编写能参数化的,就不要硬编码,提升脚本的复用性。
  • 短期内太多的脚本编译,若是超出了ES的承受范围,ES直接报circuitbreakingexception错误,这个范围默认是15条/分钟。
  • 脚本缓存默认100条,默认不设过时时间,每一个脚本最大字符65535字节,想自行配置的话能够改script.cache.expire、script.cache.maxsize和script.maxsizeinbytes参数。

一句话,提升脚本的复用性。

upsert语法

像刚刚的案例,实现的是一个播放计数器的功能,目前这个计数器是与内容存储在一块儿,若是计数器单独存储,可能会出现新上架的一首歌,但计数器的document可能还不存在,试图对它作更新操做会报documentmissingexception错误,这种场景咱们须要使用upsert语法:

POST /music/children/3/_update
{
   "script" : "ctx._source.likes++",
   "upsert": {
     "likes": 0
   }
}复制代码

若是id为3的记录不存在,第一次请求时,执行upsert里面的JSON内容,初始化一个新文档,ID为3,likes值为0;第二次请求时,文档已经存在,此时作script脚本的更新操做,likes自增。

小结

本篇简单介绍了增量更新的过程与原理,并与全量替换作了简单的对比,针对一些简单的计数场景,引入脚本的实现方式案例,脚本能够实现很丰富的功能,具体能够查看官网对Painless的介绍。

专一Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区Java架构社区