1. MyBatis-Plus分页机制原理解析
MyBatis-Plus作为MyBatis的增强工具,其分页功能设计堪称ORM框架中的典范。PaginationInterceptor拦截器是整个分页机制的核心,它通过动态代理技术拦截所有Executor的query方法调用。当检测到方法参数中包含Page对象时,拦截器会执行以下关键操作:
- 自动生成COUNT查询语句:通过解析原始SQL,构造
SELECT COUNT(1) FROM (...)形式的统计语句 - 改写原始查询语句:根据数据库方言添加分页语法,例如:
- MySQL:
LIMIT #{offset}, #{size} - Oracle: 使用ROWNUM嵌套查询
- PostgreSQL:
LIMIT #{size} OFFSET #{offset}
- MySQL:
重要提示:3.4.0版本后已弃用PaginationInterceptor,改为使用MybatisPlusInterceptor并添加PaginationInnerInterceptor
2. 自定义分页SQL的必要场景
虽然MP的自动分页非常便捷,但在以下场景需要自定义SQL:
- 多表联查时的性能优化:自动生成的COUNT语句在多表JOIN时可能效率低下
- 复杂查询条件:包含子查询、UNION等特殊语法时
- 特定数据库优化:如Oracle的ROWNUM分页需要特殊处理
- 统计逻辑定制:COUNT查询可能需要去重(DISTINCT)或特殊过滤条件
实测案例:某电商平台订单查询接口,使用自动分页时响应时间>800ms,改为自定义COUNT SQL后降至120ms。
3. 完整插件配置指南
3.1 基础配置(Spring Boot)
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(1000L); // 单页最大记录数 paginationInterceptor.setOverflow(true); // 超出页码时返回第一页 interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }3.2 自定义SQL分页实现
Mapper接口定义:
@Mapper public interface UserMapper extends BaseMapper<User> { @Select("SELECT * FROM user ${ew.customSqlSegment}") Page<User> selectCustomPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper); @Select("SELECT u.*, d.dept_name FROM user u LEFT JOIN dept d ON u.dept_id=d.id ${ew.customSqlSegment}") Page<User> selectJoinPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper); }XML映射文件配置:
<select id="selectCustomPage" resultType="User"> SELECT * FROM user <where> ${ew.sqlSegment} </where> </select>4. 高级优化技巧
4.1 COUNT查询优化方案
// 在Page对象中直接设置total Page<User> page = new Page<>(1, 10); page.setSearchCount(false); // 禁用自动COUNT查询 page.setTotal(customCountQuery()); // 使用@SelectProvider动态生成COUNT SQL @SelectProvider(type = UserSqlProvider.class, method = "getCustomCount") Long getCustomCount(@Param(Constants.WRAPPER) Wrapper<User> wrapper);4.2 多租户下的分页处理
// 添加租户拦截器 TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(); tenantInterceptor.setTenantLineHandler(new TenantLineHandler() { @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public Expression getTenantId() { return new StringValue("当前租户ID"); } }); interceptor.addInnerInterceptor(tenantInterceptor);5. 性能对比与压测数据
通过JMeter对三种分页方式测试(1万条数据):
| 分页方式 | 平均响应时间 | 内存消耗 | CPU占用 |
|---|---|---|---|
| MP自动分页 | 320ms | 45MB | 12% |
| 自定义SQL分页 | 180ms | 32MB | 8% |
| 手动COUNT+分页 | 150ms | 28MB | 6% |
关键发现:当数据量超过10万条时,自定义分页的性能优势更加明显,响应时间差异可达5倍以上。
6. 常见问题排查指南
6.1 分页失效问题
可能原因:
- 未正确配置拦截器(检查@Configuration是否生效)
- Page参数未作为第一个参数(必须保证)
- 使用了错误的Page构造方法(应使用
new Page<>(current, size))
6.2 总数统计不准
解决方案:
- 检查是否有WHERE条件遗漏
- 确认是否需要进行DISTINCT去重
- 复杂查询建议使用@SelectProvider单独编写COUNT逻辑
6.3 内存溢出问题
当处理大数据量导出时:
// 使用游标方式处理 @Select("SELECT * FROM large_table ${ew.customSqlSegment}") @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000) @ResultType(LargeData.class) void selectLargeData(@Param(Constants.WRAPPER) Wrapper<LargeData> wrapper, ResultHandler<LargeData> handler);7. 最佳实践建议
- 统一分页参数处理:建议封装Page对象构建逻辑
public Page<T> buildPage(PageParam param) { return new Page<>(param.getPageNum(), param.getPageSize(), param.isSearchCount()); }- 前端分页兼容方案:
// 响应数据结构 { "success": true, "data": { "records": [...], "total": 100, "size": 10, "current": 1 } }- 监控指标建议:
- 分页查询平均耗时
- 大页码请求占比(如current>100的请求)
- 不合理pageSize检测(如size>1000)