前面的文章里面主要讲的是查询的用法,还是延续之前的文章格式,这里讲讲修改。

1. 单文档修改

1.1. insert

其实在数据准备阶段已经有新增的例子了。

DSL
POST /operation_log/_doc
{
  "ip": "0.0.0.0",
  "module": "测试数据"
}
spring
        OperationLog operationLog=new OperationLog();
        operationLog.setIp("0.0.0.0");
        operationLog.setModule("测试数据");
        return esRestTemplate.save(operationLog);

1.2. update-(save)

新增时,springboot 用到的是 save 方法,更新时也一样可以。不过得拿到文档的id,这里id=13OkA4QBMgWicIn2wBwM。

DSL
PUT /operation_log/_doc/13OkA4QBMgWicIn2wBwM
{
  "ip": "0.0.0.0",
  "module": "测试数据1"
}
spring
esRestTemplate.save(operationLog);

1.3. update-(document)

DSL
POST /operation_log/_update/13OkA4QBMgWicIn2wBwM
{
  "doc": {
    "module":"测试数据1"
  }
}
spring
        Document document = Document.create();
        document.put("module", "测试数据1");
        UpdateQuery updateQuery = UpdateQuery
                .builder(id)
                .withDocument(document)
                .build();
        esRestTemplate.update(updateQuery,IndexCoordinates.of("operation_log"));

1.4. update-(script)

DSL
POST /operation_log/_update/13OkA4QBMgWicIn2wBwM
{
  "script": {
    "source": "ctx._source.module = params.module",
    "params": {
      "module": "测试数据1"
    }
  }
}
spring
        Map<String, Object> params = new HashMap<>();
        params.put("module", "测试数据1");
        UpdateQuery updateQuery = UpdateQuery
                .builder(id)
                .withScript("ctx._source.module = params.module")
                .withParams(params)
                .build();
        esRestTemplate.update(updateQuery, IndexCoordinates.of("operation_log"));

1.5. delete

DSL
DELETE /operation_log/_doc/13OkA4QBMgWicIn2wBwM
spring
        esRestTemplate.delete(id, OperationLog.class);

2. 批量修改 bulk

批量新增 DSL
POST /operation_log/_bulk
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"测试数据1"}
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"测试数据2"}
{"create":{"_index":"operation_log"}}
{"ip":"0.0.0.0","module":"测试数据3"}
批量更新 DSL
POST /operation_log/_bulk
{"update":{"_id":"2HP9A4QBMgWicIn26BzR"}}
{"doc":{"module":"测试数据11"}}
{"update":{"_id":"2XP9A4QBMgWicIn26BzR"}}
{"script":{"source":"ctx._source.module = params.module","params":{"module":"测试数据22"}}}
批量删除 DSL
POST /operation_log/_bulk
{"delete":{"_id":"2HP9A4QBMgWicIn26BzR"}}
{"delete":{"_id":"2XP9A4QBMgWicIn26BzR"}}
{"delete":{"_id":"2nP9A4QBMgWicIn26BzR"}}

不知是否注意到,在批量更新的语句中,支持同时 doc、script 两种更新方式。实际上来说,_bulk 其实支持同时将上述的三种语句一起提交执行。
不过项目上一般不会如此应用,都是单独分开来。像批量新增,save 方法就支持批量新增操作,虽然底层代码还是调用 bulkOperation

spring bulkUpdate
    @PatchMapping("bulk-update")
    public void bulkUpdate() {
        Map<String, Object> params = new HashMap<>();
        params.put("module", "测试数据2");
        String scriptStr = "ctx._source.module = params.module";
        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
                .build();
        List<UpdateQuery> updateQueryList = esRestTemplate.search(query, OperationLog.class)
                .stream()
                .map(SearchHit::getContent)
                .map(obj -> UpdateQuery.builder(obj.getId())
                        .withScript(scriptStr)
                        .withParams(params)
                        .build())
                .collect(Collectors.toList());
        esRestTemplate.bulkUpdate(updateQueryList, OperationLog.class);
    }

有关更详细、更好使用 bulk的部分,建议查看 es官网资料

3. 修改ByQuery

3.1. updateByQuery

DSL
POST /operation_log/_update_by_query
{
  "script": {
    "source": "ctx._source.module = params.module",
    "params": {
      "module": "测试数据1"
    }
  },
  "query": {
    "term": {
      "ip": "0.0.0.0"
    }
  }
}
spring
    @PatchMapping("update-by-query")
    public void updateByQuery() {
        Map<String, Object> params = new HashMap<>();
        params.put("module", "测试数据2");
        String scriptStr = "ctx._source.module = params.module";
        UpdateQuery updateQuery = UpdateQuery
                .builder(new NativeSearchQueryBuilder()
                        .withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
                        .build())
                .withScript(scriptStr)
                .withScriptType(ScriptType.INLINE)
                .withLang("painless")
                .withParams(params)
                .build();
        esRestTemplate.updateByQuery(updateQuery, IndexCoordinates.of("operation_log"));
    }

可以对比一下上面的 bulkUpdate 方法,发现有些不同:

  • updateByQuery 只支持Script,不支持 Document 的方式更新。
  • updateByQuery 使用 Script 方式更新时,必须传递 scriptTypeLang 这些辅助参数。原本 bulkUpdate 中也是要传的,只不过底层方法封装了,但是没有给 updateByQuery 封装。(实际踩过坑,看封装方法才得知)

3.2. deleteByQuery

DSL
POST /operation_log/_delete_by_query
{
  "query": {
    "term": {
      "ip": "0.0.0.0"
    }
  }
}
spring
        Query query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.termQuery("ip", "0.0.0.0"))
                .build();
        esRestTemplate.delete(query, OperationLog.class);

delete_by_query并不是真正意义上物理文档删除,而是只是版本变化并且对文档增加了删除标记。当我们再次搜索的时候,会搜索全部然后过滤掉有删除标记的文档。因此,该索引所占的空间并不会随着该API的操作磁盘空间会马上释放掉,只有等到下一次段合并的时候才真正被物理删除,这个时候磁盘空间才会释放。相反,在被查询到的文档标记删除过程同样需要占用磁盘空间,这个时候,你会发现触发该API操作的时候磁盘不但没有被释放,反而磁盘使用率上升了。

3.3. 调优参数

可参考es官网 ElasticSearch API guide,在批量修改文档时,有很多参数可以配合调优。

这里先列举几个常用的,剩下详细的请看官方文档:

1. refresh

ES的索引数据是写入到磁盘上的。但这个过程是分阶段实现的,因为IO的操作是比较费时的。

  • 先写到内存中,此时不可搜索。
  • 默认经过 1s 之后会被写入 lucene 的底层文件 segment 中 ,此时可以搜索到。
  • refresh 之后才会写入磁盘

以上过程由于随时可能被中断导致数据丢失,所以每一个过程都会有 translog 记录,如果中间有任何一步失败了,等服务器重启之后就会重试,保证数据写入。translog也是先存在内存里的,然后默认5秒刷一次写到硬盘里。

在 index ,Update , Delete , Bulk 等操作中,可以设置 refresh 的值。如下:

  • false:默认值。不要刷新相关的动作。在请求返回后,此请求所做的更改将在某个时刻显示。如:

    创建一个文档,而不做任何使其可以搜索的事情:
    PUT /test/test/1
    PUT /test/test/2?refresh=false
  • true或空字符串:更新数据之后,立刻对相关的分片(包括副本) 刷新,这个刷新操作保证了数据更新的结果可以立刻被搜索到。

    创建一个文档并立即刷新索引,使其可见:
    PUT /test/test/1?refresh
    PUT /test/test/2?refresh=true
  • wait_for:等待请求所做的更改在返回之前通过冲刷显示。这不会强制立即刷新,而是等待刷新发生。 Elasticsearch会自动每隔index.refresh_interval刷新已经更改的分片,默认为1秒。该设置是动态的。

    创建一个文档并等待它成为搜索可见:
    PUT /test/test/1?refresh=wait_for
2. scroll_size

这个参数是执行删除的时候,每次每个线程会查询的数据量,然后进行删除。默认值是100,就是说每个线程每次都会查询出100条数据然后再删除。

3. slices

可以理解为,默认值是一个线程在进行查询数据并删除,当设置这个slices值时,会将es下的数据进行切分,启动多个task去做删除,理解为多线程执行操作。

但是就像不建议滥用多线程一样,不建议设置slices值太大,否则会导致es出问题。建议设为索引分片数量的倍数(如:1倍、2倍),有助于基于每个分片的数据做切分。

4. conflicts

如果按查询删除遇到版本冲突,该怎么办,有两个值:

  • abort:默认值,冲突时中止。
  • proceed:冲突时继续。

举前面updateByQuery的例子。_update_by_query 在启动时获取索引的快照,并使用内部版本控制对其进行索引。这意味着如果文档在拍摄快照和处理索引请求之间发生变化,则会发生版本冲突。当版本匹配文档被更新并且版本号增加。

所有更新和查询失败导致 _update_by_query 中止并在响应失败中返回。已执行的更新仍然坚持。换句话说,进程没有回滚,只会中止。当第一个故障导致中止时,失败批量请求返回的所有故障都会返回到故障元素中;因此,有可能会有不少失败的实体。

如果你想简单地计算版本冲突,不会导致 _update_by_query中止,你可以在url设置conflicts=proceed 或在请求体设置"conflicts": "proceed"。如上例中改成:

POST /operation_log/_update_by_query?conflicts=proceed

4. 锁

Elasticsearch和数据库一样,在多线程并发访问修改的情况下,会有一个锁机制来控制每次修改的均为最新的文档,核心是使用乐观锁的机制。

_version

在 Elasticsearch 通过 _version 来记录文档的版本。第一次创建一个document的时候,它的_version内部版本号就是1;以后,每次对这个document执行修改或者删除操作,都会对这个_version版本号自动加1;哪怕是删除,也会对这条数据的版本号加1

由于 segment 时不能被修改的,所以当对一个文档执行 DELETE 之后,在插入相同id的文档,version 版本不会是0,而是在 DELETE 操作的version上递增。

在对文档进行修改和删除时,version 会递增,也可以由用户指定。只有当版本号大于当前版本时,才会修改删除成功,否则失败。当并发请求时,先修改成功的,version 会增加,这个时候其他请求就会犹豫 version 不匹配从而修改失败。

external version

es提供了一个feature,就是说,你可以不用它提供的内部_version版本号来进行并发控制,可以基于你自己维护的一个版本号来进行并发控制。

举个例子,假如你的数据在mysql里也有一份,然后你的应用系统本身就维护了一个版本号,无论是什么自己生成的或程序控制的。这个时候,你进行乐观锁并发控制的时候,可能并不是想要用es内部的_version来进行控制,而是用你自己维护的那个version来进行控制。

?version=1   基于_version

?version=1&version_type=external   基于external version

_version与version_type=external唯一的区别在于:

  • _version,只有当你提供的version与es中的_version一模一样的时候,才可以进行修改,只要不一样,就报错。
  • 当version_type=external的时候,只有当你提供的version比es中的_version大的时候,才能完成修改。
es,_version=1,    ?version=1,才能更新成功

es,_version=1,    ?version>1&version_type=external,才能成功,
比如说:?version=2&version_type=external

引用:


KerryWu
641 声望159 粉丝

保持饥饿