Java 中 Redis 缓存穿透的排查与修复实践

发布于:2025-08-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

前言

作为一名后端开发,在日常开发中经常遇到各种性能问题。最近在一次项目上线后,系统出现了明显的卡顿现象,用户反馈页面加载缓慢,后台任务处理延迟严重。经过一番排查,发现是线程池配置不当导致任务堆积,进而引发系统性能下降。这篇文章将详细记录我如何从现象入手,逐步排查并解决这个问题的过程。

问题现象

项目上线后不久,运维团队开始收到监控系统的告警,提示应用服务器的CPU使用率和内存占用持续升高。同时,用户反馈部分功能响应变慢,甚至出现超时现象。初步分析认为可能是数据库连接或网络请求的问题,但经过检查,数据库和网络均未发现明显异常。

进一步查看日志后,我发现大量任务在执行过程中被阻塞,等待队列中的任务数量不断增加,最终导致整个系统响应变慢。

问题分析

通过JVM监控工具(如JConsole、VisualVM)观察到,应用中的线程池中存在大量处于 WAITING 状态的线程,且任务队列长度不断增长。这表明线程池的任务提交速度远高于处理速度,导致任务堆积。

我回顾了代码中线程池的配置逻辑,发现线程池使用的是 ThreadPoolExecutor,但参数设置不合理。具体来说,核心线程数设置过小,最大线程数也未根据实际负载进行调整,任务队列采用了无界队列(如 LinkedBlockingQueue),导致任务无限堆积,最终造成系统资源耗尽。

排查步骤

步骤一:确认线程池状态

首先,我通过JConsole查看了线程池的状态信息,发现以下关键指标:

  • corePoolSize: 2
  • maximumPoolSize: 4
  • keepAliveTime: 60秒
  • queue: 无界队列,当前任务数超过1000个

这说明线程池无法动态扩展,任务堆积严重。

步骤二:检查任务提交逻辑

接下来,我查看了任务提交的代码逻辑,发现有多个地方直接调用了 executor.submit(task) 方法,但没有对任务队列长度进行限制。

public class TaskSubmitter {
    private final ExecutorService executor = Executors.newFixedThreadPool(2);

    public void submitTask(Runnable task) {
        executor.submit(task);
    }
}

这段代码使用了一个固定大小的线程池,且任务队列是无界的,一旦任务提交速度超过处理速度,就会导致队列无限增长。

步骤三:定位瓶颈点

为了进一步确认问题根源,我引入了日志记录,对每个任务的执行时间进行了统计,并结合监控工具观察线程池状态。

public class MyTask implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(MyTask.class);

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        try {
            // 模拟业务逻辑
            Thread.sleep(500);
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Task executed in {} ms", duration);
        }
    }
}

通过日志分析,发现大部分任务的执行时间集中在500ms左右,而线程池的核心线程数只有2个,显然无法满足高并发场景下的需求。

步骤四:优化线程池配置

为了解决这个问题,我对线程池进行了重新配置,使用有界队列,并合理设置核心线程数和最大线程数。

public class ThreadPoolConfig {
    private static final int CORE_POOL_SIZE = 8;
    private static final int MAX_POOL_SIZE = 16;
    private static final long KEEP_ALIVE_TIME = 60L;
    private static final int QUEUE_CAPACITY = 100;

    public static ExecutorService createExecutor() {
        return new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

这里的关键改动包括:

  • 将核心线程数提升至8,最大线程数设为16;
  • 使用有界队列,容量设为100;
  • 设置拒绝策略为 CallerRunsPolicy,当任务队列满时,由调用线程直接执行任务,避免任务丢失。

步骤五:测试验证

完成配置修改后,我通过压力测试工具(如JMeter)模拟高并发场景,观察系统表现。

测试结果显示,任务处理效率显著提升,任务队列长度稳定在100以内,系统响应时间大幅缩短,CPU和内存使用率趋于正常。

总结

本次线程池配置不当导致的系统卡顿问题,暴露了我在并发编程方面的经验不足。通过这次排查,我深刻认识到线程池配置的重要性,尤其是在高并发场景下,合理的线程池参数可以极大提升系统性能和稳定性。

此外,我也意识到在项目初期就应该对线程池进行充分评估和测试,而不是等到问题发生后再进行补救。未来我会更加注重线程池的监控和调优,确保系统能够应对各种负载情况。

对于其他开发者而言,建议在使用线程池时注意以下几点:

  • 避免使用无界队列,防止任务无限堆积;
  • 合理设置核心线程数和最大线程数,匹配实际负载;
  • 选择合适的拒绝策略,避免任务丢失;
  • 定期监控线程池状态,及时发现问题。

总之,线程池虽小,但影响重大,掌握其正确使用方式,是提升系统性能的重要一步。