Redis 连接数满了服务端有错误信息吗?连接数不增加却命令超时?6.2 实测连接池、超时与大 key 怎么判定

这一篇讲什么

这一篇讲三件经常被搅在一起的事:

  • 连接数:满了以后谁报错、报什么?服务端会不会留下记录?
  • 超时:「连接数没涨,命令却在超时」是怎么回事?命令超时和连接池耗尽,在 Java 里怎么区分?
  • 大 key:怎么找出来?删一个、读一个,会让其他请求等多久?

实测环境 :CentOS 7.9,Redis 6.2.4,实验实例端口 6380。Java 端 Spring Boot 4.1.1 + Lettuce 7.5.2,配置:

yaml 复制代码
spring:
  data:
    redis:
      timeout: 5s
      lettuce:
        pool:
          max-active: 8
          max-wait: 3s

1. 连接数满了

1.1 客户端看到什么,服务端记了什么

maxclients 默认 10000。为了好测,改成 20,然后开 30 个连接,每个发一条 PING:

复制代码
20 个连接收到 +PONG                                   (第 1~20 个)
10 个连接收到 -ERR max number of clients reached      (第 21~30 个)

服务端日志里一行相关记录都没有。唯一留下痕迹的地方是:

复制代码
127.0.0.1:6380> info stats
...
rejected_connections:10

所以「连接数满了服务端会有错误信息吗」的答案是:没有日志,只有 INFO stats 里 rejected_connections 这个计数器在涨。 要监控连接数打满,盯的是它。

启动日志里那句 Increased maximum number of open files to 10032 (it was originally set to 1024). 也和连接数有关:每个连接占一个文件描述符,Redis 启动时会按 maxclients 去调高自己的上限。

1.2 Java 应用看到的

连接被占满时,Spring Boot(Lettuce)里:

复制代码
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis
  org.springframework.data.redis.connection.PoolException: Could not get a resource from the pool
  io.lettuce.core.RedisConnectionException: Unable to connect to 192.168.3.111/<unresolved>:6380
  java.io.IOException: 你的主机中的软件中止了一个已建立的连接。

max number of clients reached 这句话在应用里看不到 ,只有一句「连接被中止」------ 和 protected-mode 拒绝远程连接时的报错一模一样(那次的原文同样是这一句)。看到它,要同时想到这两种可能,去服务端看 rejected_connections 或者 CONFIG GET protected-mode。

2. 连接数没涨,命令却在超时

2.1 Spring 默认只用一条连接

先看一个很多人没意识到的事实。50 个线程同时疯狂 GET 3 秒,期间在服务端 CLIENT LIST 看来自应用的连接:

复制代码
并发中服务端看到的连接数 = 1
3 秒共完成 GET 166309 次

只有 1 条。 Spring Data Redis 用 Lettuce 时,默认让普通命令共用一条连接(Lettuce 的连接是线程安全的,命令在一条连接上排队发出)。配置的连接池 max-active: 8,这时候根本没用上。

2.2 服务端卡住时:所有请求一起超时

让服务端卡 8 秒(DEBUG SLEEP 8,模拟一条慢命令或者一次长时间的阻塞),这期间 20 个线程各发一次 GET:

复制代码
20 个请求总耗时 5017ms,结果:
   20 个: QueryTimeoutException / RedisCommandTimeoutException: Command timed out after 5 second(s)
之后服务端看到的连接数 = 1

20 个请求同时超时,连接数一直是 1。 这就是「连接数不增加,命令超时」:问题不在连接,在于那唯一的一条连接上,前面有东西卡着。去服务端看 slowlog get,看有没有执行很久的命令(第 4 节的大 key 就是典型)。

2.3 连接池耗尽:报的是另一句

连接池什么时候才用上?实测里是阻塞命令 (BLPOP):它要独占一条连接,这时才从池子里借。8 个线程各发一个等 10 秒的 BLPOP,1 秒后在服务端看:

复制代码
连接 8 条:blpop 7 条,client 1 条(执行 CLIENT LIST 的那条)

这时再发第 9 个 BLPOP:

复制代码
耗时 3007ms:
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis
  org.springframework.data.redis.connection.PoolException: Could not get a resource from the pool
  java.util.NoSuchElementException: Timeout waiting for idle object, maxWaitDuration=PT3S

同一时刻普通 GET 完全正常(0ms)------ 它走的是那条共享连接,不经过池子。

2.4 两种超时怎么分

报错 等的是哪个配置 说明
RedisCommandTimeoutException: Command timed out after 5 second(s) spring.data.redis.timeout 命令发出去了,Redis 没在时限内回。看服务端慢命令、阻塞
NoSuchElementException: Timeout waiting for idle object, maxWaitDuration=PT3S lettuce.pool.max-wait 连接池里没有空闲连接。看是不是阻塞命令占着连接不还

前者调大连接池没有用;后者调大连接池才有用。

3. 大 key 怎么找

造了一组数据:一个 100 万字段的 hash、一个 100 万元素的 list、一个 10MB 的 string,外加 10 万个普通 key。

3.1 --bigkeys:按「个数」排,不是按内存

复制代码
$ redis-cli -p 6380 --bigkeys
...
Biggest   list found '"biglist"' has 1000000 items
Biggest   hash found '"bighash"' has 1000000 fields
Biggest string found '"bigstr"' has 10485760 bytes

扫完整个库用了 1.47 秒。输出开头写着它是用 SCAN 遍历的(You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands)。注意它的口径:list、hash 按元素个数,string 按字节。「100 万个字段」和「10MB」没法放在一起比,它也不会告诉你哪个更占内存。

3.2 --memkeys:按内存排

复制代码
$ redis-cli -p 6380 --memkeys
...
Biggest   list found '"biglist"' has 12957989 bytes
Biggest   hash found '"bighash"' has 64388760 bytes
Biggest string found '"bigstr"' has 12582960 bytes

用了 0.59 秒。这样就能看出 hash 占 64MB,是 list 和 string 的 5 倍。另外,10MB 的 string 实际占 12.58MB ------ 分配器按块分配,会有多出来的部分。

3.3 MEMORY USAGE:单个 key 的内存

key 默认 SAMPLES 0
bighash 64,388,760 字节(3ms) 63,588,768 字节(105ms)
biglist 12,957,989 字节(5ms) 12,956,803 字节(3ms)
bigstr 12,582,960 字节 12,582,960 字节

默认是抽样估算 ,对 100 万字段的 hash 误差约 1%,3 毫秒;SAMPLES 0 是逐个元素精确计算,要 105 毫秒 ------ 这 105 毫秒里 Redis 什么别的都不做。在线上排查时,默认值通常就够了。

4. 删一个大 key、读一个大 key,别人要等多久

测法:一个后台客户端每 10 毫秒发一次 PING,记每次的往返时间;同时对那个 100 万字段的 hash 做操作。

操作 命令本身 同时 PING 最长等了 PING 中位数
DEL bighash 212ms 198.9ms 0.16ms
UNLINK bighash 3ms 1.3ms 0.18ms
HGETALL bighash 581ms 324.9ms 0.18ms

slowlog(默认阈值 10 毫秒)里记下了:

复制代码
hgetall bighash   332512 微秒
del bighash       208795 微秒

读法:

  • DEL 一个 100 万字段的 hash,整个 Redis 卡住约 0.2 秒 。释放这么多元素的内存是在主线程上做的,这期间所有客户端的所有命令都在等。默认 lazyfree-lazy-user-del no,也就是 DEL 就是同步删。
  • UNLINK 只要 3 毫秒 ,PING 几乎没受影响 ------ 它先把 key 从库里摘掉,内存交给后台线程慢慢释放。之后 INFO memory 的 lazyfree_pending_objects 回到 0,说明后台已经释放完。删大 key 用 UNLINK。
  • HGETALL 更慢 :服务端执行 332 毫秒(slowlog),客户端拿到全部结果花了 581 毫秒(多出来的是传输)。大 key 不光删的时候慢,读整个的时候一样会卡住别人 。读大 hash 用 HSCAN 分批,或者只取需要的字段(HGET / HMGET)。

这也就是第 2.2 节「所有请求一起超时」的一种来源:某个请求对大 key 执行了 HGETALL 或 DEL,共享连接上排在它后面的、以及所有其他客户端的请求,都得等它。

5. 速查

现象 看哪里 说明
客户端 ERR max number of clients reached INFO stats 的 rejected_connections;CONFIG GET maxclients 服务端日志不记
Java IOException: 你的主机中的软件中止了一个已建立的连接 rejected_connections、protected-mode 两种原因报错一样
并发高,连接数却只有 1 --- 正常,Lettuce 共享连接
一批请求同时 Command timed out after N second(s) SLOWLOG GET 服务端被慢命令卡住;调大连接池没用
Timeout waiting for idle object 阻塞命令 连接池被独占连接占满
想知道哪个 key 最大 --memkeys(按内存);--bigkeys(按个数) 跨类型比较用 --memkeys
单个 key 多大 MEMORY USAGE key 默认抽样;SAMPLES 0 精确但慢
删大 key UNLINK DEL 会卡住主线程
读大 key HSCAN / SSCAN / ZSCAN / LRANGE 分段 HGETALL 一次读完同样卡

下一篇:主从与哨兵 ------ 主从数据不一致、从库连不上主库、主从切换期间应用能不能写,以及哨兵设了 ACL 以后为什么不切换。

相关推荐
.Bryce.2 小时前
从零到一开发技术名词大全:从需求分析到 AI Agent 工程
java·网络·人工智能·agent·需求分析·agent评测
SL_staff2 小时前
JVS-APS的7个刚性约束设计:从无限产能幻觉到可执行排产的工程实践
java·spring cloud·全栈
无言听风雨丶2 小时前
Redis 运维实战与排错
运维·数据库·redis
苍何3 小时前
DeepSeek的Super-agent 设计,和我们的工程实践
后端
Patrick_Wilson3 小时前
第一次读 Go 项目:从两个源码文件弄清 package、import 和 go.mod
后端·go·编程语言
代码山河3 小时前
基本类型转换:自动类型转换与强制类型转换
java·学习·架构·教程·面向对象·项目
Patrick_Wilson3 小时前
执行 go build . 后,Go 怎样找到代码并生成程序
后端·go·编程语言
用户6919026813393 小时前
Redis 2:Redis 在怎么处理harness记忆
后端
Patrick_Wilson3 小时前
Go 变量不赋值时,为什么也有值
后端·go·编程语言