先说个真实场景上周同事小周丢过来一段代码说MybatisPlus分页出邪门问题了——pageSize传1000查出来只有500条传5000结果还是500条。更气人的是换了个查询方法又变成全表数据一起返回分页完全没生效。一说这个群里好几个老哥都冒出来有人说遇到过单页500条限制有人说分页莫名失效最后发现是拦截器没配。这两个问题大概是MybatisPlus实战里出现频率最高的两个翻车点而且表现形式特别容易混淆一个是不分页一个是单页被钳制。这篇就专门聊聊这两个问题背后的原理、完整的排查思路以及怎么一次性避开。适合刚接入MybatisPlus的新手看也适合那种项目分页突然坏了不知道从哪查起的老手直接对照排查。1. 分页不是引入依赖就能用先搞清楚PaginationInnerInterceptor在干嘛MybatisPlus的分页插件核心是一个叫PaginationInnerInterceptor的拦截器组件。它的作用本质上就是拦截Mybatis框架已经解析好的SQL把它改写成两条SQL一条是SELECT COUNT(*)的count查询用来算总条数另一条是拼接了LIMIT、OFFSET的分页查询用来查当前页数据。改写完之后再把结果塞回Page对象里你就能拿到records、total、current这些字段了。你可以把MybatisPlusInterceptor想象成一个小区门口的保安亭PaginationInnerInterceptor是其中一个值班保安。保安亭里可以同时站好几个保安比如租户拦截器、乐观锁拦截器、分页拦截器。但前提是保安得真的站在亭子里他才会上前拦车检查。很多人的项目分页失效问题就出在这——代码里只引了mybatis-plus-boot-starter依赖但从来没往保安亭里安排分页保安。MybatisPlus在3.4.0版本之后官方重构了插件机制分页能力必须通过MybatisPlusInterceptor显式注册才能生效。这也是很多老项目在升级依赖之后突然分页失效的根本原因——以前老版本里可能某个自动配置帮你把分页弄好了升级之后机制变了你没跟上分页就罢工了。标准的注册方式是这样Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }这里有个细节非常值得注意DbType一定要和你实际用的数据库对上。你连的是MySQL就写DbType.MYSQL连的是PostgreSQL就写DbType.POSTGRE_SQL。如果写错了分页插件生成的方言SQL就是错的轻则分页数据不对重则直接SQL报错。我见过有同事在Oracle项目里配了MySQL方言跑出来的分页SQL带着LIMIT关键字Oracle根本不认直接报错排查了半天。还有一种配置了但等于没配的情况Configuration类放在了Spring Boot启动类所在包的扫描范围之外。Spring Boot默认只扫描启动类所在包以及它的子包如果你的MybatisPlusConfig放到了别的目录层级里容器压根不会加载它Bean自然也就不会执行。这种问题从代码上看非常无辜——注解、配置类、Bean方法全都写对了但就是没生效。排查方式很简单在配置类的构造函数里打一行日志启动项目的时候看这行日志打没打出来。没打出来那肯定是类没被扫描进容器直接挪位置或者加ComponentScan指定扫描路径就行。2. 分页失效的完整排查链按顺序来十分钟定位问题分页失效是个特别笼统的现象病根可能完全不同。如果你一上来就瞎改配置大概率会越改越乱。我的建议是按照下面这个顺序一步步验证每验证一步就能排除一类原因。第一步把SQL日志打开先看执行的SQL到底长什么样。在application.yml里加上这段配置mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl然后重启项目翻一下控制台日志。重点是看查询语句后面有没有LIMIT。如果SQL是完整地、不带任何前缀地把全表查询打出来了说明拦截器压根没介入——问题出在拦截器注册或配置上。如果SQL日志里能看到类似LIMIT 1,10这样的片段说明分页插件已经工作了那问题就不在于没分页而在于其他环节比如total不对、page参数没对上等等。这一步能快速把问题劈成两半非常关键。第二步检查拦截器是否真的注册了。确认配置类存在确认PaginationInnerInterceptor确实通过addInnerInterceptor加到了MybatisPlusInterceptor里再确认配置类在Spring的扫描范围内。一个很实用的土办法在MybatisPlusInterceptor的Bean方法里加一行System.out.println(分页拦截器已注册)启动时看有没有输出。没输出就往扫描路径上找原因。第三步检查Page对象到底有没有传进Mapper方法。这是新手最容易踩的坑而且代码看起来极其无辜// 错误写法Page对象创建了但压根没传给Mapper PageUser page new Page(1, 10); ListUser userList userMapper.selectList(null);你以为new了一个Page分页就自动生效了不是的。MybatisPlus的分页机制有一个隐含约定Mapper的方法参数里必须包含Page或IPage类型的对象拦截器才能从参数里拿到current和sizeSQL改写的时候才有数据可拼。正确写法是// 正确写法把Page作为Mapper方法的参数传进去 PageUser page userMapper.selectPage(new Page(1, 10), null);如果你用自定义SQLMapper接口的方法签名要这样写IPageUserVO selectUserPage(PageUserVO page, Param(status) Integer status);对应的Mapper.xml里不需要任何LIMIT插件会自动拼select idselectUserPage resultTypecom.example.UserVO SELECT * FROM user WHERE status #{status} /select注意SQL里千万别自己写死LIMIT。插件会在你写的SQL后面再拼一个LIMIT如果XML里已经有一个就会变成LIMIT 10 LIMIT 0,10这种语法错误或者顺序错乱导致数据完全不对。这是自定义SQL分页里非常容易翻车的点。第四步检查count查询有没有出错。如果日志显示分页查询本身带了LIMIT但返回的total数字不对那问题多半出在MybatisPlus自动生成的count SQL上。插件生成count语句时一般做法是把原SQL中的SELECT 字段列表替换成SELECT COUNT(*)但对于一些复杂SQL比如含DISTINCT、GROUP BY、UNION、多表LEFT JOIN的情况自动生成的结果往往是不对的。这里补一张排查对照表方便你对症下手现象可能原因验证方式SQL不带LIMIT全表返回拦截器没注册或Page没传进Mapper看SQL日志、检查配置类、检查方法参数SQL带LIMIT但total不对自动count SQL写错或JOIN导致计数重复打印日志看COUNT语句单页数量被封顶比如永远只有500条PaginationInnerInterceptor设置了maxLimit检查配置调用getMaxLimit()查看分页查询很慢越往后翻越慢深分页OFFSET太大改用游标分页或延迟关联排查的时候对着这张表走一遍基本能覆盖90%的分页异常场景。剩下的10%就是下面要单独拿出来说的500条限制这种隐蔽配置。3. 单页500条限制到底是谁干的一个被名字耽误的安全机制网上搜mybatisplus单页500条限制能搜出一堆求助帖。有意思的是MybatisPlus本身并没有写死只能查500条这个限制通常只可能是被人为配置出来的。罪魁祸首就是PaginationInnerInterceptor里的maxLimit属性PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(500L); interceptor.addInnerInterceptor(paginationInnerInterceptor);一旦配置了setMaxLimit(500L)不管前端传的pageSize是1000、5000还是10000分页插件内部会把单页条数强制钳制到500。原理很简单插件在处理分页参数时会做一次size Math.min(pageSize, maxLimit)的运算把过大的每页条数压到上限值。所以你的接口永远只能返回500条不是数据库的问题也不是网络问题是配置层的限制。那官方为什么要设计这个参数说白了这是一个非常实用的自我保护机制。你想如果接口不小心暴露到了公网有人写个脚本疯狂请求你的分页接口每次把pageSize传成9999999数据库为了执行LIMIT 9999990, 10这种语句需要先扫描近一千万行数据再丢掉CPU和IO直接被打满。maxLimit的作用就是在业务层面给单页数据量做一个保险丝防止这类请求拖垮数据库。所以很多团队的安全规范里会明确要求给分页插件设置一个单页上限500是最常见的约定值之一。如果你确认业务确实需要单页返回超过500条那就得解除这个限制。解除方式也简单把maxLimit设置成-1就行paginationInnerInterceptor.setMaxLimit(-1L);设置成-1之后插件就不再对单页条数做钳制了。不过我建议除非你确定自己的接口不会被打否则别完全放开宁可设置成一个更合理的业务上限比如10000也不要设置成无限制。我看到过太多解除限制之后接口被人刷爆的案例。再补一个容易混淆的知识点如果你发现前端表格只显示了500行但接口返回的数据条数其实是对的那问题可能不在MybatisPlus而在前端组件。很多前端表格框架默认渲染500行或者你使用的数据库管理工具比如Navicat、DataGrip默认查询结果也只显示500行。这三种500条是完全不同层面的问题场景数据是否已查出解决方向MybatisPlusmaxLimit限制未查出SQL里的LIMIT就是500修改setMaxLimit(-1L)或调大上限前端表格默认渲染限制已查出接口返回完整调整前端表格组件分页配置数据库管理工具显示限制已查出工具只显示前500行修改工具偏好设置排查的时候先用接口测试工具比如Postman直接调一下接口看返回的JSON里records数组的长度。如果接口返回的条数本来就超过500条那和MybatisPlus一点关系都没有问题在前端。另外即使你解除了500条限制我也得提醒一句深分页真正的敌人不是单页条数上限而是翻页深度。LIMIT 490000, 10这条SQLMySQL照样得先读前490010行再把前490000行丢掉最后给你返回10行。翻得越深性能越差。如果业务确实需要翻很多页更合理的方案是改用游标分页也就是记住上一页最后一条记录的ID下一页只查ID大于它的记录。或者用延迟关联先通过覆盖索引查出当前页的主键再回表拿完整数据SELECT t.* FROM user t INNER JOIN ( SELECT id FROM user WHERE status 1 ORDER BY id LIMIT 490000, 10 ) tmp ON t.id tmp.id ORDER BY t.id;这样MySQL在子查询里只用索引扫描不需要回表性能会好非常多。这个话题以后再展开这里先记住结论500条上限可以解除但深分页的性能问题靠的是方案设计不是无脑调大pageSize。4. 还有几个分页翻车场景一个个排雷排查过上面两大类问题之后项目里的分页基本能稳定运行了。但实战里还有几个非常隐蔽的翻车点我平时帮人看代码的时候没少遇到这里一并列出来。场景一多表关联查询分页total统计重复。比如查询用户列表需要LEFT JOIN用户的角色表、部门表。如果用户有多条角色记录JOIN之后会产生多行MybatisPlus自动生成的count统计会把关联出来的重复行也算进去导致total虚高。解决办法有两个方向一是把自动count换成自定义count在Mapper里单独提供一个count方法让分页插件优先用它二是调整SQL写法在子查询里先聚合。我个人更推荐自定义count的方式逻辑清晰也方便维护。select idselectUserRolePage resultTypecom.example.vo.UserVO SELECT u.id, u.name, r.role_name FROM user u LEFT JOIN role r ON u.role_id r.id WHERE u.status #{status} /select select idselectUserRolePageCount resultTypejava.lang.Long SELECT COUNT(*) FROM user u WHERE u.status #{status} /select场景二Page对象被复用导致翻页错乱。有些同学会在循环里反复使用同一个Page对象比如做批量导出的时候想着每循环一次自动翻一页结果翻来翻去查出来的都是第一页。这是因为分页插件每次都从Page对象里读取当前的current和size你复用同一个Page它不会自己帮你累加页码。正确做法是每次查询都新建一个Page或者在循环里显式调用page.setCurrent(current)更新页码。这个问题的根源还是对Page对象的生命周期没理解透它只是一个承载分页参数的普通对象不是分页游标。场景三分页查询没有稳定的ORDER BY出现数据重复或丢失。MySQL在未指定排序字段的情况下返回顺序是不保证的。两次查询同一页数据如果中间恰好有数据写入返回结果可能就不一致。对于分页功能来说这是一个很严重的隐患——用户在第一页看到的数据刷新一下跑到了第二页。解决方式很简单分页查询一定要显式指定ORDER BY而且最好用主键或者有唯一索引的字段不要只按创建时间排因为时间可能重复。选择排序字段时优先考虑唯一性稳定的字段。场景四导出大表数据时反复分页内存溢出。分页查询本身是好的但如果用来导出几十万条数据每次都把List攒到内存里最后一起写文件很容易把JVM堆内存撑爆。这种场景更适合使用MybatisPlus的游标查询能力——它基于JDBC的流式读取每次只从数据库取少量数据处理完一批再取下一批不会一次性把全量数据加载到内存。游标查询的具体用法是返回类型用CursorT注意使用完后要关闭游标释放数据库连接这个细节很多人会漏掉。最后给一张自检清单你可以直接保存下来下次分页出问题的时候逐项核对拦截器里是否注册了PaginationInnerInterceptorDbType是否和数据库匹配Page对象是否作为参数传入了Mapper方法maxLimit是否被设置成了不合理的值复杂SQL的count统计是否准确分页查询是否指定了稳定的排序字段。我在实际项目里看过太多分页相关的bug总结下来大部分不是框架的问题而是某个环节的约定没对齐——要么是配置漏了要么是参数没传对要么是被某个隐蔽的配置限制住了。尤其是maxLimit这个参数它被设置的时候往往是出于好心但时间一长团队里没人记得这一茬排查起来特别费劲。所以建议你接手一个老项目的时候第一件事就是把分页插件的配置打印出来看一眼getMaxLimit()这个方法可比你对着控制台猜半天快多了。
企业数字化 ERP 产品动态
相关推荐
SpringBoot+Vue医院挂号系统实战:防超卖与数据库设计 1. 一个挂号系统的业务边界:别把毕设做成“伪需求堆砌”1.1 患者、医生、管理员三类角色各管什么我先说一个很常见的现象:很多人在做课设的时候,习惯性地把 SpringBoot 后端拆成“用户管理、科室管理、医生管理、预约管理”四个模块ÿ… · 2026/9/26 22:50:39
Java内部类与内存泄漏:静态、匿名、局部内部类的本质区别与选型指南 1. 内部类问题的起点:从一次线上内存泄漏说起大概两年前,我们团队上线了一个资讯类App,灰度测试第三天,后台就收到了不少"手机发烫、切后台后再回来卡成PPT"的反馈。排到后面才发现,某几个页面在退出后&… · 2026/9/26 22:50:39
Unity住宅道具合集实战:从预制体到场景搭建的完整指南 讲真,做模拟经营类游戏最让人头疼的环节,往往不是玩法设计,而是“房子里的那一堆家具”。我最近在Unity里捣鼓一个家居建造项目,人手有限,美术排期又排不上,就在资源商店挑了一套居民住宅道具合集ÿ… · 2026/9/26 22:50:39
Ubuntu 22.04.5安装ROS2 Humble:鱼香ROS一键部署实战指南 1. 项目概述:为什么在Ubuntu 22.04.5上用鱼香ROS装ROS2 Humble,是当前最稳的入门路径你刚下载完Ubuntu 22.04.5 LTS镜像,双击安装完成,桌面一亮,心里盘算着:“接下来该装ROS2了。”可刚打开终端敲下sudo ap… · 2026/9/26 23:22:42
Stewart平台六自由度运动学仿真:MATLAB逆解与联合控制 先说结论:Stewart平台这东西,听着像是实验室里才碰得到的精密机构,但实际上它的身影早就在飞行模拟器、并联机床、射电望远镜、汽车测试台架里转悠了。我最早接触它是在做六自由度运动模拟台的预研,当时手里只有MATLAB和一个Solid… · 2026/9/26 23:22:42
新手入门建站:许昌永诚网络科技有限公司教你避开备案大坑 新手入门建站:许昌永诚网络科技有限公司教你避开备案大坑 备案流程一头雾水,是不是让你想直接把电脑砸了?别急,这种“看文档越看越懵”的感觉,很多新手入门建站时都经历过。今天咱们不聊虚的,直接拿许昌永诚网络科技有限公司在实战中处理过的真实案例,… · 2026/9/26 23:22:36
RAG评估实战:检索、生成与端到端指标源码解析 简介:这份源码资源面向从事检索增强生成(RAG)系统开发与调优的技术人员,聚焦RAG评估这一关键环节,帮助解决生成质量难以量化、检索效果无法系统衡量的问题。内容围绕准确率、忠实度、召回率三大核心指标展开࿰… · 2026/9/26 23:22:29
Jev调用优化层:为Coding Agent削减LLM回合与token开销 最近在调一个 coding agent 项目时,我发现一个反直觉的事实:真正拖慢进度的往往不是模型推理,而是那些"看似必要、实则多余"的 LLM 回合。一次文件定位要问一次模型,一次测试报错要问一次模型,一次工具返回内… · 2026/9/26 23:22:29
Obsidian+WorkBuddy构建可调度知识操作系统 1. 这不是又一个“Obsidian入门教程”,而是真正能跑起来的知识操作系统Obsidian WorkBuddy 这个组合最近在知识管理圈里被反复提起,但多数人点开教程后发现:要么卡在 WorkBuddy 安装失败,要么 Obsidian 里插件一堆却根本连不上 A… · 2026/9/26 23:22:29
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46