1. 双Limit问题现象与复现
最近在项目中使用MyBatis Plus做分页查询时,发现生成的SQL语句里竟然出现了两个LIMIT子句,比如LIMIT 0,10 LIMIT 0,10。这种问题会导致数据库执行报错,分页功能完全失效。经过排查,发现这是MyBatis Plus 3.4版本后一个典型的配置冲突问题。
我遇到的具体场景是这样的:项目原本使用的是3.4.1版本的MyBatis Plus,按照旧版习惯配置了PaginationInterceptor。后来引入了一个公共组件包,这个包里又自动配置了新的MybatisPlusInterceptor。启动项目后,每次分页查询都会生成重复的LIMIT语句。
用个简单的测试用例就能复现这个问题:
@Test public void testDuplicateLimit() { Page<User> page = new Page<>(1, 10); userMapper.selectPage(page, null); System.out.println(page.getRecords()); }执行后查看日志,会发现SQL变成了类似这样:
SELECT * FROM user LIMIT 0,10 LIMIT 0,102. 问题根源分析
2.1 拦截器工作机制
MyBatis的插件机制是基于责任链模式实现的。当有多个拦截器时,它们会按照配置顺序依次执行。在分页场景下,每个分页拦截器都会在SQL执行前添加自己的LIMIT逻辑。
关键问题在于:旧版的PaginationInterceptor和新版的MybatisPlusInterceptor都是独立的分页拦截器。如果两者同时存在,MyBatis会依次执行这两个拦截器,导致LIMIT子句被添加两次。
2.2 版本演进带来的变化
MyBatis Plus在3.4版本进行了重大调整:
- 3.4之前:使用单独的PaginationInterceptor
- 3.4之后:推荐使用MybatisPlusInterceptor统一管理所有插件
这个变化本意是为了统一插件管理,但如果没有及时更新旧配置,就会导致新旧拦截器共存的情况。我在项目中就是遇到了这种过渡期的问题 - 旧代码保留了PaginationInterceptor的配置,而新引入的公共包又添加了MybatisPlusInterceptor。
3. 解决方案与实践
3.1 标准解决方案
最彻底的解决方式是统一使用新的MybatisPlusInterceptor。具体配置如下:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } // 必须注释或删除旧的配置 // @Bean // public PaginationInterceptor paginationInterceptor() { // return new PaginationInterceptor(); // } }这里有几个关键点需要注意:
- 完全移除PaginationInterceptor的Bean定义
- 通过addInnerInterceptor方法添加PaginationInnerInterceptor
- 如果需要支持多种数据库,可以设置数据库类型:
new PaginationInnerInterceptor(DbType.MYSQL)3.2 临时解决方案
如果项目暂时不能完全移除旧配置,可以采用以下临时方案:
- 在公共配置类中添加条件判断:
@Bean @ConditionalOnMissingBean(MybatisPlusInterceptor.class) public MybatisPlusInterceptor mybatisPlusInterceptor() { // 配置同上 }- 或者在特定场景下手动清理分页参数:
// 在执行分页查询前 PageHelper.clearPage();不过这些方案都有局限性,建议尽快迁移到标准方案。
4. 深度排查技巧
4.1 如何确认拦截器冲突
当遇到分页问题时,可以通过以下方式确认是否是双拦截器导致:
- 查看启动日志中的Interceptor注册信息
- 使用调试模式观察SQL构建过程
- 在MyBatis配置中开启完整SQL日志:
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl4.2 其他可能引起冲突的场景
除了本文讨论的主要场景外,还有一些情况也可能导致类似问题:
- 自定义分页拦截器与官方拦截器混用
- 不同模块重复配置拦截器
- 旧版本PageHelper与MyBatis Plus混用
我曾经遇到过一个棘手案例:项目同时使用了PageHelper和MyBatis Plus,两者都修改了SQL语句,导致分页完全混乱。最终解决方案是统一使用MyBatis Plus的分页机制,彻底移除PageHelper依赖。
5. 最佳实践建议
基于多次项目实战经验,我总结出以下MyBatis Plus分页配置的最佳实践:
版本统一:确保所有模块使用相同版本的MyBatis Plus
单一配置源:在基础模块中集中配置MybatisPlusInterceptor
禁用自动配置:如果使用Spring Boot,注意自动配置的冲突
mybatis-plus.auto-config=false测试验证:编写单元测试验证分页SQL的正确性
@Test void testPaginationSQL() { String sql = userMapper.selectPage(new Page<>(2, 10), null).getSql(); assertThat(sql).contains("LIMIT 10,10"); }监控报警:在生产环境添加SQL监控,及时发现异常分页语句
6. 源码解析与原理
理解MyBatis Plus分页的实现原理,能帮助我们更好地排查问题。核心流程大致如下:
拦截器注册阶段:
// MybatisPlusInterceptor初始化 public void addInnerInterceptor(InnerInterceptor innerInterceptor) { innerInterceptors.add(innerInterceptor); }SQL拦截阶段:
// PaginationInnerInterceptor执行逻辑 public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 解析分页参数 // 修改原始SQL String newSql = dialect.buildPaginationSql(originalSql, page, boundSql); }SQL重写逻辑(以MySQL为例):
public String buildPaginationSql(String originalSql, long offset, long limit) { return originalSql + " LIMIT " + offset + "," + limit; }
当两个拦截器都执行这个流程时,自然就会出现双LIMIT问题。这也是为什么新版本要将所有拦截器统一管理的原因 - 确保每个SQL修改操作都能有序执行。
7. 常见问题排查清单
根据社区反馈和自身经验,我整理了一份双LIMIT问题的排查清单:
检查依赖版本
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>搜索项目中的PaginationInterceptor定义
检查是否有模块引入了自动配置
确认没有混用PageHelper
查看生成的SQL日志
检查Interceptor的执行顺序
确保没有自定义分页逻辑冲突
这个清单基本覆盖了90%的双LIMIT问题场景。按照这个顺序排查,通常能在10分钟内定位问题根源。
8. 项目升级指南
对于还在使用旧版MyBatis Plus的项目,建议按照以下步骤升级:
- 先升级MyBatis Plus到最新稳定版
- 全局搜索移除PaginationInterceptor
- 添加统一的MybatisPlusInterceptor配置
- 测试所有分页相关功能
- 特别注意动态数据源等特殊场景
在最近的一个微服务改造项目中,我们用了这个方案成功升级了15个服务,整个过程比较平滑。唯一遇到的坑是有一个服务自定义了方言实现,需要额外适配新的拦截器接口。
9. 性能优化建议
解决了基础功能问题后,还可以进一步优化分页性能:
使用优化后的count查询:
new PaginationInnerInterceptor().setOptimizeJoin(true)对大表使用延迟关联优化:
SELECT * FROM user INNER JOIN (SELECT id FROM user LIMIT 100000,10) AS tmp USING(id)缓存常用分页结果
考虑使用游标分页替代传统分页
这些优化在我的电商项目中,将商品列表页的加载时间从800ms降低到了200ms左右,效果非常明显。