当前位置:首页>java>那行不起眼的代码,让我差点丢了工作

那行不起眼的代码,让我差点丢了工作

  • 2026-08-24 17:25:33
那行不起眼的代码,让我差点丢了工作

开端

中午一点多,正在午休,我被连环夺命call惊醒。

“线上服务OOM,全挂了!用户都在投诉!”

这是我入职这家公司以来,最漫长的一一天。而起因,竟是一行看似无害的循环代码。

那个致命的循环

先来看看罪魁祸首:

public void exportData(List<Long> userIds) {
    int
 page = 1;
    int
 pageSize = 100;
    List<ExportData> allData = new ArrayList<>();

    do
 {
        PageHelper.startPage(page, pageSize);
        List<UserData> dataList = userDao.selectByCondition(params);

        // 转换并累积数据

        List<ExportData> exportList = convertToExportData(dataList);
        allData.addAll(exportList);

        page++;  // 页码递增

    } while (dataList.size() == pageSize);  // 看起来没问题的退出条件

    // 后续处理

    writeToExcel(allData);
}

看起来没什么问题,对吧?分页查询,每次100条,查完就退出。

但就是这个“看起来没问题”的逻辑,在昨晚的生产环境上,把8GB的堆内存啃得渣都不剩。

诡异的无限循环

让我们一步步拆解这个陷阱:

  1. 1. 分页插件的小心思:MyBatis PageHelper有个reasonable参数,默认是true
  2. 2. 最后一页的陷阱:假如总数据是300条
    • • 第1页:1-100条 ✓
    • • 第2页:101-200条 ✓
    • • 第3页:201-300条 ✓
    • • 到这里都正常
  3. 3. 但第4页呢?按照正常逻辑,应该没数据了

然而,当reasonable=true时:如果你查询的页码超过最大页数,PageHelper不会返回空,而是返回最后一页的数据!

所以:

  • • 第4页:返回第3页的数据(201-300条)
  • • dataList.size() == 100,满足循环条件
  • • 第5页:还是返回第3页的数据
  • • ……
  • • 无限循环,直到OOM

为什么测试时没发现?

问得好!因为我们的测试数据很“贴心”:

  • • 测试环境:315条数据
  • • 第1页:100条
  • • 第2页:100条
  • • 第3页:100条
  • • 第4页:15条(因为315%100=15)

看到了吗?最后一页不是100条!所以循环在第四页正常退出了。

生产环境呢?总数据刚好是100的整数倍……

解决:不只是改个参数

第一步:紧急修复(5分钟)

把while条件改成:

do {
    // ... 查询逻辑

} while (dataList.isEmpty().size()>=totalSize);  // 空就退出

简单有效。

第二步:深入理解分页(第二天)

研究了PageHelper的源码,发现reasonable参数的逻辑是:

// PageHelper源码简化版
if
 (reasonable && pageNum > totalPages) {
    pageNum = totalPages;  // 超过总页数?给你最后一页!
}

这设计本身没问题,是为了用户体验——用户输入了不存在的页码,给ta最后一页总比报错好。

但用在do-while里,就是灾难。

第三步:固化开发规范

循环分页查询绝对不能依赖dataList.size()判断,而是用更安全的方式:

// 方案1:先查总数,或者从分页结果中获取总量,或总页码
int
 total = userDao.countByCondition(params);
int
 totalPages = (total + pageSize - 1) / pageSize;  // 向上取整

for
 (int page = 1; page <= totalPages; page++) {
    PageHelper.startPage(page, pageSize);
    List<UserData> dataList = userDao.selectByCondition(params);
    // 处理逻辑

}

// 方案2:游标方式(推荐大数据量)

Long
 lastId = null;
while
 (true) {
    List<UserData> dataList = userDao.selectAfterId(lastId, pageSize);
    if
 (dataList.isEmpty()) break;

    // 处理数据

    lastId = dataList.get(dataList.size() - 1).getId();
}

从这次事故学到的

  1. 1. 不要相信“合理”的默认值
    • • reasonable=true在某些场景下是合理的
    • • 但在循环分页中,它就是炸弹
    • • 明确每个参数的意义,特别是在框架层面
  2. 2. 边界条件,边界条件,还是边界条件
    • • 测试了315条数据(最后不满页)
    • • 没测试300条数据(刚好满页)
    • • 就是这15条的差距,让bug溜进了生产
  3. 3. 分页查询的坑
    // 错误的:依赖结果集大小判断
    while
     (list.size() == pageSize)

    // 正确的:明确知道何时结束

    // 要么先查总数

    // 要么用游标/ID范围

    // 要么确保空结果集时退出
  4. 4. 监控要能看到“趋势”
    如果当时有监控能告警“同一个接口在短时间被循环调用”,我们可能早就发现了。

长效措施:建立防呆体系

  1. 1. 代码审查清单
    • • 所有分页查询必须显式设置reasonable=false
    • • 循环分页必须提供安全退出机制
    • • 禁止在内存中累积大数据集
  2. 2. 测试用例模板
    @Test
    public
     void testPagination() {
        // 测试各种数据量

        testWithDataCount(0);      // 空数据
        testWithDataCount(99);     // 不满一页
        testWithDataCount(100);    // 刚好一页
        testWithDataCount(101);    // 多一条
        testWithDataCount(300);    // 整页倍数
    }
  3. 3. 生产环境熔断
    @Aspect
    public
     class LoopGuardAspect {
        private
     static ThreadLocal<Integer> loopCount = new ThreadLocal<>();

        @Around("execution(* *..*Dao.*(..))")

        public
     Object guard(ProceedingJoinPoint pjp) throws Throwable {
            Integer
     count = loopCount.get();
            if
     (count == null) count = 0;

            if
     (count > 100) {  // 单个请求最多查100次
                throw
     new TooManyQueriesException("疑似无限循环");
            }

            loopCount.set(count + 1);
            try
     {
                return
     pjp.proceed();
            } finally {
                loopCount.remove();
            }
        }
    }

最后的话

那天晚上8点,修复上线,服务恢复。

老板在群里@我:“问题解决了?根本原因是什么?”

我写了又删,删了又写,最后回复:

“分页查询的循环条件有漏洞,已经修复。深层原因是我们对使用的工具理解不够透彻,测试用例覆盖不全。我会写一份详细的事故报告。”

他没再追问,但我知道,这次教训值多少钱。

在技术这条路上,最危险的从来不是那些复杂的算法、高深的架构,而是这些“看起来没问题”的代码。

它们像潜伏在草丛里的蛇,平时安静温顺,一旦条件吻合,就会给你致命一击。

从那天起,我养成了一个习惯:看到while循环,就心里一紧;看到分页查询,就检查三遍。

因为我知道,在深夜的生产环境里,没有“看起来”这回事。

转自公众号:码上生存指南

最新文章

随机文章