Elasticsearch深度分页全解:从原理到跳页实战

数据库 其他数据库
在数据爆炸的时代,分页查询已成为系统标配功能。但当面对亿级数据量时,传统的分页方式却可能成为压垮系统的最后一根稻草。本文将深入剖析Elasticsearch深度分页的成因,并提供常见的解决方案.

在数据爆炸的时代,分页查询已成为系统标配功能。但当面对亿级数据量时,传统的分页方式却可能成为压垮系统的最后一根稻草。本文将深入剖析Elasticsearch深度分页的成因,并提供常见的解决方案.

1.深度分页:你以为的翻页,其实是性能炸弹

分页机制剖析

当使用from+size进行分页时,Elasticsearch的处理流程暗藏隐患

# 典型分页请求示例
GET /orders/_search
{
  "from": 1000,
  "size": 10,
  "sort": [{"create_time": "desc"}]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

处理流程

  • 客户端请求第N页数据(from = 1000, size = 10)
  • 协调节点向所有分片广播查询请求
  • 每个分片在内存中计算排序,准备前1010条结果
  • 合并所有分片返回的1010×分片数次数据
  • 最终截取第1000-1010条数据返回客户端

性能灾难三宗罪

# 查看默认最大分页限制
GET /_settings?include_defaults
# 输出结果片段
"index.max_result_window" : "10000"
  • 1.
  • 2.
  • 3.
  • 4.

致命影响

  • 内存黑洞:翻到第1000页时,单个分片需处理1000×size数据量
  • 网络风暴:分片数×数据量的跨节点传输消耗
  • 响应悬崖:页码超过max_result_window(默认1w)时直接报错

2.破局之道:Search After与Scroll API原理解析

Search After(游标分页核心原理)

graph LR
    A[请求第一页] --> B(返回排序值游标)
    B --> C[携带游标请求下一页]
    C --> D{是否还有数据}
    D -- 是 --> C
    D -- 否 --> E[结束查询]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

技术本质

  • 基于上一页最后一条记录的排序值进行分页
  • 避免全局排序,仅保持单次查询的顺序一致性
  • 时间复杂度稳定为O(size)

适用场景

  • 移动端瀑布流浏览
  • 后台连续分页查询
  • 需要实时性的分页需求

Spring Boot实现

// 构建基础查询
SearchSourceBuilder builder = new SearchSourceBuilder()
    .size(10)
    .sort(SortBuilders.fieldSort("create_time").order(SortOrder.DESC))
    .sort(SortBuilders.fieldSort("_id")); // 保证排序唯一性
// 设置search_after参数
if (lastCreateTime != null && lastId != null) {
    builder.searchAfter(new Object[]{lastCreateTime, lastId});
}
SearchRequest request = new SearchRequest("orders")
    .source(builder);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

Scroll API(滚动查询)

核心原理

graph TB
    A[初始化Scroll] --> B(获取scroll_id)
    B --> C[使用scroll_id分批获取]
    C --> D{是否完成}
    D -- 否 --> C
    D -- 是 --> E[清除Scroll上下文]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

技术本质

  • 创建查询的快照视图
  • 通过保持搜索上下文实现批次获取
  • 适合非实时的大数据量处理

适用场景

  • 全量数据导出
  • 离线数据分析
  • 大数据迁移场景

Spring Boot实现

// 初始化滚动查询
SearchRequest request = new SearchRequest("orders");
request.scroll(TimeValue.timeValueMinutes(1L)); // 保持上下文1分钟
// 后续获取批次数据
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueSeconds(30L));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

3.跳页难题解决方案

跳页问题本质剖析

graph TD
    A[用户请求第N页] --> B{是否缓存过位置}
    B -- 是 --> C[直接使用缓存游标]
    B -- 否 --> D[估算近似位置]
    D --> E[二次校准查询]
    E --> F[返回精确结果]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

技术挑战

  • 无法直接定位到任意偏移量
  • 传统分页方式性能不可接受
  • 需要平衡准确性与性能

混合分页策略

public SearchResult queryProducts(int targetPage) {
    if (targetPage <= 100) {
        return traditionalPaging(targetPage); // 传统分页
    } else if (targetPage <= cachedMaxPage) {
        return searchAfterWithCache(targetPage); // 带缓存的search_after
    } else {
        return timeRangeFilterPaging(targetPage); // 时间范围过滤分页
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

跳页缓存层设计

// Redis存储分页快照
public void cachePageSnapshot(int pageNum, Object[] searchAfterValues) {
    String key = "product_list:page:" + pageNum;
    redisTemplate.opsForValue().set(key, searchAfterValues, 5, TimeUnit.MINUTES);
}
// 获取缓存游标
public Object[] getCachedSnapshot(int pageNum) {
    String key = "product_list:page:" + pageNum;
    return (Object[]) redisTemplate.opsForValue().get(key);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

4.性能优化全景方案

实测数据对比

分页方式

10页耗时

100页耗时

1000页耗时

内存消耗

From/Size

120ms

450ms

超时失败

Search_After

80ms

85ms

90ms

Scroll

100ms

110ms

120ms

测试环境:3节点集群,单个索引10个分片,500万测试数据

5.最佳实践指南

前端设计原则

  • 使用无限滚动替代传统分页
  • 提供精准过滤条件
  • 展示总条数范围而非精确值

后端防御策略

// 分页参数校验
public void validatePageParams(int page, int size) {
    if (page > MAX_ALLOWED_PAGE) {
        throw new BusinessException("超出最大可查询页数");
    }
    if (size > 100) {
        throw new BusinessException("单页数量不可超过100");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

监控预警方案

# 监控search_after上下文
GET _nodes/stats/indices/search?filter_path=**.open_contexts
# 检查scroll上下文
GET _nodes/stats/indices/search?filter_path=**.scroll_current
  • 1.
  • 2.
  • 3.
  • 4.

当面对强制要求的精确跳页场景时,可考虑预计算+二级缓存方案。通过定时任务预先建立热点页的游标映射表,结合短时缓存实现快速跳转。您在实际项目中是如何解决这个难题的?

责任编辑:武晓燕 来源: JAVA充电
相关推荐

2022-02-28 10:05:12

组件化架构设计从原组件化模块化

2020-04-28 22:12:30

Nginx正向代理反向代理

2025-04-01 01:04:00

Redis集群缓存

2025-03-14 12:30:00

Redis RDBRedis数据库

2021-05-11 07:51:30

React ref 前端

2021-10-04 09:14:18

ElasticSear深度分页

2024-07-07 21:49:22

2009-11-11 10:21:25

路由选择协议

2018-05-17 15:18:48

Logistic回归算法机器学习

2025-04-02 07:29:14

2023-05-15 08:32:45

2018-03-30 18:17:10

MySQLLinux

2024-03-27 10:14:48

2010-06-29 14:20:52

2023-08-03 08:03:05

2023-02-07 08:55:04

进程栈内存底层

2021-12-13 06:56:46

漏洞Log4j对象

2021-04-23 10:55:52

人工智能深度学习

2017-07-06 11:34:17

神经形态计算人工智能突触

2017-06-16 16:58:54

机器学习神经形态架构
点赞
收藏

51CTO技术栈公众号