1. MybatisPlus分页机制深度解析
作为Java持久层框架的增强工具,MybatisPlus的分页功能设计堪称其最亮眼的特性之一。我曾在多个百万级数据量的生产环境中验证过其稳定性,相比传统Mybatis手写分页SQL的方案,它至少能减少80%的样板代码。其核心原理是通过ThreadLocal绑定分页参数,在执行SQL前自动拦截并重写语句。
1.1 基础分页实现
新建Page对象时需注意三个关键参数:
java复制Page<User> page = new Page<>(current, size); // current从1开始
- current:当前页码(从1开始计数)
- size:每页记录数
- isSearchCount:是否进行count查询(大数据量时可关闭)
实际查询只需一行代码:
java复制IPage<User> result = userMapper.selectPage(page, queryWrapper);
返回的result对象包含:
- records:当前页数据列表
- total:总记录数(isSearchCount为true时)
- pages:总页数
踩坑提示:当使用left join时count语句会出错,需要自定义分页SQL
1.2 性能优化实践
当单表数据超过500万时,我总结出这些优化方案:
- count优化:
java复制page.setOptimizeCountSql(true); // 默认已开启
这会自动将SELECT COUNT(*)优化为SELECT COUNT(1),并在left join场景下去除不必要的表关联
- 大数据量分页:
java复制page.setSearchCount(false); // 禁用count查询
List<User> list = userMapper.selectList(page, wrapper);
适合不需要总页数的场景,比如移动端上拉加载
- ID分段查询:
java复制wrapper.ge("id", startId).lt("id", endId);
配合有序ID使用,比LIMIT offset方案快10倍以上
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 突破单页500条限制的实战方案
最近在物联网项目中处理设备日志时,遇到了单页500条的限制问题。通过分析源码发现这是Page类的默认限制:
java复制// 源码片段
protected long size = 10;
protected long maxLimit = 500L;
2.1 官方方案调整
有三种修改方式:
- 全局配置(推荐):
yaml复制mybatis-plus:
global-config:
db-config:
max-limit: 1000
- 单次查询覆盖:
java复制Page<User> page = new Page<>(1, 1000);
page.setMaxLimit(1000L);
- 继承自定义Page类:
java复制public class UnlimitedPage<T> extends Page<T> {
public UnlimitedPage() {
setMaxLimit(Long.MAX_VALUE);
}
}
2.2 大数据量分页方案
当需要导出数万条数据时,建议采用游标分页:
java复制// 使用流式查询
try (Cursor<User> cursor = userMapper.selectCursor(wrapper)) {
cursor.forEach(user -> {
// 处理每条数据
});
}
配合自定义拦截器实现内存分页:
java复制@Intercepts(@Signature(type= ResultSetHandler.class, method="handleResultSets", args={Statement.class}))
public class MemoryPageInterceptor implements Interceptor {
// 实现内存分页逻辑
}
3. 高阶分页场景解决方案
3.1 多表联查分页
在订单关联商品查询时,常规分页会出现问题。解决方案:
- 使用@SqlParser注解:
java复制@SqlParser(filter = true)
@Select("SELECT o.*,p.name FROM order o LEFT JOIN product p ON o.pid=p.id ${ew.customSqlSegment}")
IPage<OrderVO> selectOrderPage(Page page, @Param(Constants.WRAPPER) Wrapper wrapper);
- 子查询方案:
java复制@Select("SELECT * FROM order WHERE id IN (SELECT id FROM (SELECT id FROM order ${ew.customSqlSegment} LIMIT #{page.offset},#{page.size}) temp)")
List<Order> selectSubQueryPage(Page page, @Param(Constants.WRAPPER) Wrapper wrapper);
3.2 动态表名分页
在 SaaS 多租户系统中,我这样实现动态表名分页:
- 实现动态表名处理器:
java复制public class TenantTableNameHandler implements TableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
return getTenantTable(tableName);
}
}
- 配置拦截器:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(new TenantTableNameHandler()));
return interceptor;
}
4. 分页性能监控与调优
4.1 慢分页SQL定位
通过自定义拦截器记录分页SQL执行时间:
java复制public class PagePerformanceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long cost = System.currentTimeMillis() - start;
if(cost > 500) { // 超过500ms记录警告
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
log.warn("Slow page query: {} cost {}ms", ms.getId(), cost);
}
return result;
}
}
4.2 分页缓存策略
对于高频访问的分页数据,采用二级缓存方案:
- 开启Mybatis二级缓存:
xml复制<cache eviction="LRU" flushInterval="60000" size="1024"/>
- 自定义分页缓存键:
java复制public class PageCacheKey extends CacheKey {
private final Page<?> page;
public PageCacheKey(Page<?> page, Object... objects) {
super(objects);
this.page = page;
}
@Override
public boolean equals(Object obj) {
// 自定义分页参数比较逻辑
}
}
5. 特殊分页需求实现
5.1 字段加解密分页
结合加密拦截器实现安全分页:
- 实现加密拦截器:
java复制public class EncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 查询结果解密
Object result = invocation.proceed();
if(result instanceof Page) {
decryptPage((Page<?>) result);
}
return result;
}
}
- 条件加密处理:
java复制wrapper.apply("AES_DECRYPT(name, 'key') = {0}", "张三");
5.2 分页结果后处理
对分页数据进行二次加工:
java复制public <T> IPage<T> processPage(IPage<T> page, Function<T, T> processor) {
page.getRecords().replaceAll(processor);
return page;
}
// 使用示例
IPage<User> result = processPage(rawPage, user -> {
user.setName(maskName(user.getName()));
return user;
});
6. 最新版本特性适配
6.1 SpringBoot3兼容方案
在SpringBoot3环境中需注意:
- 依赖版本要求:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2+</version>
</dependency>
- 自动分页配置变化:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
6.2 与Kafka的整合实践
实现分页数据实时推送:
java复制public IPage<User> selectPageWithKafka(Page<User> page, Wrapper<User> wrapper) {
IPage<User> result = userMapper.selectPage(page, wrapper);
kafkaTemplate.send("user-page-topic",
new PageMessage(result.getRecords(), result.getTotal()));
return result;
}
7. 生产环境问题排查指南
7.1 常见异常处理
- 分页失效:
- 检查是否配置了PaginationInnerInterceptor
- 确认SQL中没有limit手动拼接
- 总数不准:
- 检查是否有left join未处理
- 确认wrapper条件正确性
- 内存溢出:
- 避免单次查询过大分页size
- 使用游标方式替代
7.2 日志调试技巧
开启SQL日志分析:
yaml复制logging:
level:
com.baomidou.mybatisplus: DEBUG
典型日志分析:
log复制DEBUG --- [io-8080-exec-1] c.b.m.e.p.i.PaginationInnerInterceptor : 原始SQL: SELECT id,name FROM user
DEBUG --- [io-8080-exec-1] c.b.m.e.p.i.PaginationInnerInterceptor : 分页SQL: SELECT id,name FROM user LIMIT 0,10
DEBUG --- [io-8080-exec-1] c.b.m.e.p.i.PaginationInnerInterceptor : 总数SQL: SELECT COUNT(1) FROM user
8. 扩展开发建议
8.1 自定义分页插件
实现分布式环境下的分页:
java复制public class DistributedPaginationInterceptor extends PaginationInnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms,
Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) {
// 从分布式缓存获取total
Long total = redisTemplate.opsForValue().get(buildTotalKey(ms, parameter));
if(total != null) {
((Page<?>) parameter).setTotal(total);
return;
}
super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
}
8.2 前端分页适配方案
统一分页响应格式:
java复制public class PageResult<T> {
private long current;
private long size;
private long total;
private List<T> records;
public static <T> PageResult<T> success(IPage<T> page) {
PageResult<T> result = new PageResult<>();
result.setCurrent(page.getCurrent());
result.setSize(page.getSize());
result.setTotal(page.getTotal());
result.setRecords(page.getRecords());
return result;
}
}
在前后端分离项目中,这种标准化响应能减少30%以上的对接沟通成本。实际开发中,我还会在PageResult中添加success标志和错误信息字段,形成完整的响应协议。
