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 响应
threadsTopic listener、RemoteService 等用户回调/任务
connectionPoolSize单机模式普通命令连接上限
masterConnectionPoolSize集群/主从模式每个 master 的连接上限
slaveConnectionPoolSize每个 replica 的读连接上限
subscriptionConnectionPoolSizePub/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 无法判断客户端池是否健康。


生产建议

  1. 普通 Spring Cache/RedisTemplate 优先使用 Lettuce 默认共享连接模型;需要池化时说明原因。
  2. 阻塞命令、事务和 Pub/Sub 使用专用连接,不与普通缓存流量争池。
  3. 所有 pool、业务 executor、在途 future 和重试队列必须有界。
  4. max-wait + command timeout 应小于 HTTP/RPC 总 deadline,超时后停止无意义工作。
  5. 不在 Netty/Reactive EventLoop 执行阻塞或 CPU 密集任务。
  6. Cluster 按每节点、每 JVM 计算总连接数;扩容应用前确认 Redis maxclients
  7. 先治理慢命令与大 key,再考虑增加连接或线程。
  8. 用与生产相同的 key 大小、pipeline、TLS、Cluster 和持久化配置压测 p99/p999。

面试结论

Redis 服务端没有通用命令线程池,核心命令仍由主线程串行执行;I/O threads 只并行网络阶段。Lettuce 基于 Netty,原生连接线程安全,普通命令多数可共享连接,池主要用于阻塞/事务等需要独占状态的场景;Jedis 单连接不能跨线程共享,通常使用 JedisPool;Redisson 的 nettyThreads、业务回调 threads、命令连接池和订阅连接池是不同资源。连接和线程不是越多越快,必须有界并结合主线程负载、池等待、EventLoop 延迟和总连接数压测。


相关