Redis 连接池耗尽的一次异常定位

转载请注明出处:

  最近在项目中遇到一个奇怪的现象,项目运行环境中的redis在业务运行中,一直没有更新redis的值,在服务的日志中也没有看到相关的异常,导致服务看起来正常,但和redis相关的功能却没有更新。记录下这个异常定位解决的过程。

  登录到redis里面,发现redis也是运行正常的,且能正常获取。所以进入到了服务端里面,获取jvm线程进行具体分析,看到有很多个线程栈如下:

定位分析过程

  • pool-4-thread-1

    线程名称,表明该线程属于线程池 pool-4 的第一个工作线程(线程池通常由 ThreadPoolExecutor 管理)。

  • Id=211

    JVM 内部分配的线程唯一标识符(非操作系统线程ID)。

CPU 时间统计

复制代码
cpu=692644037380 ns usr=644020000000 ns 
  • cpu=692644037380 ns

    线程从启动至今消耗的 总 CPU 时间(包括内核态和用户态),单位为纳秒(≈ 692.64 秒)。

  • usr=644020000000 ns

    线程在 用户态(User Mode) 消耗的 CPU 时间(≈ 644.02 秒)。
    差值意义cpu - usr ≈ 48.62秒 为线程在内核态(Kernel Mode)的耗时,通常由系统调用(如 I/O、锁竞争)引起。


线程阻塞与等待统计

复制代码
blocked 2294 for -1 ms waited 28442 for -1 ms
  • blocked 2294

    线程因 竞争锁(synchronized) 而被阻塞的次数(总计 2294 次)。

  • for -1 ms

    阻塞时间的统计方式,-1 ms 表示未记录具体阻塞时长(需启用 JVM 参数 -XX:+PrintBlocked 获取)。

  • waited 28442

    线程在 等待条件触发 (如 Object.wait()Condition.await())的次数(总计 28442 次)。

  • for -1 ms

    等待时间的统计方式,-1 ms 表示未记录具体等待时长(需启用 -XX:+PrintWait 获取)。


线程状态与堆栈跟踪

复制代码
java.lang.Thread.State: WAITING
  at sun.misc.Unsafe.park(Native Method)
  - waiting on (a java.util.concurrent.ThreadPoolExecutor$Worker@5cf37a65)
  • Thread.State: WAITING

    线程处于 无限期等待 状态,通常由以下操作触发:

    • Object.wait()(无超时参数)。

    • LockSupport.park()

    • Condition.await()(无超时参数)。

  • sun.misc.Unsafe.park(Native Method)

    线程通过 LockSupport.park() 进入阻塞状态,底层调用 Unsafe.park()

  • waiting on (a java.util.concurrent.ThreadPoolExecutor$Worker@5cf37a65)

    线程正在等待 ThreadPoolExecutor.Worker 对象(线程池工作线程的封装)关联的条件变量(如任务队列非空)。


关键堆栈分析

复制代码
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)
  • 核心路径

    1. 线程从 LinkedBlockingQueue.take() 尝试获取任务。

    2. 若队列为空,调用 ConditionObject.await() 进入等待。

    3. 最终通过 LockSupport.park() 挂起线程,直到新任务到达。


性能问题诊断

1. 高 waited 次数(28442 次)

  • 可能原因

    • 线程池任务队列长期为空,工作线程频繁等待新任务。

    • 任务生产速度不足(如上游系统吞吐量低)。

    • 线程池配置不合理(核心线程数过多,超出实际需求)。

2. 高 blocked 次数(2294 次)

  • 可能原因

    • 线程池内部锁竞争(如 Worker 线程争用任务队列)。

    • 共享资源(如数据库连接池)的同步访问冲突。

3. CPU 时间分配

  • 用户态耗时占比
    usr / cpu ≈ 644.02 / 692.64 ≈ 93%,表明线程主要执行用户代码,而非系统调用。若应用为计算密集型,此比例为正常现象。

优化建议

1. 线程池配置优化

  • 调整核心线程数

    若队列长期为空,减少 corePoolSize,避免线程闲置。

    复制代码
    new ThreadPoolExecutor(
        corePoolSize,   // 根据负载动态调整(如使用动态线程池框架)
        maxPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(capacity)
    );

2. 任务队列监控

  • 检查队列容量

    若使用无界队列(如 LinkedBlockingQueue 未指定容量),可能导致内存溢出,建议改为有界队列。

  • 监控队列堆积

    通过 JMX 或 ThreadPoolExecutorgetQueue().size() 实时观察任务积压情况。

3. 减少锁竞争

  • 使用无锁数据结构

    替换 LinkedBlockingQueueConcurrentLinkedQueue(需配合非阻塞任务调度逻辑)。

  • 分离读写操作

    若共享资源访问频繁,使用读写锁(ReentrantReadWriteLock)替代独占锁。

问题解决:

  根据截图中的线程栈调用过程,可以定位到项目代码执行调用的地方,发现调用的地方是频繁批量更新redis缓存值得,且每次都是单独一条设置更新得。因此很快推测出来,是这个调用得地方在频繁更新redis缓存值时,导致服务中redis得连接数不够了,因此将代码中更新redis值得方式,使用管道得方式进行更新设置,问题得以解决。

相关推荐
流星白龙17 分钟前
【Redis】1.Redis特性
数据库·redis·缓存
二宝哥14 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
ShiXZ21319 小时前
Redis docker-compose 部署指南
redis·docker·eureka
青山木1 天前
Redis 缓存一致性全解:从 Cache Aside 到延迟双删、Canal+MQ 的进阶之路
java·数据库·redis·后端·缓存
YDS8291 天前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
爬也要爬着前进2 天前
redis主从搭建
数据库·redis·缓存
用户095367515832 天前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌2 天前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆2 天前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰2 天前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis