很久没有怎么随记笔记了,今天这里是为了纠正一个一直以来我们使用es的一个误区,这个误区很大的可能你会就范。很多童靴会把update_by_query拿mysql的语法特性来用,那你就大错特错了,这里有必要温习下我之前的一篇update_by_query,理论上讲es的准实时的仅限于search,而get id则是实时的。
实践往往是检验真理的唯一标准,看下面演示吧
http://xxx:9200/mytest_user/_settings
{
"index" : {
"refresh_interval" : -1
}
}
http://xxx:9200/mytest_user/_doc/2
{
"product_type": "test",
"product_code": "324049",
"shop_code": "9N72"
}
http://xxx:9200/mytest_user/_doc/2/_update
{ "doc" : {
"name" : "new_name"
}
}
http://xxx:9200/mytest_user/_doc/2/_update
{ "doc" : {
"name" : "alex",
"age" : 20
}
}
...
http://xxx:9200/mytest_user/_update_by_query?conflicts=proceed
{
"query" : {
"term" : { "product_code": "324049" }
},
"script": {
"source": "ctx._source.en_product_name='cn';ctx._source.plu_code='00';"
}
}
http://xxx:9200/mytest_user/_doc/2
没错_update_by_query使用了search,顾没有任何反应。而update api借助get API的实时性做到了(即先根据文档ID做一次GET,然后拿最新文档修改后写回去),而get API为此有个参数可以控制的是为非实时(http://xxx:9200/mytest_user/_doc/4?realtime=false)。
realtime
官方介绍,
默认情况下,get API是实时的,并且不受索引刷新率的影响(当数据在搜索中变为可见时)。如果文档已更新但尚未刷新,则get API将发出刷新调用以使文档可见。这还会使上次刷新后的其他文档可见。为了禁用realtime GET,可以将realtime参数设置为false。update API的文档和源码都没有提供一个“禁用”实时性的参数。update对GET的调用,传入的realtime是写死为true的。
为何get API会要求实时?
update允许对文档做部分字段更新。如果有2个请求分别更新了不同的字段, 可能先更新的数据只在writter buffer里,searcher里看不到,那后面的更新还是在老版本文档上做的,造成部分更新丢失。
上面的结论我们借助cat的监控可以看到:http://xxx:9200/_cat/segments/mytest_user?v
如果realtime
设置为false,就从searcher里面拿,而searcher只能访问refresh过的数据。刚写入的数据存在于index writter buffer里,暂时无法搜索到,所以这种方式拿到的数据是准实时的。
在5.x版本以上,实时则能够访问到index writter buffer里的数据,并且还执行了强制刷新(并非refresh_interval),生成了新的segment file。如果短时间反复大量更新相同doc id的操作,会因为过于频繁的refresh短时间生成很多小segment,继而不断做短合产生性能损耗。官方认为,在提升大多数应用场景性能情况下,对于这种较少见的场景下的性能损失是值得的,应该在应用层面解决。
注:update方法更新文档,如果关闭了Upsert,意味着如果更新的文档id如果不存在,会抛出doc missing异常,大量抛出和捕获doc missing异常开销很高。
在2.4版本中,没有采用refresh的方式让数据实时,而是直接访问的translog来保证GET的实时性。官方在这个变更里 https://github.com/elastic/elasticsearch/pull/20102 将其更新方式改为了refresh。理由是之前ES里有很多地方用translog维护数据的位置,使得很多操作变得很慢,去掉对translog的依赖可以提高性能。
代码验证环节
代码实现中确实有realtime参数和 refresh("realtime_get"); 的函数调用
//源自core/src/main/java/org/elasticsearch/index/engine/InternalEngine.java
public GetResult get(Get get, Function searcherFactory, LongConsumer onRefresh) throws EngineException {
assert Objects.equals(get.uid().field(), uidField) : get.uid().field();
try (ReleasableLock lock = readLock.acquire()) {
ensureOpen();
if (get.realtime()) {
VersionValue versionValue = versionMap.getUnderLock(get.uid());
if (versionValue != null) {
if (versionValue.isDelete()) {
return GetResult.NOT_EXISTS;
}
if (get.versionType().isVersionConflictForReads(versionValue.getVersion(), get.version())) {
throw new VersionConflictEngineException(shardId, get.type(), get.id(),
get.versionType().explainConflictForReads(versionValue.getVersion(), get.version()));
}
long time = System.nanoTime();
refresh("realtime_get");
onRefresh.accept(System.nanoTime() - time);
}
}
// no version, get the version from the index, we know that we refresh on flush
return getFromSearcher(get, searcherFactory);
}
现在足以可见如果对es的更新需求特别多,首先需要考虑借助get API(依赖 _id),否则使用update_by_query还是你手写的类似语义(先search,再update)都不得不接受更新丢失的问题。