Redis 客户端线程池与连接池
→ 返回 Redis
客户端侧至少有三种资源,不能混为一个“Redis 线程池”:
业务线程池 执行业务代码、调用同步 API
Netty EventLoop Lettuce/Redisson 发送命令、读取响应、解码
Redis 连接池 管理 TCP 连接;池中元素是 connection,不是 thread线程池解决“谁执行代码”,连接池解决“复用多少条 TCP 连接”,Redis 服务端 I/O threads 解决“服务端如何并行网络 I/O”。三者调优目标不同。
Lettuce:共享连接通常已经足够
Lettuce 基于 Netty,原生 StatefulRedisConnection 是线程安全的。普通非阻塞、非事务命令可由多个应用线程共享一条连接:命令被编码进入 channel,响应按协议顺序匹配各自 future。
Thread A ─ GET a ─┐
Thread B ─ SET b ─┼─► shared StatefulRedisConnection ─► Redis
Thread C ─ INCR c ┘连接池不是默认越大越好。Lettuce 官方明确指出:多数场景不需要池化,多连接不会让 Redis 单分片的命令执行自动并行,反而增加连接、心跳、缓冲区和故障恢复成本。
哪些操作需要独占连接
| 操作 | 原因 |
|---|---|
BLPOP / BRPOP / 阻塞 XREAD | 当前连接会进入阻塞状态,不能承载普通命令 |
MULTI/EXEC | 事务状态绑定连接,不能让其他线程命令混入 |
WATCH | 乐观事务监视状态属于连接 |
| Pub/Sub | 订阅连接进入专用消息模式 |
| 大批 Pipeline | 避免无限占用共享连接输出/待响应队列 |
Spring Data Redis 默认让 LettuceConnectionFactory 的普通非阻塞、非事务操作共享线程安全的 native connection;阻塞和事务操作使用独立 connection provider。RedisTemplate 负责按操作获取/归还连接,可以作为线程安全组件注入;不要跨线程缓存并共享一个 Spring RedisConnection 实例,因为该包装对象本身不是线程安全的。
Jedis:一个连接不能并发共享
Jedis 的普通连接是同步、阻塞模型,单个 Jedis 实例不应被多个线程同时使用。典型方式是每次操作从 JedisPool 借一条连接:
try (Jedis jedis = jedisPool.getResource()) {
return jedis.get(key);
} // close() 归还池,不是关闭整个池池大小不足时业务线程等待,过大时 Redis 连接数、客户端内存和上下文切换增加。必须设置 maxWait,避免池耗尽后请求无限等待;连接泄漏会表现为 active 持续等于 maxTotal。
Redisson:Netty 线程和连接池是两组参数
Redisson 内部维护 Netty EventLoop 和按节点划分的异步连接池:
| 配置方向 | 职责 |
|---|---|
nettyThreads | 发送命令、读取并解码 Redis 响应 |
threads | Topic listener、RemoteService 等用户回调/任务 |
connectionPoolSize | 单机模式普通命令连接上限 |
masterConnectionPoolSize | 集群/主从模式每个 master 的连接上限 |
slaveConnectionPoolSize | 每个 replica 的读连接上限 |
subscriptionConnectionPoolSize | Pub/Sub、锁通知等订阅连接上限 |
这些值可能按节点相乘。例如 6 个 Redis 节点、每节点几十条连接,再乘多个应用实例,总连接数会迅速放大;规划时必须用“每 JVM × JVM 数 × 节点数”估算,而不是只看单个配置值。
不要在 Netty listener 或 reactive 回调里做慢 SQL、阻塞 HTTP、文件 I/O 或长时间 JSON 计算。阻塞 EventLoop 会让同一线程负责的其他连接无法及时读写,最终表现为大量 RedisTimeoutException,即使 Redis 服务端本身并不忙。重业务应切换到有界业务 Executor,并设置队列与拒绝策略。
Spring Boot 连接池配置怎么理解
spring:
data:
redis:
timeout: 2s
connect-timeout: 1s
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 500ms| 参数 | 含义 | 过小/过大风险 |
|---|---|---|
max-active | 同时可借出的连接上限 | 小:等待;大:连接和资源膨胀 |
max-idle | 最大空闲连接 | 小:高峰后频繁重建;大:长期占连接 |
min-idle | 保持的最小空闲数 | 大:启动即建立很多无用连接 |
max-wait | 借连接最长等待 | 应小于总请求 deadline,禁止无限等待 |
timeout | 命令响应超时 | 过短误杀,过长堆积请求 |
connect-timeout | 建连超时 | 需覆盖正常网络握手,但不能拖垮故障恢复 |
对只使用普通 GET/SET 的 Lettuce 应用,先验证是否真的需要池。加入 commons-pool2 和 pool 配置的主要理由应是阻塞命令、事务/连接状态隔离或经过压测确认,而不是把 JDBC 连接池经验直接照搬到 Redis。
连接池大小不能只套公式
Redis 调用的平均 RTT 很短且 Lettuce 可多路复用,因此“Tomcat 200 线程就配 200 Redis 连接”通常没有依据。容量评估至少考虑:
并发在途请求 ≈ 峰值 QPS × p99 Redis 往返秒数但这只是起点,还要区分:
- 同步 Jedis:一条连接同一时刻通常服务一个调用;
- 共享 Lettuce:一条连接可以承载多个在途命令,但队列不能无限增长;
- 阻塞命令:一条连接可能被占用数秒甚至无限期,必须独立预算;
- Cluster/replica read:客户端对多个节点分别建连;
- Pipeline:连接数少也能提高吞吐,但单次批量过大会放大内存和尾延迟。
先设置小而有界的池与等待时间,通过池等待、超时、Redis QPS和 p99 逐步增加;如果 Redis 主线程已饱和,增加客户端池只会排更长的队。
异步、Reactive 与虚拟线程
| 模型 | 优点 | 主要风险 |
|---|---|---|
| 同步 API + 平台线程 | 简单直观 | 高并发时线程等待和池耗尽 |
| 同步 API + 虚拟线程 | 降低等待线程成本 | Redis/连接/队列容量没有变,仍需限流 |
| Lettuce async | 少量线程承载大量在途请求 | future 队列无界、回调阻塞 EventLoop |
| Reactive | 组合与背压能力强 | 阻塞代码混入 reactive 链导致 EventLoop 卡死 |
异步 API 只是不让调用线程原地等待,不会自动产生端到端背压。连接断开期间,客户端可能缓冲待发送命令;若请求队列无界,故障持续时会耗尽 JVM 内存。应配置总 deadline、最大在途请求、断线行为和有界队列。
不要为了“异步”把同步 RedisTemplate 随意包进无限 CompletableFuture/@Async 线程池;这只是把阻塞转移到另一个池,并可能同时耗尽业务线程、Redis 连接池和下游容量。
阻塞命令的隔离
普通缓存连接:GET / SET / DEL,短超时
队列消费连接:BLPOP / XREAD BLOCK,专用连接和独立线程
Pub/Sub 连接:长期订阅,专用连接
事务连接:MULTI / WATCH,操作结束立即归还阻塞命令的 socket 被 Redis 挂起等待数据,不等于 Redis 主线程被阻塞;但它会占住客户端连接。若与普通缓存共用一个小池,几个 XREAD BLOCK 0 就能让所有 GET 借不到连接。
常见故障定位
| 现象 | 首要检查 |
|---|---|
| pool exhausted / borrow timeout | 连接泄漏、阻塞命令混池、池过小、Redis 变慢 |
| RedisTimeoutException 且服务端 CPU 低 | Netty EventLoop 被阻塞、网络丢包、客户端队列积压 |
| Redis CPU 高且 p99 高 | slowlog、大 key、Lua、服务端主线程瓶颈 |
| 连接数异常增长 | 多个 client 实例未关闭、Cluster 每节点池相乘、健康检查过多 |
| 故障恢复后瞬间流量冲击 | 离线命令缓冲与重试同时回放,缺少退避/熔断 |
| Reactive 仍出现大量线程 | 链中混入阻塞调用或错误使用 scheduler |
建议同时观察:
INFO clients
INFO stats
INFO commandstats
CLIENT LIST
SLOWLOG GET 20应用侧观察连接池 active/idle/waiters、命令 timeout、Netty pending tasks、在途请求数、重试次数和 JVM event-loop 延迟。只看 Redis connected_clients 无法判断客户端池是否健康。
生产建议
- 普通 Spring Cache/
RedisTemplate优先使用 Lettuce 默认共享连接模型;需要池化时说明原因。 - 阻塞命令、事务和 Pub/Sub 使用专用连接,不与普通缓存流量争池。
- 所有 pool、业务 executor、在途 future 和重试队列必须有界。
max-wait + command timeout应小于 HTTP/RPC 总 deadline,超时后停止无意义工作。- 不在 Netty/Reactive EventLoop 执行阻塞或 CPU 密集任务。
- Cluster 按每节点、每 JVM 计算总连接数;扩容应用前确认 Redis
maxclients。 - 先治理慢命令与大 key,再考虑增加连接或线程。
- 用与生产相同的 key 大小、pipeline、TLS、Cluster 和持久化配置压测 p99/p999。
面试结论
Redis 服务端没有通用命令线程池,核心命令仍由主线程串行执行;I/O threads 只并行网络阶段。Lettuce 基于 Netty,原生连接线程安全,普通命令多数可共享连接,池主要用于阻塞/事务等需要独占状态的场景;Jedis 单连接不能跨线程共享,通常使用 JedisPool;Redisson 的 nettyThreads、业务回调 threads、命令连接池和订阅连接池是不同资源。连接和线程不是越多越快,必须有界并结合主线程负载、池等待、EventLoop 延迟和总连接数压测。