紧急!99% 后端踩过分页坑!6 种 Java 分页方式,代码直接复制

后端开发最容易踩的坑,分页绝对排第一

你是不是也遇到过:

  • 数据库查 10 万条数据,接口直接超时 504;
  • 前端列表加载半天刷不出来,用户疯狂吐槽;
  • 大数据量查询导致 OOM,服务直接崩服!

实则这些问题,都是没选对分页方式!今天整理了 Java 开发中最常用的 6 种分页,从基础到高级,每种都附直接复制的实战代码 + 适用场景 + 避坑指南,新手也能轻松上手,提议点赞 + 收藏 + 转发,避免踩坑!

一、6 种分页方式详解,从入门到精通

1. MySQL LIMIT 分页:最简单但有坑!

原理

用 MySQL 的LIMIT offset, size语法,跳过前offset条,返回size条数据(列如LIMIT 10,20= 第 11-30 条)。

实战代码(直接复制)

// 分页参数实体

@Data

public class PageParam {

private Integer pageNum = 1; // 默认第1页

private Integer pageSize = 20; // 默认每页20条

// 计算偏移量

public Integer getOffset() {

return (pageNum – 1) * pageSize;

}

}

// Mapper接口

public interface UserMapper {

// XML:SELECT * FROM user LIMIT #{offset}, #{pageSize}

List<User> selectByPage(@Param(“offset”) Integer offset, @Param(“pageSize”) Integer pageSize);

}

// Service调用

@Service

public class UserService {

@Autowired

private UserMapper userMapper;

public List<User> getUserList(PageParam param) {

return userMapper.selectByPage(param.getOffset(), param.getPageSize());

}

}

适用场景

  • 中小数据量(≤10 万条);
  • 管理后台普通列表,不用频繁跳页。

避坑重点

❌ 深分页(列如LIMIT 100000,20)巨慢!MySQL 要先扫 100020 条再丢弃前 10 万条;

✅ 优化:搭配ORDER BY id(主键有序),让 MySQL 用索引快速定位。

2. Keyset 分页:大数据量首选,性能炸裂!

原理

也叫 “游标分页”,用前一页最后一条数据的索引值(列如 id、创建时间)当条件,不用offset,直接定位。

核心语法:WHERE id > 上一页最大id LIMIT size。

实战代码(直接复制)

// 分页参数

@Data

public class KeysetPageParam {

private Long lastId = 0L; // 上一页最大id(默认0查第一页)

private Integer pageSize = 20;

}

// Mapper接口

public interface UserMapper {

// XML:SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT #{pageSize}

List<User> selectByKeyset(@Param(“lastId”) Long lastId, @Param(“pageSize”) Integer pageSize);

}

// Service调用

public List<User> getUserListByKeyset(KeysetPageParam param) {

return userMapper.selectByKeyset(param.getLastId(), param.getPageSize());

}

适用场景

  • 大数据量(≥10 万条);
  • APP 列表下拉加载更多(不用跳页)。

核心优势

✅ 性能极致:无论多少页,查询速度都一样;

✅ 无错乱:不会因数据新增 / 删除导致重复或漏数据。

3. 游标分页:高级通用方案,支持多字段排序!

原理

Keyset 分页的升级版,支持多字段排序(列如 “创建时间降序 + id 降序”),还能加密游标,避免暴露数据库字段。

实战代码(直接复制)

// 游标加密工具类

public class CursorUtil {

// 加密:createTime + “,” + id

public static String encryptCursor(LocalDateTime createTime, Long id) {

String raw = createTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + “,” + id;

return Base64.getEncoder().encodeToString(raw.getBytes());

}

// 解密

public static String[] decryptCursor(String cursor) {

String raw = new String(Base64.getDecoder().decode(cursor));

return raw.split(“,”);

}

}

// 分页参数

@Data

public class CursorPageParam {

private String cursor; // 加密游标(空=第一页)

private Integer pageSize = 20;

}

// Mapper接口

public interface UserMapper {

// XML:WHERE create_time < #{lastCreateTime} OR (create_time = #{lastCreateTime} AND id < #{lastId})

// ORDER BY create_time DESC, id DESC LIMIT #{pageSize}

List<User> selectByCursor(

@Param(“lastCreateTime”) LocalDateTime lastCreateTime,

@Param(“lastId”) Long lastId,

@Param(“pageSize”) Integer pageSize

);

}

// Service调用

public PageResult<User> getUserListByCursor(CursorPageParam param) {

LocalDateTime lastCreateTime = LocalDateTime.MIN;

Long lastId = 0L;

// 解密游标(非第一页)

if (StringUtils.hasText(param.getCursor())) {

String[] parts = CursorUtil.decryptCursor(param.getCursor());

lastCreateTime = LocalDateTime.parse(parts[0]);

lastId = Long.parseLong(parts[1]);

}

List<User> userList = userMapper.selectByCursor(lastCreateTime, lastId, param.getPageSize());

// 生成下一页游标

String nextCursor = null;

if (userList.size() == param.getPageSize()) {

User lastUser = userList.get(userList.size() – 1);

nextCursor = CursorUtil.encryptCursor(lastUser.getCreateTime(), lastUser.getId());

}

return new PageResult<>(userList, nextCursor);

}

适用场景

  • 高并发、大数据量(电商商品列表、社交 APP 动态流);
  • 安全性要求高,不想暴露数据库字段。

4. ES 滚动分页:ES 大数据量专属!

原理

ES 的from+size深分页性能差,推荐用search_after滚动分页,基于排序字段游标加载,无资源占用。

实战代码(直接复制)

@Autowired

private ElasticsearchRestTemplate esTemplate;

public List<EsUser> searchEsUserByScroll(String lastSortValue, Integer pageSize) {

// 构建查询

NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()

.withQuery(QueryBuilders.matchAllQuery())

.withSort(SortBuilders.fieldSort(“createTime”).order(SortOrder.DESC))

.withSort(SortBuilders.fieldSort(“_id”).order(SortOrder.DESC))

.withPageable(PageRequest.of(0, pageSize));

// 非第一页添加search_after

if (StringUtils.hasText(lastSortValue)) {

queryBuilder.withSearchAfter(lastSortValue.split(“,”));

}

// 执行查询

SearchHits<EsUser> searchHits = esTemplate.search(queryBuilder.build(), EsUser.class);

List<EsUser> userList = searchHits.stream()

.map(SearchHit::getContent)

.collect(Collectors.toList());

// 生成下一页排序值

String nextSortValue = null;

if (userList.size() == pageSize) {

List<Object> sortValues = searchHits.getSearchHits().get(searchHits.getSearchHits().size() – 1).getSortValues();

nextSortValue = sortValues.get(0) + “,” + sortValues.get(1);

}

return userList;

}

适用场景

  • ES 日志检索、商品搜索;
  • 深分页(列如查第 1000 页)。

避坑重点

❌ 别用scroll分页!占用 ES 资源,适合批量导出;

✅ 实时查询用search_after,性能更优。

5. 内存分页:慎用!仅适用于极小数据量

原理

先查全量数据到内存,再用List.subList分页(风险极高!)。

代码示例(谨慎使用)

// 仅适用于≤100条数据(列如下拉框选项)

public List<User> getUserListByMemory(PageParam param) {

List<User> allUsers = userMapper.selectAll(); // 风险:数据量大必OOM

int start = (param.getPageNum() – 1) * param.getPageSize();

int end = Math.min(start + param.getPageSize(), allUsers.size());

return allUsers.subList(start, end);

}

避坑重点

❌ 绝对禁止大数据量使用!会导致内存溢出;

❌ 数据更新后需重新查全量,一致性差。

6. MyBatis-Plus 分页插件:开发效率神器!

原理

MP 的分页插件,基于 MyBatis 拦截器,自动解析Page参数,不用手动写LIMIT,支持多数据库。

实战代码(直接复制)

// 配置分页插件(Spring Boot 3.x)

@Configuration

public class MyBatisPlusConfig {

@Bean

public MybatisPlusInterceptor mybatisPlusInterceptor() {

MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;

}

}

// Mapper接口

public interface UserMapper extends BaseMapper<User> {

// 直接用Page参数,MP自动分页

Page<User> selectByAge(Page<User> page, @Param(“age”) Integer age);

}

// Service调用

public Page<User> getUserListByMP(Integer pageNum, Integer pageSize, Integer age) {

Page<User> page = Page.of(pageNum, pageSize);

return userMapper.selectByAge(page, age);

}

适用场景

  • 快速开发(原型、中小项目);
  • 多数据库适配(一套代码兼容 MySQL、Oracle)。

核心优势

✅ 开发快:不用算 offset,MP 自动处理;

✅ 功能强:支持分页总数、排序、条件过滤。

二、6 种分页方式对比,快速选型

分页方式

核心优势

适用场景

避坑要点

传统 LIMIT

简单易实现,支持跳页

中小数据量、管理后台列表

深分页性能差,避免 offset 过大

Keyset 分页

大数据量高效,无错乱

APP 下拉加载、高频分页

仅支持单字段排序,不支持跳页

游标分页

多字段排序,安全隐蔽

高并发、大数据量、实时列表

需加密游标,实现稍复杂

ES 滚动分页

适配 ES 深分页

ES 日志检索、商品搜索

实时查询用 search_after,弃用 scroll

内存分页

无需数据库支持

极小数据量(≤100 条)

禁止大数据量使用,避免 OOM

MP 分页插件

开发效率高,多库兼容

快速开发、中小项目

深分页仍需优化,依赖 MP 生态

三、分页避坑终极指南(必看!)

  1. 避免深分页:MySQL 深分页(offset>10000)优先用 Keyset / 游标分页,或限制最大页码(列如最多 100 页);
  1. 参数校验:页码pageNum≥1,每页条数pageSize限制在 10~100 之间(防恶意请求pageSize=10000);
  1. 总数优化:不需要总页数时,省略COUNT(*)查询(如下拉加载),减少数据库压力;
  1. 排序加索引:分页的ORDER BY字段必须建索引,否则全表扫描,性能极差;
  1. 分布式分页:分库分表用 Sharding-JDBC 分页插件,或基于全局主键的 Keyset 分页。

四、互动福利!分页工具包免费领

为了方便大家快速上手,我整理了「Java 分页工具包」,包含:

  • 6 种分页方式完整源码(Spring Boot+MyBatis/MP/ES);
  • 游标加密工具类;
  • 分页参数校验器;
  • 分库分表分页解决方案。

领取方式:点赞 + 收藏本文,评论区留言 “分页”,私信我领取!

最后互动

你平时用哪种分页方式?踩过哪些坑?评论区分享你的经历~

觉得有用的话,点赞 + 收藏 + 转发,让更多后端开发者避坑!关注我,下期分享分库分表分页高级技巧!

#JAVA开发##JAVA培训##技术文档编写#

© 版权声明

相关文章

1 条评论

  • 头像
    尘归离别 读者

    收藏了,感谢分享

    无记录
    回复