一次由 PageHelper 分页污染引发的 Bug 排查实录

一次由 PageHelper 分页污染引发的 Bug 排查实录

  • 一、问题背景
  • 二、问题现象
  • 三、深入排查
    • [1. 复现现象](#1. 复现现象)
    • [2. 问题根源:PageHelper 的全局分页上下文](#2. 问题根源:PageHelper 的全局分页上下文)
  • 四、最终解决方案
    • [1. 调整分页时机](#1. 调整分页时机)
    • [2. 显式清除分页上下文](#2. 显式清除分页上下文)
  • 五、优化与扩展建议
    • [1. 避免在分页期间执行其他 SQL](#1. 避免在分页期间执行其他 SQL)
    • [2. 使用独立事务或异步校验](#2. 使用独立事务或异步校验)
    • [3. 使用 MyBatis-Plus 或自定义分页参数](#3. 使用 MyBatis-Plus 或自定义分页参数)
    • [4. 在项目统一分页入口中加入清理逻辑](#4. 在项目统一分页入口中加入清理逻辑)
  • 六、总结

------从「查不到用户」到「分页上下文污染」,一次看似离谱的排查之旅


一、问题背景

在开发一个定制资源管理模块时,有这样一个场景:

管理员用户可以分页查看单位下的所有定制数据。

分页查询逻辑中,会先校验当前用户的角色权限(是否为管理员),再去执行分页查询列表。

相关伪代码如下:

java 复制代码
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:

sql 复制代码
SELECT role_type FROM user_manage WHERE fid = 123 AND uid = 111111;

结果明明存在,角色信息完全正常。


三、深入排查

1. 复现现象

通过调试发现:

log 复制代码
==> 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 保存分页参数:

java 复制代码
PageHelper.startPage(page, size);

一旦调用,它会将分页信息绑定到当前线程上。

后续同线程内的所有查询语句 ,都会被它拦截,自动改写为 SELECT count(0)limit ... 的分页 SQL。

在本案例中:

  • 先执行了 PageHelper.startPage()

  • 再执行了 validateUserPermission(),该方法内部调用了 userManageMapper.getUserRoleType(fid, uid)

  • 这个查询被分页插件错误地分页了!

于是变成:

sql 复制代码
SELECT count(0) FROM user_manage WHERE del_flag = 0 AND fid = ? AND uid = ?;

执行结果当然只有一条计数结果,

自然无法映射到 Integer roleType 字段中,导致角色信息为 null,触发"用户无角色"的异常。


四、最终解决方案

1. 调整分页时机

只需将分页逻辑移动到权限校验之后:

java 复制代码
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(比如复杂场景),

可以使用:

java 复制代码
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,因此天然不会污染其他查询。

例如:

java 复制代码
IPage<CustomizeVO> page = new Page<>(dto.getPage(), dto.getSize());
customMapper.selectPage(page, new QueryWrapper<>());

4. 在项目统一分页入口中加入清理逻辑

可以在 PageHelper.startPage() 之前先执行一次:

java 复制代码
PageHelper.clearPage();

防止前一层(如拦截器或 AOP)残留的分页上下文干扰当前请求。


六、总结

问题现象 根因 解决方案
第1页正常,第2页开始查询不到用户 PageHelper 分页污染了权限 SQL 调整分页时机或使用 PageHelper.clearPage()
MyBatis 日志中出现 SELECT count(0) 分页上下文仍在作用中 清除或重启分页上下文
多层调用混合分页与非分页查询 ThreadLocal 未隔离 使用新事务或 MyBatis-Plus 分页

⚙️ 一句话总结

PageHelper 的分页配置是「线程级全局状态」,

一旦开启,同线程内的所有 SQL 都会受影响,除非手动清除。


写在最后

这个问题看似偶发,其实在微服务或复杂多层调用场景中非常常见。

经验教训是:

👉 分页要"就地调用",
👉 权限校验、统计查询要"避开分页线程",

👉 出现奇怪的 SELECT count(0) 时,第一反应就是:是不是 PageHelper 干的。


相关推荐
q***38517 分钟前
SpringCloud实战十三:Gateway之 Spring Cloud Gateway 动态路由
java·spring cloud·gateway
小白学大数据26 分钟前
Python爬虫伪装策略:如何模拟浏览器正常访问JSP站点
java·开发语言·爬虫·python
程序员西西1 小时前
SpringBoot接口安全:APIKey保护指南
java·spring boot·计算机·程序员·编程·编程开发
summer_west_fish1 小时前
单体VS微服务:架构选择实战指南
java·微服务·架构
v***8572 小时前
Ubuntu介绍、与centos的区别、基于VMware安装Ubuntu Server 22.04、配置远程连接、安装jdk+Tomcat
java·ubuntu·centos
h***59332 小时前
MySQL如何执行.sql 文件:详细教学指南
数据库·mysql
郑重其事,鹏程万里2 小时前
键值存储数据库(chronicle-map)
数据库·oracle
烤麻辣烫2 小时前
黑马程序员大事件后端概览(表现效果升级版)
java·开发语言·学习·spring·intellij-idea
q***96582 小时前
Spring总结(上)
java·spring·rpc
思密吗喽2 小时前
宠物商城系统
java·开发语言·vue·毕业设计·springboot·课程设计·宠物