SpringBoot整合SpringDataElasticsearch
ElasticSearch的scroll滚动查询以及在Springboot中的使用
ES三种查询
面试官:Mysql千万级大表如何进行深度分页优化?
问题描述
在分页查询中,当查询数据总量超过10000时,es为了避免大量数据加载到内存导致内存溢出默认情况下会加限制最大1w条
当数量超过的时候会提示异常:
org.springframework.data.elasticsearch.RestStatusException: Elasticsearch exception [type=search_phase_execution_exception, reason=all shards failed]; nested exception is ElasticsearchStatusException[Elasticsearch exception [type=search_phase_execution_exception, reason=all shards failed]]; nested: ElasticsearchException[Elasticsearch exception [type=illegal_argument_exception, reason=Result window is too large, from + size must be less than or equal to: [10000] but was [52030]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.]]; nested: ElasticsearchException[Elasticsearch exception [type=illegal_argument_exception, reason=Result window is too large, from + size must be less than or equal to: [10000] but was [52030]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.]];
这是因为官方为了保证es性能和稳定性,规定每次查询最大数量为1w。
解决方案
可以使用滚动查询(Scroll API)来解决这个问题。其实原理也单,就是将本次查询最后id当作下次查询条件,一直轮询,直到查询没数据就返回。
public void page(Req req) {
NativeSearchQuery nsq = new NativeSearchQueryBuilder()
//取消es中最大10000条限制
.withTrackTotalHits(Boolean.TRUE)
.withQuery(assemblePageBoolQueryBuilder(req))
.withSorts(SortBuilders.fieldSort("created_date").order(SortOrder.DESC))
.withPageable(PageRequest.of(req.getPage(), req.getRows())).build();
SearchScrollHits<MonitorOrderSearchEsDto> search = elasticsearchRestTemplate.searchScrollStart(60000, nsq, MonitorOrderSearchEsDto.class, ES_INDEX);
//滚动id,记录当前查询的终止位置
String scrollId = search.getScrollId();
//快照在es缓存中保存时长,自定义
long scrollTimeInMillis = 60 * 1000;
//滚动次数,模拟分页数(page)
int scrollTimes = 0;
//当滚动查询无数据返回 或 滚动次数大于分页数,不再查询
while (search.hasSearchHits() && scrollTimes < req.getPage()) {
search = elasticsearchRestTemplate.searchScrollContinue(scrollId, scrollTimeInMillis, MonitorOrderSearchEsDto.class, ES_INDEX);
//记录每次的滚动id
scrollId = search.getScrollId();
scrollTimes = scrollTimes + 1;
}
//因为es每次滚动查询会生成快照,需要清除当前滚动id
elasticsearchRestTemplate.searchScrollClear(Collections.singletonList(scrollId));
//业务处理...
}
- 60000,只是一个示例的时间数据,该参数表明查询结果在es中保存时效时间
- 需分别调用3个方法 searchScrollStart,searchScrollContinue,searchScrollClear
总结:
- 滚动查询是建立在普通查询基础上的
- 滚动查询相当于快照,如果在使用scroll进行滚动查询期间有所增删改操作,那么查询结果不会同步最新
解决方案2
为避免深度分页,查询接口可以将上次查询结果的最后一条数据作为参数滚动查询,查询下一页时会为入参注入”latestRow”字段,该对象为上次查询结果的最后一条数据.
同时需要设置id为有序。因为每次查询的结果集都是有序的并且在查询参数中加入latestRow, 因此会把上次的查询结果集给过滤了。
在查询方法参数里加入latestRow作为比较参数,以上次查询的最后一条数据作为起点,往后查询。
public ResponseInfo<List<EsDto>> export(ExportReq req) {
ResponseInfo<List<EsDto>> responseInfo = new ResponseInfo<>();
NativeSearchQuery nsq = new NativeSearchQueryBuilder()
.withQuery(assemblePageBoolQueryBuilder(req))
.withSorts(SortBuilders.fieldSort("id").order(SortOrder.DESC))
.withPageable(PageRequest.of(0,10000)).build();
SearchHits<EsDto> search = elasticsearchOperations.search(nsq, EsDto.class, ES_INDEX);
//业务处理...
}
很巧妙的符合了 max_result_window 的要求最大1w的要求,将每次查询效率最大化。
同时也避免了深分页的问题。