Jedis连接池耗尽异常:机制解析、排查链路与修复实践

发布时间:2026/10/21 14:37:42
Jedis连接池耗尽异常:机制解析、排查链路与修复实践 难得遇上一个标题就把问题讲得清清楚楚的报错JedisExhaustedPoolException: Could not get a resource since the pool is exhausted。这个异常在我这边线上环境出现过不止一次每次排查链路都不太一样有的是真·连接池太小有的是连接泄漏还有的是Redis节点本身已经扛不住了。今天就把我处理这个问题的完整思路、排查步骤和最终落地的修复方案摊开讲按先看懂机制、再定位根因、最后对症下药的顺序来。1. 先搞清楚Jedis连接池的运作机制别急着调参数1.1 连接池的三种关键状态很多同学一看到pool is exhausted就条件反射地调大maxTotal但调完之后问题往往更严重。所以我先说底层机制。Jedis连接池本质上是一个基于Apache Commons Pool 2的有界资源池核心状态有三个空闲连接池中已经建立、处于可用状态的连接随时可以被borrow走。活跃连接被业务代码借走、正在执行Redis命令或等待响应的连接。销毁中的连接被标记为无效、等待物理断开的连接。当业务线程调用jedisPool.getResource()时池子会按顺序尝试如果有空闲连接直接返回一个如果没有空闲连接且当前活跃数小于maxTotal则创建新的物理连接如果活跃数已经达到maxTotal上限那么线程就会进入等待状态。1.2 耗尽不是一瞬间发生的而是一个累积过程Could not get a resource since the pool is exhausted是池在maxWait超时时间内仍然无法借到连接时抛出的异常。也就是说问题不是此刻刚好满员而是在maxWait这段时间里归还的连接数量始终赶不上借出的数量。我用一个不太严谨但很好懂的类比连接池就像是医院的门诊号。maxTotal是每天放出的号源总数maxWait是患者等待叫号的最长耐心时间。如果某个医生问诊特别慢慢查询或者有患者看完病不走连接未归还那么后面排队的患者就会大量超时显示号已耗尽。所以调maxTotal本质上是多放了几个号但如果问诊速度和患者滞留问题不解决号源再多也照样会耗尽。1.3 借还流程里容易被忽略的细节看一段常见的使用代码try (Jedis jedis jedisPool.getResource()) { jedis.set(key, value); }这确实是最标准的写法因为Jedis实现了Closeabletry-with-resources会自动归还连接。但问题往往出在大家都以为我写了close就不会泄漏的盲区里。实际上close()方法在Jedis里的行为逻辑比较复杂如果调用close()时连接内部状态正常连接会归还到池中如果连接已经损坏比如底层Socket异常、超时中断那么close()会直接销毁这条物理连接。从池的统计指标上看活跃数会下降但空闲数可能并没有增加这就导致池的吞吐能力在不知不觉中被削弱。这个细节在后面排查连接数在涨、池却没恢复时特别关键。2. 完整排查链路从异常堆栈到指标曲线2.1 第一步拿到完整堆栈先分清是借不到还是借到了但报错第一现场的信息永远是异常堆栈。JedisExhaustedPoolException的堆栈通常会长这样redis.clients.jedis.exceptions.JedisExhaustedPoolException: Could not get a resource since the pool is exhausted at redis.clients.jedis.util.Pool.getResource(Pool.java:58) at redis.clients.jedis.JedisPool.getResource(JedisPool.java:127) ... Caused by: java.util.NoSuchElementException: Timeout waiting for idle object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:456)注意Caused by这部分Timeout waiting for idle object就是池等待超时的直接证据。如果堆栈里出现的是java.net.SocketTimeoutException或redis.clients.jedis.exceptions.JedisConnectionException那说明连接其实已经借到了但通信环节出了问题只是后续操作失败时包装成了别的异常。这两种场景的处理方式完全不同第一步必须分清楚。我习惯的做法是先把异常堆栈完整贴到日志检索平台按关键字JedisExhaustedPoolException聚合再看五分钟内的异常数量曲线。如果异常是瞬间突增大概率是流量尖峰或Redis节点抖动如果是平缓上升更可能是连接泄漏或池参数不合理的慢性问题。2.2 第二步全局视角看谁在借连接、借了多久、还不还只看异常堆栈只能知道池被耗尽但不知道为什么耗尽。这一步要把视角拉到全局。我通常先看三个维度的数据Redis服务端指标connected_clients、instantaneous_ops_per_sec、slowlog。如果connected_clients在异常发生前后明显上升但没有回落说明有连接未被及时释放或者应用实例的连接数本身就设置过高。应用实例JVM线程状态重点看Redis命令执行线程的堆栈。用jstack抓线程快照搜索Jedis相关调用栈看是否有大量线程卡在SocketInputStream.read或getResource上。前者是借到了连接但在等响应后者是根本借不到连接在干等。连接池自身的监控指标这需要代码里提前暴露Metrics。如果没有现成的指标就得临时加。我强烈建议在生产项目里给连接池加上监控至少有numActive活跃数、numIdle空闲数、numWaiters等待线程数、createdCount累计创建数、destroyedCount累计销毁数这几个值。以下是一次真实排查中记录到的典型指标变化时间活跃数空闲数等待线程数异常数10:0032180010:05464231510:105009721010:15500156680可以看到等待线程数在疯涨空闲数迅速归零活跃数顶到上限。这个数据已经能说明池确实满了但还看不出满的原因。2.3 第三步翻慢日志和Redis节点状态排除服务端背锅连接池耗尽不一定就是应用层的问题。Redis节点如果有慢查询持有连接的时间就会变长池内的连接周转率会降低等效于池变小了。我遇到过一种典型场景某条KEYS命令没错就是那个被反复警告的KEYS在一个数据量很大的实例上执行了几十秒。那几十秒里这条连接完全被占住其他等待命令的线程全部在排队同时maxTotal被占满新请求直接报ExhaustedPoolException。从应用日志看是连接池耗尽从服务端看是慢查询拖垮了连接周转。所以排查时必须同步查SLOWLOG。执行SLOWLOG GET 50看最近50条慢命令的耗时和内容。同时看redis-cli info里的blocked_clients、connected_clients和内存指标。如果服务端connected_clients远大于应用实例数乘以maxTotal那还要考虑是不是有多套环境共享了同一个Redis实例或者存在讲不清的连接来源。2.4 第四步从业务代码里揪出连接杀手服务端没问题、指标已确认池满接下来就是业务代码审查。这一步最关键也最容易发现看起来没问题但其实有问题的代码。这里有几个高频雷区我按踩中概率排序第一个雷区获取连接后执行了阻塞操作Jedis jedis jedisPool.getResource(); // 模拟一个长达10秒的业务处理 Object result someSlowBusinessProcess(); jedis.set(order: orderId, result.toString()); jedis.close();这段代码在低并发的时候不会出事但在并发上来后会迅速打满连接池。因为getResource到close之间的时间完全不属于Redis操作却占着连接不释放。正确做法是尽量缩短连接占用时间把耗时操作移出去。第二个雷区异常路径上少了closeJedis jedis null; try { jedis jedisPool.getResource(); jedis.set(k, v); } catch (Exception e) { log.error(redis error, e); } finally { // 忘了关闭 }这种代码在出异常时会漏掉归还。虽然finally没写close不一定会泄漏但一旦中间某步抛异常且没有兜底释放连接就会长期占用。到池满时所有线程都卡在拿连接上形成雪崩。第三个雷区自定义业务线程池与Jedis池耦合过深比如一个订单处理线程池设置了200个线程每个线程处理中都会借连接。Jedis池的maxTotal只有50那么同一时刻必然有多达150个线程在getResource上排队等待。池的maxWait一到直接抛异常。这种问题不是连接泄漏而是连接池容量与并发模型严重不匹配。第四个雷区没有配置testOnReturn或testOnBorrow时的隐患如果长期借到的是已经死掉的连接比如网络闪断、Redis重启后没有被正确销毁的旧连接业务线程拿到连接后执行命令抛JedisConnectionException。有些团队在catch块里直接把连接close()这本身没问题但如果业务代码把异常吞掉且不归还连接又会回到第二个雷区。更隐蔽的是池里的连接其实已经失效但池还认为它们是空闲的每次借出都在赌运气。3. 根因归类后对症下药参数、代码、兜底三管齐下3.1 参数调整改配置前先算清楚账如果根因确实是池容量不够修改配置时要先做个粗略估算。我一般用一个简单公式建议maxTotal ≈ 单实例并发请求峰值 × (单请求平均占用连接时间 / 请求平均耗时)举个例子接口A的QPS峰值为500平均耗时20ms其中Redis操作耗时2ms。那么连接占用率为2/20 10%理论上池只需要500 × 0.1 50个连接。但这是理想值考虑到GC停顿、网络抖动、突发流量我会在这个基础上放1.5到2倍的余量。Jedis连接池的一组常用参数与我的建议值如下参数默认值建议值备注maxTotal850-200按QPS算上限不是越大越好maxIdle8和maxTotal相同或略低避免频繁创建销毁minIdle05-10维持最低空闲连接maxWait-1无限3000-5000ms有界等待避免线程全部挂死testOnBorrowfalsetrue借出时验证连接可用性testWhileIdlefalsetrue空闲时定期验证提前剔除失效连接注意maxTotal不是设置越高越好。连接数越多Redis服务端要维护的connected_clients越多内存和CPU开销都会增加。之前我见过某团队把maxTotal调到1000结果Redis实例的connected_clients直接拉满单实例吞吐反而不稳定。合理的做法是先按公式算一遍再结合压测结果微调。3.2 代码层面修复连接生命周期管理是根治手段参数只能缓解代码生命周期管理才是根治。推荐统一封装一个Redis操作工具把借还逻辑收敛在一处避免散落各处的原生调用。下面是我常用的封装模板Component public class RedisExecutor { private final JedisPool jedisPool; public RedisExecutor(JedisPool jedisPool) { this.jedisPool jedisPool; } public T T execute(JedisCallbackT callback) { try (Jedis jedis jedisPool.getResource()) { return callback.doInRedis(jedis); } catch (JedisExhaustedPoolException e) { // 这里不要只是打日志最好加一个降级开关 throw new RedisBusyException(Jedis pool exhausted, e); } } FunctionalInterface public interface JedisCallbackT { T doInRedis(Jedis jedis); } }业务侧调用变成String value redisExecutor.execute(jedis - jedis.get(user: userId));这样一个核心收益是连接占用时间被压缩到最小不存在借了连接去做耗时的业务计算这类低级问题。同时所有异常路径都由try-with-resources兜底杜绝泄漏。如果Redis客户端升级到Jedis 4.x使用JedisPooled会更省事因为API自带借还逻辑JedisPooled jedisPooled new JedisPooled(localhost, 6379); jedisPooled.set(k, v);不过JedisPooled牺牲了一点灵活性不适合有复杂pipeline或事务的场景选型时留意一下。3.3 兜底策略别让连接池异常直接击穿核心链路连接池耗尽到一定程度会引发连锁反应大量线程阻塞在getResource上占住业务线程池不放最终导致整个应用不可用。所以除了修复池本身还要有一个快速失败和降级的兜底机制。我在项目里加了一个简单的熔断开关public T T executeWithFallback(JedisCallbackT callback, SupplierT fallback) { try { return execute(callback); } catch (RedisBusyException e) { // 降级走本地缓存、默认值、或更新异步任务队列 return fallback.get(); } }降级策略要根据业务性质来定读场景优先查本地Caffeine缓存返回稍旧的数据允许短暂的数据不一致。写场景发送到内存队列或消息队列异步落库等Redis恢复后补偿写入。非关键场景直接返回空值或默认值并打warn日志方便后续告警和复盘。这里不推荐无限重试因为连接池已耗尽时重试只会让更多线程堆积。设置单次请求的重试次数为0或最多一次且重试之间的间隔必须大于maxWait。3.4 参数与代码的配合一个真实调整过程去年某次大促前某业务单节点的连接池配置是maxTotal50、maxWait3000。压测刚过200 QPS日志里开始密集出现JedisExhaustedPoolException。我先用本章第2节的排查链路确认没有慢查询、没有连接泄漏、没有死连接问题根因就是纯容量不足。按公式估算该接口单次请求Redis占用约10msQPS峰值800理论连接数为800 × 0.01 8但考虑毛刺我把maxTotal调到100maxIdle调到50minIdle保持10maxWait保持3000。调完后再压测异常消失Redis服务端connected_clients稳定在110左右CPU和内存都在健康水位。之后我又加了一道熔断降级兜底保证即使突发流量超过预设值也不会把应用拖垮。这套组合拳在大促当天表现很稳全程零异常。4. 真正容易翻车的两个隐蔽坑4.1 maxWait与调用方超时时间的隐形冲突连接池的maxWait只是借连接阶段的超时。从业务方的角度看一次Redis操作的总超时其实是总耗时 maxWait(等待借连接) socketTimeout(执行命令)如果你的spring-mvc接口超时时间设置的是3000ms而Jedis的maxWait也设置成3000mssocketTimeout又是2000ms那么一次Redis操作在最坏情况下会吃掉5000ms。这会导致接口超时但连接池异常日志可能完全看不到因为getResource在3秒内成功返回了只是后续命令执行又花了2秒。我统一建议maxWait不要超过业务链路总预算的一半。socketTimeout根据慢查询的最大耗时减去一定余量来设置而不是拍脑袋。在全局配置文件里把各层超时时间写清楚避免不同模块设置互相矛盾。4.2 集群模式与单机模式下的表现差异如果你用的不是单机JedisPool而是JedisCluster情况会略有不同。JedisCluster内部每个节点都维护一个连接池而且默认的maxTotal值可能并不适用于高并发场景。JedisCluster的默认连接池配置是JedisPoolConfig的子类maxTotal只有8。在集群模式下这8个连接会被分到多个节点上如果某个节点的访问特别集中那么这个节点对应的子池会率先耗尽异常表现同样是JedisExhaustedPoolException。集群模式下排查时要看具体抛异常的节点是谁不能只盯着整个集群看。另外集群模式还容易遇到更新节点时连接池没有重建的问题导致连接池中的节点列表与实际集群拓扑不一致从而浪费池容量。遇到这类问题要检查renewTimeout和discovery相关配置确保拓扑变化后池能及时刷新。4.3 小心看起来正常但实际没归还的管道模式在高性能场景里有些团队会为了减少RTT使用Pipeline。Pipeline模式下Jedis连接在sync()之后才归还。如果业务代码在Pipeline的sync()之前就异常退出连接状态会停留在正在接收响应的状态close()时会走销毁分支。更隐蔽的是多个线程共享同一个Pipeline连接来提升吞吐。这种用法一旦有一个线程抛异常退出整个连接的状态就不可控了。正确的做法是Pipeline连接保持线程独占使用完立刻sync()及时归还。我见过最离谱的一次泄漏事故就是因为一个定时任务里Pipeline的sync()方法放在循环外整个循环执行了五分钟连接被独占五分钟。那阵子Redis查询并不慢但连接池被一个任务打满所有业务请求都开始超时。5. 最后的实战总结与我的经验坐标如果你正在被JedisExhaustedPoolException折磨按这个顺序处理大概率能快速收敛第一步看清堆栈里的Caused by区分等不到连接和连接不可用。第二步汇总服务端指标、应用线程栈、池指标确认根因方向。第三步审查业务代码优先解决连接占用过长、异常不归还、Pipeline误用等问题。第四步再谈参数调整按QPS和连接占用时长估算maxTotal。第五步加一层降级兜底把池耗尽从致命故障降级为可容忍毛刺。最后分享一个我一直在用的监控小技巧给连接池的numActive和numWaiters设置告警阈值分别在maxTotal * 0.8和50左右触发告警。这两个指标远比等异常出现后再看堆栈有预警价值。连接池这个组件不怕容量小怕的是容量用完的时候你还在盲目调整参数。保持监控、保持敬畏比任何大招都管用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询