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

笔记哥 / 05-10 / 33点赞 / 0评论 / 195阅读
转载请注明出处:   最近在项目中遇到一个奇怪的现象,项目运行环境中的redis在业务运行中,一直没有更新redis的值,在服务的日志中也没有看到相关的异常,导致服务看起来正常,但和redis相关的功能却没有更新。记录下这个异常定位解决的过程。   登录到redis里面,发现redis也是运行正常的,且能正常获取。所以进入到了服务端里面,获取jvm线程进行具体分析,看到有很多个线程栈如下:          ![](https://cdn.res.knowhub.vip/c/2505/10/8a42889f.png?G1YAAMTsdJzIJ4Sm26hD2jvFHc2ARBZBpYT1es9Z%2byb6fhdIjs9offr%2b8JfWpxMz49JKAlEogmdwSZZhhlANiRWocQ0H) ## 定位分析过程 - **`pool-4-thread-1`**: 线程名称,表明该线程属于线程池 `pool-4` 的第一个工作线程(线程池通常由 `ThreadPoolExecutor` 管理)。 - **`Id=211`**: JVM 内部分配的线程唯一标识符(非操作系统线程ID)。 - ### **CPU 时间统计** ```csharp 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、锁竞争)引起。 * * * ### **线程阻塞与等待统计** ```csharp 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` 获取)。 * * * ### **线程状态与堆栈跟踪** ```csharp 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` 对象(线程池工作线程的封装)关联的条件变量(如任务队列非空)。 * * * ### **关键堆栈分析** ```csharp 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`,避免线程闲置。 ```csharp new ThreadPoolExecutor( corePoolSize, // 根据负载动态调整(如使用动态线程池框架) maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(capacity) ); ``` #### **2. 任务队列监控** - **检查队列容量**: 若使用无界队列(如 `LinkedBlockingQueue` 未指定容量),可能导致内存溢出,建议改为有界队列。 - **监控队列堆积**: 通过 JMX 或 `ThreadPoolExecutor` 的 `getQueue().size()` 实时观察任务积压情况。 #### **3. 减少锁竞争** - **使用无锁数据结构**: 替换 `LinkedBlockingQueue` 为 `ConcurrentLinkedQueue`(需配合非阻塞任务调度逻辑)。 - **分离读写操作**: 若共享资源访问频繁,使用读写锁(`ReentrantReadWriteLock`)替代独占锁。 ## 问题解决:   根据截图中的线程栈调用过程,可以定位到项目代码执行调用的地方,发现调用的地方是频繁批量更新redis缓存值得,且每次都是单独一条设置更新得。因此很快推测出来,是这个调用得地方在频繁更新redis缓存值时,导致服务中redis得连接数不够了,因此将代码中更新redis值得方式,使用管道得方式进行更新设置,问题得以解决。