
1. 池化思想在 Redis 访问链路上到底解决了什么半夜被超时告警叫醒打开监控一看Redis 服务端连接数只有三十几个CPU 闲得发慌可应用侧的线程栈里却堆着几百个等待中的业务线程接口的平均耗时从 3ms 涨到了 800ms。这种“服务端不忙、客户端却堵死”的场面十有八九不是 Redis 本身的问题而是 Redis 6.X 连接池的配置出了偏差。连接池这个东西很微妙配得松平时看不出问题流量一抖就雪崩配得紧又会把好好的高并发能力白白浪费。这篇内容想讲清楚的就是这件事Redis 6.X 下连接池的池化思想怎么落地参数怎么算出来Spring Boot 里怎么配线上出问题怎么查。它不算入门科普适合已经能把 Redis 跑起来、但在高并发或者长稳场景里吃过亏的开发者也适合刚接手一套中间件配置、看到maxTotal8却不知道能不能改的同学。文中涉及的客户端覆盖 Jedis、Lettuce、Redisson 三类参数计算会给出完整的推导过程排查部分会给出可以直接对照的现象表。池化思想本身并不新鲜线程池、数据库连接池、HTTP 连接池底层的抽象几乎一模一样把创建成本高的资源提前建好放在一个容器里用的时候借、用完还容器负责容量控制和生命周期管理。Redis 的 TCP 连接正好符合“创建成本高”这个特征——三次握手、认证、可选的 SELECT 库、TLS 握手一条连接建立起来动辄零点几毫秒到几毫秒如果每条命令都走一遍这套流程QPS 上千之后光连接建立就能把 CPU 吃光还会在服务端制造大量TIME_WAIT。1.1 一次 Redis 命令背后的隐藏成本很多人对 Redis 的性能认知停留在“单机十万 QPS、命令亚毫秒”于是默认访问 Redis 就是一次函数调用。实际链路要长得多业务线程从池里借连接、序列化命令、写 socket、内核协议栈处理、服务端事件循环读取、执行命令、回写响应、客户端读取解析、归还连接。这条链路上真正花在“执行命令”上的时间可能只占三成剩下的全是网络往返和上下文切换。不池化的情况下每次操作都要新建连接成本会被放大好几倍。实测在同一台机器上用短连接跑GET单线程 QPS 大概只有长连接的三分之一到二分之一因为每条命令都要付出connect和close的系统调用代价。更麻烦的是服务端大量短连接涌入会让 Redis 的连接管理开销飙升maxclients默认 10000被快速耗尽超出的连接直接被拒绝客户端看到的就是ERR max number of clients reached。还有一种常被忽略的成本是 TIME_WAIT。主动关闭方会进入 TIME_WAIT 状态并保持 2MSL如果客户端频繁建连断连本机端口会被大量占用严重时新连接直接失败。池化复用长连接等于从源头上把这个问题消掉了——这也是“池化”最朴素的价值不是让单次操作变快而是让整体不再为重复的准备工作付费。从工程角度看连接池还带来一个隐性收益可控。所有对外访问都被收拢到一个有容量上限的容器里你能看到当前有几个活跃、几个空闲、多少线程在排队出问题时至少有指标可查。没有池的时候连接散落在各处排查基本靠猜。1.2 池化四要素容量、复用、校验、回收把连接池拆开看本质就四件事每一件都对应一组配置参数。容量决定池子最多能同时提供多少条连接对应maxTotalLettuce 里叫max-active。它是一道硬闸门防止某个下游抖动时把应用线程全部拖死在这里。复用是池子的核心机制借出归还空闲连接留在池里等待下次使用对应maxIdle和minIdle两个水位线。校验解决“借到的连接是不是还活着”对应testOnBorrow、testOnReturn、testWhileIdle。回收处理“闲置太久的连接该扔掉”对应minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis、numTestsPerEvictionRun。这四件事任意一件缺位池子都会出问题。只有容量没有校验网络闪断后池里躺着一堆死连接业务借到就报错只有校验没有回收连接会一直占着服务端的配额慢慢逼近maxclients只有回收没有容量洪峰来了照样把服务端打满。很多线上事故的根因就是把其中某一项配成了极端值比如把maxWaitMillis设成 -1 表示无限等待看起来“不会失败”实际上是让线程无限期挂起故障从“快速失败”变成了“整体雪崩”。1.3 什么情况下不该池化这一点必须提前讲清楚否则容易走偏不是所有场景都该给 Redis 上连接池。如果用的是 Lettuce它的连接本身是线程安全的基于 Netty 异步模型一条连接就能承载很高的并发默认的共享连接模式在绝大多数普通读写场景下性能反而比池化更好——因为省掉了借还的锁竞争。官方文档也明确说过Lettuce 的连接是线程安全的池化只在特定场景才需要。真正需要池化的是这几种使用了阻塞式命令比如BLPOP这种会让连接挂住的需要在同一连接上执行事务或WATCH需要频繁切换数据库编号或者用了某些必须独占连接的特性。Jedis 则相反它的Jedis实例不是线程安全的一条连接同一时刻只能被一个线程使用所以必须配合连接池否则只能每个线程自己 new 一个等于没池化。这也是为什么同样的参数在不同客户端下含义完全不同照抄别人的配置往往会翻车。2. 客户端选型Jedis、Lettuce 的连接模型差在哪配置参数长得像底层模型差得远。选型没搞明白就去调参数等于在错误的坐标系里找最优解。2.1 Jedis 的“一连接一线程”模型Jedis 走的是同步阻塞模型。一个Jedis对象内部封装一条 socket 连接调用get()时会一直阻塞到响应回来。因为不是线程安全的多线程共用同一个实例会出现协议错乱一个线程的响应被另一个线程读走所以必须从JedisPool借用完还。这个模型的优点是直观、调试友好、命令覆盖全缺点是并发能力完全由连接数决定。你有多少条连接同一时刻就能并行处理多少个命令。想提高吞吐只能加连接而连接加多了服务端压力又上来了。所以在 Jedis 体系下maxTotal是一个需要认真算的参数不是拍脑袋填个 8 或者 200 就完事。另一个细节是 Jedis 的 3.x 和 4.x 在 API 上有断层。3.x 的参数是maxWaitMillis、timeBetweenEvictionRunsMillis、minEvictableIdleTimeMillis这种 long 型毫秒值4.x 改成了setMaxWait(Duration)、setTimeBetweenEvictionRuns(Duration)、setMinEvictableIdleTime(Duration)。网上大量老文章还在用 3.x 的写法直接抄到 4.x 项目里会编译不过或者更糟——用了带默认值的重载配置静默失效。这个坑后面会细说。2.2 Lettuce 的共享连接与两种池化模式Lettuce 基于 Netty连接是线程安全的一条连接上可以并发跑多个命令响应通过 Promise 回调分发。默认模式下整个应用准确说是整个RedisConnectionFactory对每个 Redis 节点只维持一条连接所有请求复用这一条。它的吞吐上限远高于单条 Jedis 连接因为写操作是异步的不存在“一条连接只能串行处理一个命令”的限制。Spring Boot 2.x 之后默认客户端换成了 Lettuce但很多人不知道的是只有在配置里显式写了spring.redis.lettuce.pool.*并且 classpath 里有commons-pool2时池化才会真正启用。没配 pool 属性用的就是共享连接模式配了 pool 但没引 commons-pool2配置会被静默忽略日志里可能只有一行不起眼的警告。Lettuce 池化之后连接模型变成“池里的每条连接仍然线程安全”所以它和 Jedis 池的语义不一样Jedis 池是“借走独占”Lettuce 池是“借走共享但带容量限制”。这意味着 Lettuce 的max-active不需要按“并发线程数”来估而是按“共享连接扛不住的场景下的补充”来估。实践中 Lettuce 池化最常见的动机不是性能而是避免阻塞命令把共享连接堵死——一条BLPOP挂在共享连接上后面所有命令都排队这才是灾难。2.3 Redisson 的池参数与适用边界Redisson 是另一套体系它把 Redis 当成分布式对象存储来用提供分布式锁、布隆过滤器、限流器等高级结构。它的连接池参数是connectionPoolSize每节点的普通连接数、connectionMinimumIdleSize每节点最小空闲、subscriptionConnectionPoolSize订阅连接数用于 pub/sub。Redisson 的默认值比较保守connectionPoolSize默认 64connectionMinimumIdleSize默认 24。注意这两个都是按节点算的三主三从的集群下实际连接数要乘以节点数再乘以实例数很容易就把maxclients撑满。如果项目里只是用 Redisson 做分布式锁不需要高并发读写把池子调小反而更稳。2.4 三者的选型对照维度JedisLettuceRedisson连接模型同步阻塞非线程安全异步非阻塞线程安全异步线程安全是否必须池化必须默认不必特定场景需要内置池按需调参池参数前缀maxTotal/maxIdle/minIdlemax-active/max-idle/min-idleconnectionPoolSize/...事务与阻塞命令天然支持需独占连接建议池化支持连接数估算口径并发线程数阻塞连接数 余量节点数 × 池大小适用场景传统同步栈、需要精细控制Spring Boot 默认、高吞吐普通读写分布式锁、高级数据结构选型上没有绝对优劣。同步栈的老项目继续用 Jedis 完全没问题只要把池参数算清楚新项目用 Spring Boot 默认的 Lettuce 也够用但要清楚自己到底是在共享连接还是在池化别以为配了pool就万事大吉。3. 参数拆解每一个数字背后的因果这一节是重点。连接池调优之所以难是因为大部分参数之间存在耦合单独看任何一个都没有意义。3.1 maxTotal / max-active 到底该填多少先把结论放前面连接数 ≈ 峰值 QPS × 单次命令平均耗时秒× 安全系数。这个公式来自利特尔法则Littles Law系统中的并发请求数等于到达速率乘以平均停留时间。放在连接池场景下如果每个请求占用一条连接 T 秒每秒来 Q QPS那么稳定状态下同时在用连接的请求数就是 Q × T。举个例子目标峰值 8000 QPS单次GET端到端耗时 1.2ms含网络往返不含业务逻辑那么同时占用连接数 ≈ 8000 × 0.0012 ≈ 9.6取整 10。再考虑流量毛刺、GC 停顿导致的耗时抖动、慢查询给 2 到 3 倍余量maxTotal定在 24 到 32 之间比较合适。如果算出来只有 10 左右而你把maxTotal设成 200会发生什么平时池里只有十几条活跃剩下全空闲白占服务端配额一旦某个下游变慢200 个线程可以同时冲到 Redis 上把压力放大 20 倍触发服务端的maxclients保护反而更容易出事。容量上限的意义是“限流”不是“越多越好”。反过来把maxTotal设成 8而实际需要 30 条并发多出来的 22 个请求全部在池的等待队列里排队。这时候你看应用监控会看到大量线程处于WAITING接口 RT 直线上升但 Redis 服务端的 CPU 和连接数都很低——典型的“客户端自我限流”。这种问题最难查因为所有指标看起来都“正常”。还有一个容易被忽略的约束是服务端侧的总量。假设你的应用部署了 20 个实例每个实例maxTotal32那么最坏情况下会有 640 条连接打到 Redis。Redis 的maxclients默认 10000看起来够但要留出运维通道、监控采集、其他应用的配额安全水位建议控制在 70% 以内。换算下来20 实例 × 32 640远低于 7000 的水位线没问题但如果实例数扩到 200 个6400 就逼近红线了这时候要么下调单实例的maxTotal要么考虑在中间加一层代理。3.2 maxIdle 和 minIdle 的水位线怎么定maxIdle是池中最多保留多少条空闲连接minIdle是最少保留多少条。这两个参数决定的不是“性能上限”而是“稳态成本”。maxIdle设得比maxTotal小很多是很多配置模板的默认做法比如maxTotal32, maxIdle8。它的后果是流量高峰过后多出来的连接会被立刻销毁下次高峰再来又要重新创建。如果流量是明显的潮汐型白天高、夜间低这种配置等于每天做两次大规模的连接重建重建期间新建连接的开销叠加在业务请求上会看到周期性的 RT 抖动。更稳妥的做法是让maxIdle接近maxTotal比如maxTotal32, maxIdle32让池子能自然“记住”峰值规模。minIdle是保底水位作用是避免流量低谷时池子被清空、下次请求现建连接。它应该不小于“日常稳态并发数”。前面算出来稳态需要 10 条连接那minIdle设在 8 到 10 之间是合理的保证低谷期也有热连接可用减少首包延迟。有一个副作用需要提醒minIdle越大长期占用的服务端连接越多。如果应用有几十个小实例每个实例minIdle16光保底就是几百条连接挂着虽然 Redis 处理空闲连接的成本几乎为零但配额是实打实被占掉的。小实例反而应该降低minIdle让池子按需弹性伸缩。3.3 maxWait超时链路的最后一环maxWaitLettuce 叫max-wait表示从池里借连接最多等多久超时抛NoSuchElementException或者PoolExhaustedException。这个参数最忌讳设成 -1 或者一个很大的值。设成无限等待的初衷通常是“不想让请求失败”实际效果是把故障从“快速失败”变成“线程堆积”。连接池满了说明后端处理不过来这时候正确的做法是尽快失败、让上游降级或者重试而不是让线程傻等。线程是有限资源Tomcat 线程池就那 200 个全卡在借连接上整个应用就瘫了连健康检查都返回不了。合理取值和业务 SLA 挂钩。接口整体超时预算假设是 500msRedis 查询只是其中一环那么maxWait设 100ms 到 200ms 比较合适留出余量给后续逻辑。如果设得太短比如 10ms在正常波动下也会频繁误报反而降低成功率。还有一个细节是maxWait要和 Redis 操作的 socket 超时timeout区分开前者是等池子里的连接后者是命令发出后等响应两个超时叠加才是一次调用的最坏耗时做容量规划时要一起算。3.4 检测与驱逐四个参数决定连接“活不活”这一组参数是线上问题的高发区尤其是客户端和服务端对“空闲”的判定不一致时。testOnBorrow借连接时做一次 PING 校验。好处是绝对不会拿到死连接代价是每次借出多一次往返。高 QPS 下这个开销不可忽视通常建议关掉改用testWhileIdle。testOnReturn归还时校验开销更大除了特殊场景一般不开。testWhileIdle在驱逐线程扫描时对空闲超过一定时间的连接做校验失效的踢掉。这是性价比最高的方案建议开启。timeBetweenEvictionRunsMillis驱逐线程多久跑一次。设太大比如 5 分钟会导致死连接长时间躺在池里设太小比如 100ms会无谓消耗 CPU。30 秒是个比较通用的值。minEvictableIdleTimeMillis连接空闲多久后可以被驱逐。这是最关键的参数必须小于服务端主动断开空闲连接的时间。服务端的对应配置是timeoutRedis 配置项不是客户端那个。Redis 默认timeout0表示永不主动断开空闲客户端但很多托管环境或者运维规范会把它设成 300 秒甚至 60 秒来释放资源。如果客户端minEvictableIdleTimeMillis设了 10 分钟而服务端 60 秒就断那么池里会积累大量“客户端以为活着、实际已被 FIN 掉”的连接业务借到就报Unexpected end of stream。正确的做法是让客户端的空闲驱逐时间明显小于服务端超时。服务端 300 秒客户端就设 60 到 120 秒同时开testWhileIdle双保险。3.5 参数速查表参数Jedis 4.x作用推荐起点调整信号maxTotal池容量上限QPS×RT×2.5出现取连接超时则上调maxIdle最大空闲数等于 maxTotal周期性 RT 抖动则上调minIdle最小空闲数接近稳态并发冷启动延迟明显则上调maxWait借连接等待上限100~200ms排队超时多则结合上游重试评估testWhileIdle空闲校验true出现死连接报错时确认已开timeBetweenEvictionRuns驱逐扫描周期30s死连接残留久则调小minEvictableIdleTime空闲驱逐阈值60~120s必须小于服务端 timeouttestOnBorrow借出校验false极少数强一致场景开4. Spring Boot 落地配置参数想清楚了落到代码里还有一堆细节。这一节给出可以直接抄的配置以及几个静默失效的坑。4.1 Jedis 手动装配的完整写法Jedis 4.x 的JedisPoolConfig继承自GenericObjectPoolConfigJedis需要注意 setter 的参数类型已经换成了Duration。下面这份配置对应前面算出来的场景峰值 8000 QPS、单次 1.2ms、2.5 倍余量。import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.time.Duration; public class RedisPoolFactory { public JedisPool buildPool() { JedisPoolConfig config new JedisPoolConfig(); // 容量10 并发 × 2.5 余量向上取整 config.setMaxTotal(32); config.setMaxIdle(32); config.setMinIdle(8); // 借连接最多等 150ms超时快速失败 config.setMaxWait(Duration.ofMillis(150)); // 健康校验只在空闲扫描时做借出时不校验 config.setTestOnBorrow(false); config.setTestOnReturn(false); config.setTestWhileIdle(true); // 驱逐30 秒扫一次空闲超过 90 秒的连接回收 config.setTimeBetweenEvictionRuns(Duration.ofSeconds(30)); config.setMinEvictableIdleTime(Duration.ofSeconds(90)); config.setNumTestsPerEvictionRun(-1); // 扫描全部空闲连接 return new JedisPool(config, 127.0.0.1, 6379, 2000, // 命令 socket 超时单位毫秒 your-password, 0, // database null, // clientName建议填应用名便于排查 false); // ssl } }几个点值得单独说。setNumTestsPerEvictionRun(-1)表示每轮扫描检查全部空闲连接配合testWhileIdle能保证死连接被及时发现如果空闲连接很多设成 -1 会增加一点开销这时候可以改成固定数量比如 32。clientName那一项强烈建议填上应用名线上用CLIENT LIST排查时能一眼看出是哪套服务占着连接省掉大量扯皮时间。如果是 3.x 版本上面所有Duration参数要换成对应的xxxMillis长整型方法名也略有差异。升级版本时这部分代码必然要改建议封装成独立的配置类别散落在各处。4.2 Spring Boot 自动配置的 YAML 写法Spring Boot 2.x 用spring.redis.*Spring Boot 3.x 改成了spring.data.redis.*前缀别写错写错了配置不生效还不报错。spring: data: redis: host: 127.0.0.1 port: 6379 password: your-password database: 0 timeout: 2000ms client-name: order-service lettuce: pool: max-active: 32 max-idle: 32 min-idle: 8 max-wait: 150ms time-between-eviction-runs: 30s min-evictable-idle-time: 90s这是 Lettuce 的写法。注意spring.data.redis.lettuce.pool下的参数和 Jedis 的 spring 前缀spring.data.redis.jedis.pool名字基本对应但max-active对应max-total别混。还有一个坑要反复强调Lettuce 池化必须依赖commons-pool2。如果项目里没有这个依赖上面pool下的所有配置都会被忽略Spring Boot 会退回共享连接模式。表现是配置改了没反应连接数始终是每节点一条。加上依赖implementation org.apache.commons:commons-pool2:2.11.0Maven 对应commons-pool2的坐标。启动后如果想验证池化有没有生效可以在运行一段时间后执行redis-cli CLIENT LIST看当前应用的连接数如果稳定在min-idle以上、随流量上涨说明池化起作用了如果始终只有一两条大概率是没生效。4.3 集群与哨兵模式下的连接数放大单机模式的连接数就是池大小但集群模式完全不是。Jedis 的JedisCluster内部是每个节点一个池也就是说maxTotal32、三主三从可读从节点的情况的六节点集群最坏情况单个应用实例就有 6 × 32 192 条连接。计算总量时必须乘上节点数。20 个应用实例 × 6 节点 × 32 3840已经接近前面说的 7000 安全水位线的一半了。这种情况下正确的做法是把单节点的maxTotal降下来比如降到 16总量降到 1920同时确认单节点 16 条连接足够支撑分到该节点的流量。哨兵模式简单一些客户端连的是主节点池大小就是实际连接数但从节点读取读写分离时也要单独算。另外哨兵切换瞬间旧连接会全部失效如果maxWait设得太长切换期间线程会大量堆积建议在客户端加一层重试并配合较短超时。4.4 一个容易被忽略的启动顺序问题连接池是懒加载的第一次请求才建第一批连接。这意味着应用刚启动、流量刚切过来的那一瞬间会有几十个请求同时触发建连耗时明显高于稳态。如果上游有严格的超时比如网关 200ms冷启动阶段很容易触发一批失败。解决办法是预热在应用启动完成后主动借出minIdle条连接再归还让池子提前填充。实现很简单监听ApplicationReadyEvent循环执行几次GET空 key 即可。这个小动作在滚动发布时特别有用能显著降低发布期间的首包错误率。5. 压测与调优从监控数字反推配置参数是算出来的但算得对不对得靠数据验证。这一节讲怎么设计一轮有针对性的压测。5.1 压测前的基线采集不要一上来就压满。先采集三个基线数据单次命令的平均耗时不含业务逻辑、服务端当前连接数和maxclients、应用实例数。这三个数字决定了安全区间。单次耗时用简单循环测就行并发 1 跑一万次取平均。这个值通常比想象的高跨机房环境 1ms 到 3ms 都正常同机房 0.3ms 到 0.8ms。这个数字直接进公式别凭感觉估。然后算理论连接数再往上给余量得到一个初始maxTotal。压测的目的是验证这个值在目标 QPS 下不成为瓶颈而不是从零开始试。5.2 三组典型压测结果的解读下面三组数据来自模拟环境现象很有代表性。第一组maxTotal8目标 5000 QPS。结果QPS 卡在 2600 上不去应用侧线程大量处于等待Redis 服务端 CPU 只有 15%连接数稳定在 8。结论很明确池子太小客户端自我限流。把maxTotal提到 32 后 QPS 直接跑到 6000。第二组maxTotal200目标 5000 QPS。结果稳态 QPS 达标但 P99 从 12ms 涨到 80ms服务端连接数在 180 上下波动偶发max number of clients reached。原因是池子太大慢请求堆积时全部压到服务端加上其他应用也在连总连接数超了。把maxTotal降到 40 后 P99 回落到 15ms。第三组minEvictableIdleTime600s服务端timeout60s。结果低峰期过后请求开始零星报Unexpected end of stream报错率大概 0.3%。这就是典型的客户端空闲阈值大于服务端拿到死连接。把客户端降到 90s 并开启testWhileIdle后报错消失。三组数据说明的是同一件事连接池的问题很少表现为“慢”更多表现为“卡在某个固定数字上”或者“偶发但稳定的报错”。看到 QPS 卡住不动、CPU 却很闲第一反应就该是池子。5.3 监控指标怎么接Jedis 侧的指标可以直接从JedisPool拿getNumActive()借出数、getNumIdle()空闲数、getNumWaiters()等待线程数。这三个值上报到监控系统后能做出一张很直观的图active 逼近 maxTotal 且 waiters 持续大于 0说明容量不够active 很低但 waiters 很高说明连接泄漏或者有慢命令占着连接不放。Lettuce 侧稍微麻烦一点需要通过 Micrometer 拿。Spring Boot 提供了lettuce指标前缀开启方式是配置management.metrics.enable.lettucetrue。能拿到的指标包括命令耗时分布、连接数、活跃 channel 数。Redisson 也有对应的 Micrometer 集成。除了客户端指标服务端侧一定要看INFO clients里的connected_clients和blocked_clients以及INFO stats里的rejected_connections。rejected_connections大于 0 就是明确的信号有人被拒了得赶紧查是谁在狂建连接。告警阈值建议这样设waiters 0持续 1 分钟以上报警说明有排队active/maxTotal 0.85持续 5 分钟报警容量水位rejected_connections有任何增量立即报警。这套阈值比盯着 RT 曲线管用得多因为连接池的问题是“原因指标”RT 是“结果指标”等 RT 报警已经晚了。6. 线上高频问题排查实录下面这几个现象在排查工单里出现的频率最高基本能覆盖八成连接池相关故障。6.1 连接泄漏池子慢慢被吃干净典型表现应用跑上几个小时或几天后active数缓慢爬升并最终等于maxTotal然后开始大量取连接超时。重启后恢复过几天再次出现。这种形态几乎可以断定是连接泄漏——某条代码路径借了连接没还。Jedis 的泄漏最常见于Jedis jedis pool.getResource()之后在try块里抛了异常而finally里没有close()或者写错了变量。另一种隐蔽的情况是用了jedis.close()但对象在中途被重新赋值关掉的是新对象。定位手段有两个。一是开commons-pool2的泄漏检测set abandonedConfig相关参数removeAbandonedTimeout、logAbandoned它会把未归还连接的申请堆栈打出来。这个功能有性能开销只在排查期开。二是定期 dump 线程栈看哪个方法在GenericObjectPool.borrowObject附近挂着顺藤摸瓜。Lettuce 用共享连接时不会有这个问题但池化之后同样的规则适用——借出的StatefulConnection必须close()归还。6.2 Unexpected end of stream 的三种成因这个报错翻译过来就是“读响应时连接被对端关了”。三种常见原因排查顺序如下。第一空闲连接被服务端断开。前面的第三种压测场景就是。判断方法看报错是否集中在低峰期后、请求稀疏的接口上。解决方式是客户端空闲驱逐时间小于服务端timeout并开testWhileIdle。第二服务端重启或主从切换。这种报错是突发的、全量的同时服务端会有重启日志或切换记录。连接池对此无能为力需要在客户端加重试并且重试要新建连接而不是复用旧的。注意重试次数别太多避免切换期间把新主压垮。第三网络中间设备负载均衡、防火墙静默断连。有些设备会有空闲连接超时比如 5 分钟没流量就断且不通知两端。这种情况下客户端和服务端都以为连接活着。处理思路和服务端超时一样把客户端空闲驱逐时间调到小于中间设备的超时并且开testWhileIdle。三种成因的现象很接近区分靠的是时间分布集中在低峰期是第一种集中在某个时间点且全量是第二种周期性出现且和流量模式无关是第三种。6.3 池子满了但看起来都很闲有一种情况特别迷惑active打满waiters大量堆积但服务端 CPU 很低、connected_clients也不高。这意味着连接确实被借出去了但没有在被高效使用。可能的原因某条命令耗时异常长比如在大 key 上执行KEYS或者SMEMBERS一条连接被占住几十秒或者用了阻塞命令BLPOP但超时设得很长又或者事务里先MULTI之后因为异常没走EXEC/DISCARD连接卡在事务状态。排查手段是用CLIENT LIST看每个连接的cmd、age、idle字段。cmd显示当前正在执行的命令如果一堆连接的cmd停在大 key 操作上问题就找到了。age特别大的连接要重点关注正常连接会被频繁复用age不会太大。预防手段是在客户端侧加慢命令监控把耗时超过阈值的命令连同 key 一起记日志。别指望服务端的SLOWLOG它的默认阈值是 10ms很多“相对慢”的命令进不去但在高并发下已经足够拖垮连接池。6.4 常见问题速查表现象最可能原因快速验证处理动作QPS 卡在固定值服务端 CPU 低maxTotal 过小看 waiters 是否持续 0按公式上调 maxTotalactive 缓慢爬升直至打满连接泄漏开 abandoned 检测看堆栈修复未 close 的代码路径低峰期后零星Unexpected end of stream客户端空闲阈值 服务端对比两侧配置客户端调到 90s 并开空闲校验某时间点全量报错服务端重启/主从切换查服务端日志加重试 缩短超时P99 抖动且周期性出现maxIdle 远小于 maxTotal看空闲连接是否被批量销毁maxIdle 调到接近 maxTotalmax number of clients reached实例数 × 池大小超限算总量对比 maxclients降单实例池大小或扩服务端配额线程全卡在取连接maxWait 过大或 -1看线程栈改成 100~200ms 快速失败6.5 几个固化下来的小习惯讲几个不写在文档里、但确实能省事的做法。给每个应用的连接加上clientName格式统一成“应用名-环境-实例序号”。出问题时CLIENT LIST一拉按名字前缀就能统计出谁占了多少连接不用一个个问。把连接池指标接入统一监控并且给waiters单独配一条曲线。这个指标比 active 更敏感active 高可能只是流量大waiters 大于 0 就说明真的有人在等是容量不足的直接证据。压测环境里故意把服务端timeout调小比如 30 秒跑一轮长稳能提前把客户端空闲阈值配错的问题暴露出来。这个操作成本很低收益很大比在生产上踩雷强。版本升级时专门回归连接池配置。Jedis 3.x 升 4.x、Spring Boot 2.x 升 3.x配置前缀和参数类型都变很容易出现“配置写了但没生效”的静默失败。验证方法就是前面说的压一下看连接数有没有起来。跨机房访问 Redis 时把maxWait和 socket 超时都往上调一档。跨机房 RTT 通常 1ms 到 3ms抖动也比同机房大按同机房的经验值配会频繁误判超时。7. 我在实际配置中形成的判断顺序踩过几次坑之后我现在拿到一套 Redis 连接池配置基本按固定顺序过一遍很少再出问题。先看客户端是哪个。Jedis 就必须池化且maxTotal要按并发算Lettuce 先确认是不是真的启用了池化有没有 commons-pool2、有没有配 pool 属性很多“调了没用”的困惑都出在这里Redisson 则是先看节点数和实例数相乘之后的总量。再看容量。用 QPS 乘耗时算出理论并发乘 2 到 3 倍余量定maxTotal然后乘实例数、乘节点数确认总量不超过服务端maxclients的七成。这一步做完八成的容量问题就规避了。接着对齐时间参数。服务端timeout、客户端空闲驱逐时间、驱逐扫描周期这三个必须形成“服务端 客户端 扫描周期”的递减关系。任何一处反了都会出现间歇性的死连接报错。最后配超时和失败策略。maxWait给个几百毫秒以内的有限值让故障快速暴露而不是堆积。业务侧要能接受这种失败加合理重试和降级而不是靠无限等待来“提高成功率”。这套顺序不复杂难的是每次都认真走一遍。连接池的问题有个特点配置错了不会立刻爆炸而是等到某个流量高峰或者某个低峰期才露头。等到那时候再回头算成本比一开始花二十分钟推一遍高得多。我自己现在会把这套计算过程直接写在配置文件的注释里写上当时的 QPS 假设和计算式过半年再回来看还能知道这个 32 是怎么来的不至于被下一个人随手改成 128。