一次由 PageHelper 分页污染引发的 Bug 排查实录
一次由 PageHelper 分页污染引发的 Bug 排查实录
- 一、问题背景
- 二、问题现象
- 三、深入排查
- 1. 复现现象
- 2. 问题根源:PageHelper 的全局分页上下文
- 四、最终解决方案
- 1. 调整分页时机
- 2. 显式清除分页上下文
- 五、优化与扩展建议
- 1. 避免在分页期间执行其他 SQL
- 2. 使用独立事务或异步校验
- 3. 使用 MyBatis-Plus 或自定义分页参数
- 4. 在项目统一分页入口中加入清理逻辑
- 六、总结
——从「查不到用户」到「分页上下文污染」,一次看似离谱的排查之旅

一、问题背景
在开发一个定制资源管理模块时,有这样一个场景:
管理员用户可以分页查看单位下的所有定制数据。
分页查询逻辑中,会先校验当前用户的角色权限(是否为管理员),再去执行分页查询列表。
相关伪代码如下:
public PageResult getCustomizeList(SelectCustomizeDTO dto) {// 分页配置PageHelper.startPage(dto.getPage(), dto.getSize());// 权限校验(查询单位 + 用户角色信息)validateUserPermission(dto.getFid(), dto.getUid(), allowRoleTypes, true);// 分页查询数据List<CustomizeVO> customizeList = customMapper.getCustomizeList(...);return new PageResult(new PageInfo<>(customizeList));
}
逻辑很正常,看上去没问题。
但在上线后,却遇到了一个非常诡异的 Bug。
二、问题现象
当管理员访问数据列表时:
-
第一页数据正常显示
-
第二页开始,直接报错:用户在单位下,无角色信息
然而在数据库中手动执行 SQL:
SELECT role_type FROM user_manage WHERE fid = 123 AND uid = 111111;
结果明明存在,角色信息完全正常。
三、深入排查
1. 复现现象
通过调试发现:
==> Preparing: SELECT count(0) FROM user_manage WHERE del_flag = 0 AND fid = ? AND userid = ?
==> Parameters: 123(Integer), 111111(Long)
<== Columns: count(0)
<== Row: 1
注意!MyBatis 打印的 SQL 不是原本的
SELECT role_type ...,而是SELECT count(0)。
这说明——分页插件 PageHelper 正在拦截这个查询!
2. 问题根源:PageHelper 的全局分页上下文
PageHelper 的分页实现方式是通过 ThreadLocal 保存分页参数:
PageHelper.startPage(page, size);
一旦调用,它会将分页信息绑定到当前线程上。
后续同线程内的所有查询语句,都会被它拦截,自动改写为 SELECT count(0) 或 limit ... 的分页 SQL。
在本案例中:
-
先执行了
PageHelper.startPage() -
再执行了
validateUserPermission(),该方法内部调用了userManageMapper.getUserRoleType(fid, uid) -
这个查询被分页插件错误地分页了!
于是变成:
SELECT count(0) FROM user_manage WHERE del_flag = 0 AND fid = ? AND uid = ?;
执行结果当然只有一条计数结果,
自然无法映射到 Integer roleType 字段中,导致角色信息为 null,触发“用户无角色”的异常。
四、最终解决方案
1. 调整分页时机
只需将分页逻辑移动到权限校验之后:
public PageResult getCustomizeList(SelectCustomizeDTO dto) {// 先做权限校验validateUserPermission(dto.getFid(), dto.getUid(), allowRoleTypes, true);// 再开启分页PageHelper.startPage(dto.getPage(), dto.getSize());List<CustomizeVO> customizeList = customMapper.getCustomizeList(...);return new PageResult(new PageInfo<>(customizeList));
}
2. 显式清除分页上下文
如果确实需要在分页开启之后再调用其他 SQL(比如复杂场景),
可以使用:
PageHelper.clearPage(); // 清除 ThreadLocal 分页上下文
validateUserPermission(fid, uid, allowRoleTypes, true);
PageHelper.startPage(page, size); // 再次启用分页
这样只让后续的分页查询生效,而不影响权限验证、统计查询等逻辑。
五、优化与扩展建议
1. 避免在分页期间执行其他 SQL
分页设置与业务 SQL 解耦是最简单的方案。
建议做到:
-
只在真正分页查询前调用
startPage() -
任何业务前置检查(如权限、统计、过滤)都应在分页之前完成。
2. 使用独立事务或异步校验
如果权限逻辑较复杂,可以单独封装成一个独立的 Service 方法并加上 @Transactional(propagation = Propagation.REQUIRES_NEW),
这样在独立事务中执行,不受外部分页或线程上下文影响。
3. 使用 MyBatis-Plus 或自定义分页参数
MyBatis-Plus 的分页拦截器使用 Page 对象,不依赖 ThreadLocal,因此天然不会污染其他查询。
例如:
IPage<CustomizeVO> page = new Page<>(dto.getPage(), dto.getSize());
customMapper.selectPage(page, new QueryWrapper<>());
4. 在项目统一分页入口中加入清理逻辑
可以在 PageHelper.startPage() 之前先执行一次:
PageHelper.clearPage();
防止前一层(如拦截器或 AOP)残留的分页上下文干扰当前请求。
六、总结
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 第1页正常,第2页开始查询不到用户 | PageHelper 分页污染了权限 SQL | 调整分页时机或使用 PageHelper.clearPage() |
MyBatis 日志中出现 SELECT count(0) | 分页上下文仍在作用中 | 清除或重启分页上下文 |
| 多层调用混合分页与非分页查询 | ThreadLocal 未隔离 | 使用新事务或 MyBatis-Plus 分页 |
⚙️ 一句话总结:
PageHelper 的分页配置是「线程级全局状态」,
一旦开启,同线程内的所有 SQL 都会受影响,除非手动清除。
写在最后
这个问题看似偶发,其实在微服务或复杂多层调用场景中非常常见。
经验教训是:
👉 分页要“就地调用”,
👉 权限校验、统计查询要“避开分页线程”,
👉 出现奇怪的SELECT count(0)时,第一反应就是:是不是 PageHelper 干的。
