滁州市本地信息网

Java项目中mysql深度分页解决方案大全

2026-04-26 14:42:02 浏览次数:1
详细信息

LIMIT offset, size中offset非常大时)的性能问题,有以下几种主流解决方案:

一、问题分析

-- 传统分页:offset越大性能越差(需要扫描前offset条记录)
SELECT * FROM table ORDER BY id LIMIT 1000000, 20;

二、解决方案大全

1. 基于游标的分页(Cursor-based Pagination)

原理:记录上一页最后一条记录的位置

// API设计
GET /items?cursor=last_id&size=20&direction=next

// SQL实现
SELECT * FROM table 
WHERE id > last_id 
ORDER BY id 
LIMIT 20;

// 支持向前翻页
SELECT * FROM table 
WHERE id < first_id 
ORDER BY id DESC 
LIMIT 20;

优点

缺点

2. 覆盖索引优化

-- 原始查询(慢)
SELECT * FROM table ORDER BY create_time DESC LIMIT 1000000, 20;

-- 优化后
SELECT t.* FROM table t
INNER JOIN (
    SELECT id FROM table
    ORDER BY create_time DESC
    LIMIT 1000000, 20
) AS tmp ON t.id = tmp.id;

3. 延迟关联(Deferred Join)

@Query(nativeQuery = true, value = """
    SELECT t.* FROM (
        SELECT id FROM table
        WHERE status = :status
        ORDER BY create_time DESC
        LIMIT :offset, :size
    ) AS tmp
    INNER JOIN table t ON t.id = tmp.id
    """)
List<Entity> findWithDeferredJoin(@Param("status") int status, 
                                  @Param("offset") long offset, 
                                  @Param("size") int size);

4. 业务边界限制

// 限制最大翻页深度
public Page<T> paginate(int page, int size) {
    int maxPage = 100; // 最大允许100页
    if (page > maxPage) {
        throw new BusinessException("最多查询前" + maxPage + "页");
    }
    // 正常分页逻辑
}

5. 基于ES/Solr的搜索方案

// 使用Elasticsearch进行分页搜索
SearchRequest request = new SearchRequest("index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.from(offset);
sourceBuilder.size(size);
request.source(sourceBuilder);

// 对于深度分页,使用search_after
sourceBuilder.searchAfter(lastSortValues);
sourceBuilder.size(size);

6. 分区/分表策略

// 按时间分表查询
public List<Order> getOrders(int page, int size, Date startDate) {
    // 根据时间范围确定表名
    String tableName = getTableNameByDate(startDate);
    String sql = "SELECT * FROM " + tableName + " ORDER BY id LIMIT ? OFFSET ?";
    return jdbcTemplate.query(sql, rowMapper, size, (page-1)*size);
}

7. 物化视图/汇总表

-- 创建汇总表
CREATE TABLE summary_table (
    id BIGINT PRIMARY KEY,
    main_id BIGINT,
    summary_data VARCHAR(500),
    KEY idx_created(created_time)
) ENGINE=InnoDB;

-- 查询时先查小表
SELECT * FROM summary_table 
ORDER BY created_time DESC 
LIMIT 1000000, 20;

8. 二级缓存方案

@Component
public class PaginationCacheService {
    @Cacheable(value = "pageCache", 
               key = "'page:' + #page + ':size:' + #size + ':params:' + #paramsHash")
    public PageResult<T> getCachedPage(int page, int size, String paramsHash) {
        // 数据库查询
        return dao.findPage(page, size, params);
    }

    // 使用布隆过滤器防止缓存穿透
    public boolean mightExist(int page) {
        return bloomFilter.mightContain("page:" + page);
    }
}

9. 流式查询处理

// 使用JPA Stream
@Query("SELECT e FROM Entity e WHERE e.status = :status")
Stream<Entity> streamAllByStatus(@Param("status") String status);

// 分页处理
try (Stream<Entity> stream = repository.streamAllByStatus("active")) {
    List<Entity> pageData = stream
        .skip(offset)
        .limit(size)
        .collect(Collectors.toList());
}

10. 混合方案:游标+缓存

public class HybridPaginationService {
    private Map<String, CursorPosition> cursorCache = new ConcurrentHashMap<>();

    public PageResult<T> paginate(String cursorToken, int size) {
        CursorPosition cursor = cursorCache.get(cursorToken);
        if (cursor == null) {
            cursor = new CursorPosition(0, System.currentTimeMillis());
        }

        List<T> items = dao.findAfterCursor(cursor.getLastId(), size);

        // 生成新游标
        String newCursor = generateCursor(items.get(items.size()-1).getId());
        cursorCache.put(newCursor, new CursorPosition(
            items.get(items.size()-1).getId(),
            System.currentTimeMillis()
        ));

        return new PageResult<>(items, newCursor);
    }
}

三、方案选择建议

场景 推荐方案 说明
后台管理系统 延迟关联 + 缓存 允许一定延迟,支持跳页
C端无限滚动 游标分页 性能最优,体验好
搜索功能 ES/Solr 支持复杂查询和深度分页
报表导出 流式查询 大数据量导出
实时性要求高 覆盖索引优化 简单有效

四、完整示例:游标分页实现

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @GetMapping
    public ApiResponse<CursorPageResult<ProductDTO>> listProducts(
            @RequestParam(required = false) String cursor,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "next") String direction) {

        CursorPageResult<ProductDTO> result;
        if ("prev".equals(direction)) {
            result = productService.getPreviousPage(cursor, size);
        } else {
            result = productService.getNextPage(cursor, size);
        }

        return ApiResponse.success(result);
    }
}

@Service
public class ProductService {

    public CursorPageResult<ProductDTO> getNextPage(String cursor, int size) {
        Long lastId = parseCursor(cursor); // 解析游标

        List<Product> products;
        if (lastId == null) {
            products = productRepository.findFirstPage(size);
        } else {
            products = productRepository.findAfterId(lastId, size);
        }

        if (products.isEmpty()) {
            return CursorPageResult.empty();
        }

        String nextCursor = encodeCursor(products.get(products.size()-1).getId());
        String prevCursor = encodeCursor(products.get(0).getId());

        return CursorPageResult.of(
            products.stream().map(this::toDTO).collect(Collectors.toList()),
            nextCursor,
            prevCursor,
            products.size() == size
        );
    }

    @Repository
    public interface ProductRepository extends JpaRepository<Product, Long> {

        @Query("SELECT p FROM Product p WHERE p.id > :lastId ORDER BY p.id ASC")
        List<Product> findAfterId(@Param("lastId") Long lastId, 
                                 Pageable pageable);

        default List<Product> findAfterId(Long lastId, int size) {
            return findAfterId(lastId, PageRequest.of(0, size));
        }

        @Query("SELECT p FROM Product p ORDER BY p.id ASC")
        List<Product> findFirstPage(Pageable pageable);

        default List<Product> findFirstPage(int size) {
            return findFirstPage(PageRequest.of(0, size));
        }
    }
}

五、监控与优化建议

监控慢查询

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;

-- 分析执行计划 EXPLAIN SELECT * FROM table LIMIT 1000000, 20;



2. **索引优化原则**
- 为ORDER BY字段建立索引
- 复合索引顺序:(where条件字段, order by字段)
- 避免filesort

3. **架构层面考虑**
- 读分离,将分页查询定向到从库
- 考虑使用TiDB等分布式数据库
- 对于历史数据归档处理

选择哪种方案需要根据具体业务场景、数据量、并发量和技术栈综合考虑。通常建议优先使用游标分页,如果必须支持跳页,则使用延迟关联+缓存方案。
相关推荐