紧急!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 生态 |
三、分页避坑终极指南(必看!)
- 避免深分页:MySQL 深分页(offset>10000)优先用 Keyset / 游标分页,或限制最大页码(列如最多 100 页);
- 参数校验:页码pageNum≥1,每页条数pageSize限制在 10~100 之间(防恶意请求pageSize=10000);
- 总数优化:不需要总页数时,省略COUNT(*)查询(如下拉加载),减少数据库压力;
- 排序加索引:分页的ORDER BY字段必须建索引,否则全表扫描,性能极差;
- 分布式分页:分库分表用 Sharding-JDBC 分页插件,或基于全局主键的 Keyset 分页。
四、互动福利!分页工具包免费领
为了方便大家快速上手,我整理了「Java 分页工具包」,包含:
- 6 种分页方式完整源码(Spring Boot+MyBatis/MP/ES);
- 游标加密工具类;
- 分页参数校验器;
- 分库分表分页解决方案。
领取方式:点赞 + 收藏本文,评论区留言 “分页”,私信我领取!
最后互动
你平时用哪种分页方式?踩过哪些坑?评论区分享你的经历~
觉得有用的话,点赞 + 收藏 + 转发,让更多后端开发者避坑!关注我,下期分享分库分表分页高级技巧!
#JAVA开发##JAVA培训##技术文档编写#

收藏了,感谢分享