“Read timed out”本质是Lettuce共享连接阻塞与连接池耗尽,而非Redis服务端慢;慢查询仅触发问题,真正卡住的是客户端连接排队和线程等待。

“Read timed out”不是Redis慢,而是请求在客户端排队——慢查询只是导火索,真正卡住的是Lettuce共享连接+连接池耗尽。
为什么Redis慢查询会导致Spring Boot线程堆积
Redis本身是单线程,一个慢命令(如HGETALL、KEYS、SCAN无游标)会阻塞整个实例的后续命令;而Lettuce默认使用共享连接(StatefulRedisConnection),所有请求共用同一个连接。一旦某条命令执行超时或卡住,后续发往该连接的所有execute()调用都会排队等待,最终表现为线程在GenericObjectPool.borrowObject()或Future.get()处WAITING,而不是CPU飙升或Redis报错。
- 堆栈里出现
io.lettuce.core.RedisCommandTimeoutException,但Redis服务端slowlog get查不到记录 → 很可能是客户端连接池已满,新请求根本没发出去 - 监控发现
lettuce.pool.active持续等于max-active,且lettuce.pool.idle长期为0 → 连接被占满且未释放,大概率有连接泄漏或慢命令拖住连接 - 应用QPS没涨,但HTTP线程数持续上升 → 说明业务线程在等Redis响应,不是下游接口拖慢,而是Redis链路内部堵死
如何用AOP精准定位哪类Redis操作在拖慢系统
别依赖redis-cli --latency,它测的是PING往返,和你代码里opsForHash().entries()的真实耗时差十倍。必须在Spring Boot进程内拦截RedisTemplate.execute(),按命令类型分段计时:
- 只拦
execution(* org.springframework.data.redis.core.RedisTemplate.execute*(..)),覆盖execute、executePipelined、executeReadOnly,不拦getConnection()(否则会把连接池等待误判为Redis慢) - 用
StopWatch拆成三段:connection acquisition(连接获取)、serialization(序列化)、command execution(纯命令耗时),每段单独打日志 - 对不同命令设动态阈值:
GET/SET>20ms打ERROR;HGETALL/LRANGE>100ms打WARN;SCAN>500ms且isScan:true才告警,避免误杀合理慢操作 - 生产环境别全量开启
future.get()阻塞测时,改用采样:比如每1000次execute()中随机抽1次做完整计时
连接池配置不合理会让慢查询问题雪上加霜
很多人以为调大max-active就能解决问题,其实错得离谱。Lettuce连接池不是“越多越好”,而是要匹配实际并发模型:
-
max-active设太高(比如>64),反而加剧线程竞争,导致borrowObject()锁争用变高,大量线程卡在AbstractQueuedSynchronizer$ConditionObject.await() -
min-idle设为0是危险操作——流量低谷时连接全被回收,突增流量来临时要重新建连,触发TCP握手+认证开销,放大超时概率 - 必须配
max-wait(如5000ms),否则连接池耗尽时线程无限等待;同时配time-between-eviction-runs和min-evictable-idle-time防连接假死 - 云环境(如蓝易云)要特别注意安全组入站规则是否放通Redis端口,否则连接池会因“连不上”而非“读超时”持续重试,把
max-wait耗尽
最容易被忽略的三个点
第一,RedisTemplate的execute()返回Future,AOP里不调get()就永远测不准真实执行时间;第二,JdkSerializationRedisSerializer序列化一个大Map可能比Redis命令本身还慢,但日志里只显示“命令超时”;第三,scan类命令天然慢,但如果你没传SCAN的COUNT参数,默认每次只扫10个key,百万数据要扫十万次——这根本不是Redis慢,是你代码写法有问题。


















