这是典型的空闲连接未及时释放问题:客户端未复用连接或未配置空闲超时,服务端timeout对PUBSUB等连接无效,keepalive仅保活不清理,连接池过大易致OOM,需结合客户端回收策略与服务端timeout兜底。

Redis客户端连接数暴增但QPS很低,内存却持续上涨
这是典型的空闲连接未及时释放问题。Redis服务端本身不主动断开长期空闲的客户端,全靠客户端或中间层管理生命周期。连接对象(尤其是带订阅、Pipeline或未设超时的)会持续占用服务端内存和文件描述符,最终触发 maxclients 拒绝新连接,或系统级 Too many open files 错误。
实操建议:
- 检查客户端是否复用连接:Node.js 的
ioredis默认启用连接池,但若每次请求都 newRedis()就会不断新建连接;Python 的redis-py中ConnectionPool必须显式传入,直接用Redis(host=...)会隐式创建新池 - 确认客户端空闲连接回收配置:如
ioredis的poolOptions.idleTimeoutMillis(默认 0,即永不过期),应设为 30000(30秒);redis-py的connection_pool.max_idle_time_seconds(需 v4.6.0+) - 服务端加兜底:在
redis.conf中设置timeout 60(单位秒),对无交互的普通连接生效;注意该参数对PUBSUB、MONITOR、带CLIENT SETNAME的连接无效
使用 keepalive 能否解决 Redis 连接堆积
不能。TCP keepalive 只探测链路是否断开,不通知 Redis 服务端“这个连接已无业务意义”。即使启用了 tcp-keepalive 300(redis.conf),它仅防止 NAT 超时断连,不会触发连接清理逻辑。
实操建议:
- keepalive 是保活手段,不是清理手段:它让中间网络设备不丢弃连接,反而可能延长僵尸连接存活时间
- 真正起作用的是客户端自身的连接池空闲驱逐策略,或者服务端的
timeout配置(对非特殊连接) - 若用 Nginx 或 Envoy 做代理,需额外配置其 upstream 的 idle timeout(如 Nginx 的
keepalive_timeout),否则代理层连接不释放,后端 Redis 看到的仍是活跃连接
Redis PUBSUB 客户端连接无法被 timeout 清理
没错。timeout 参数对所有订阅类连接(SUBSCRIBE、PSUBSCRIBE)、MONITOR、以及设置了 CLIENT SETNAME 的连接完全无效。这类连接一旦建立,就会一直挂在那里,直到客户端主动 UNSUBSCRIBE 或断开。
实操建议:
- 避免在长周期任务中反复新建 SUBSCRIBE 连接:应复用单一连接,并用
CLIENT SETNAME标记用途,便于CLIENT LIST里识别定位 - 定期巡检:用
redis-cli CLIENT LIST | grep "sub\|cmd=subscribe"查看订阅连接,结合idle字段判断是否异常空闲(但注意字段值在此类连接中恒为 0) - 业务层加心跳与超时:例如在订阅逻辑里启动定时器,10分钟无消息则主动
UNSUBSCRIBE+QUIT,而不是依赖服务端
连接池 maxConnections 设太高反而导致 OOM
是的。连接池上限不是越大越好。每个 Redis 连接在服务端至少占用 10KB 内存(协议解析缓冲区 + 结构体),1000 个空闲连接就是 10MB;若还开启 SSL,单连接开销翻倍。更危险的是客户端本地资源:Java 的 JedisPool 每个连接对应一个 socket 和线程等待,Python 的 redis-py 异步客户端(aioredis v2)每个连接也持有 event loop 句柄。
实操建议:
- 按实际并发峰值设池大小:例如 QPS 500、平均响应 20ms,则理论并发约 10,池大小设 20–30 即可,留 100% 余量
- 监控真实连接数:用
INFO clients看connected_clients,用CLIENT LIST统计不同addr或name的数量,比盲目调大池子更有效 - 拒绝“保险起见”式配置:把
max_connections=1000写死在配置里,等于给内存泄漏开了绿灯
最麻烦的是那些没走连接池、又没设超时、还混着 PUBSUB 的脚本类调用——它们往往藏在定时任务或旧管理后台里,平时不报警,一出事就占满 maxclients。得去 CLIENT LIST 里挨个看 addr 和 cmd 字段,才能揪出来。










