当前位置: 首页 > news >正文

【Redis从入门到精通】第36篇:Redis客户端属性大揭秘——一个连接背后有多少状态

上一篇【第35篇】Redis为什么这么快——单线程也能跑出10万QPS的秘密
下一篇【第37篇】Redis服务器启动全流程——从redis-server到ready to accept


你敲下redis-cli,输入SET foo bar,回车,毫秒之间结果就回来了。这一切看起来轻松惬意,但在这背后,Redis服务器为你的连接维护了一个"小本本",里面密密麻麻记录着这个客户端的所有状态信息。这个"小本本"就是client结构体。今天,我们就来扒一扒一个Redis客户端连接到底有多少"秘密"。

client结构体——连接的"身份证"

在Redis源码中(server.h),client结构体是整个服务器与客户端交互的核心数据结构。每个redis-cli连接、每个主从复制链路、每个Sentinel监控连接,在服务器内部都是一个client实例。这个结构体有多大呢?粗略算一下,光字段数量就超过80个。我们挑重点来说。

先看看它的"五脏六腑":

client 结构体速览 ┌──────────────────────────────────┐ │ +-------------+ +------------+ │ │ | 网络层 | | 输入层 | │ │ | - fd | | - querybuf | │ │ | - ctime | | - argc/argv| │ │ | - flags | | - cmd | │ │ +-------------+ +------------+ │ │ +-------------+ +------------+ │ │ | 输出层 | | 状态层 | │ │ | - buf/bufpos| | - db | │ │ | - reply | | - name | │ │ | - sentlen | | - auth.. | │ │ +-------------+ +------------+ │ └──────────────────────────────────┘

网络层核心字段

字段类型含义值得注意的细节
fdint文件描述符普通客户端是socket fd;-1代表伪客户端(fake client)
ctimetime_t客户端创建时间用于计算连接存活时长,CLIENT LISTage字段来源于此
lastinteractiontime_t最后交互时间每次接收命令都会更新,超时检查的依据
flagsuint64_t客户端标志位一个字段承载数十种状态,比如主从角色、阻塞状态、事务状态等
namesds客户端名称通过CLIENT SETNAME设置,调试时非常有用

flags标志位详解—— 这可能是整个结构体里信息密度最高的字段。它是一个64位的无符号整数,每个bit代表一种状态:

flags 常见标志位 Bit 63 Bit 0 ┌──────────────────────────────────────────────┐ │...│MONITOR│MASTER│SLAVE│BLOCKED│MULTI│PUBSUB│...│ └──────────────────────────────────────────────┘ 主要标志位说明: REDIS_SLAVE (1<<0) — 这是一个从库连接 REDIS_MASTER (1<<1) — 这是一个主库连接 REDIS_MONITOR (1<<2) — 客户端执行了 MONITOR 命令 REDIS_MULTI (1<<3) — 客户端处于事务(MULTI)状态 REDIS_BLOCKED (1<<4) — 客户端被阻塞(如BLPOP) REDIS_DIRTY_CAS (1<<5) — WATCH的key被修改过 REDIS_CLOSE_AFTER_REPLY (1<<6) — 回复后关闭连接 REDIS_UNIX_SOCKET (1<<7) — 通过Unix socket连接 REDIS_LUA_CLIENT (1<<8) — 执行Lua脚本的伪客户端 REDIS_ASKING (1<<9) — 集群模式下ASK转向 REDIS_READONLY (1<<10) — 集群从节点只读 REDIS_PUBSUB (1<<11) — 客户端订阅了频道 REDIS_PRE_PSYNC (1<<12) — 从库等待PSYNC回复

踩坑提示CLIENT LIST中看到的flags字段(如N代表普通客户端、M代表master、S代表slave)就是由这些bit位映射而来的。如果你写了一个连接池,记得确认每个连接的状态标志是否正确——曾经有个生产事故就是因为某个连接被意外标记为MONITOR状态,导致服务器疯狂向它发送监控命令,最终OOM了。

输入缓冲区:命令的"缓冲区"

客户端发送过来的数据不是一次性就解析执行的,Redis设计了一套分层的输入缓冲区机制:

// client结构体中的输入相关字段structclient{sds querybuf;// 输入缓冲区,sds动态字符串size_tqb_pos;// querybuf已解析的位置intargc;// 当前命令的参数个数robj**argv;// 当前命令的参数列表structredisCommand*cmd;// 当前要执行的命令指针intreqtype;// 请求类型(内联命令还是RESP协议)};

输入处理的ASCII流程图:

网络字节流 │ ▼ ┌──────────┐ │ querybuf │ ← 原始数据append到这里 └────┬─────┘ │ readQueryFromClient() ▼ ┌──────────────┐ │ RESP协议解析 │ ← processMultibulkBuffer() └──────┬───────┘ │ 解析成功 ▼ ┌──────────────┐ │ argc/argv │ ← 解析结果存入这里 └──────┬───────┘ │ lookupCommand() ▼ ┌──────────────┐ │ cmd │ ← 在redisCommandTable中找到 └──────┬───────┘ │ 参数校验 (arity) ▼ ┌──────────────┐ │ 执行命令 │ ← call() └──────────────┘

关于querybuf有几个要点:

  1. 最大1GBquerybuf使用sds动态字符串,理论上可以很大。但Redis有一个硬限制——proto-max-bulk-len(默认512MB),批量参数总长度不能超过这个值。
  2. 多次读取querybuf是累积的。如果一次read没读完整条命令,下次事件触发时会继续append,然后尝试重新解析。
  3. qb_pos的作用:解析完一条命令后,qb_pos会标记已消费的位置。如果有剩余数据(管道模式),会从qb_pos之后继续解析下一个命令。
# 查看客户端输入缓冲区使用情况redis-cli CLIENT LIST|awk-F' ''{print $1, $NF}'# 输出示例:id=3 addr=127.0.0.1:54321 qbuf=0 qbuf-free=32768# qbuf=0 表示当前输入缓冲区没有未解析的数据(命令已被消费)# qbuf-free=32768 表示还有32KB空闲空间

踩坑提示:如果qbuf这个值持续增长,可能是客户端发了命令但Redis没解析——要么是协议格式错误,要么是命令不完整。曾经有人写了一个Java客户端,忘记在命令末尾加\r\n,导致Redis一直等数据,qbuf慢慢涨到几百MB,最终触发OOM杀手。

输出缓冲区:回复的"双通道"

Redis的回复发送也很有意思,它用了"固定缓冲区 + 链表"双通道设计:

// 输出相关字段structclient{charbuf[PROTO_REPLY_CHUNK_BYTES];// 固定大小输出缓冲区(默认16KB)intbufpos;// buf中已使用的字节数list*reply;// 输出缓冲区链表(用于大回复)size_treply_bytes;// reply链表中总字节数size_tsentlen;// 当前对象已发送的字节数};

输出机制示意图:

addReply() 被调用 │ ▼ ┌─────────────────────────────┐ │ 总回复量 < 16KB? │ └──┬──────────────┬──────────┘ YES NO │ │ ▼ ▼ ┌───────┐ ┌──────────────┐ │ buf[] │ │ buf[]写满后 │ │ │ │ 追加到reply链表│ └───┬───┘ └──────┬───────┘ │ │ └───────┬───────┘ ▼ ┌──────────────────────┐ │ 可写事件触发时 │ │ writeToClient() 发送 │ └──────────────────────┘

这个设计的精妙之处在于:

  • 小回复(如+OK\r\n:1\r\n)直接放进buf[16KB],零额外分配。
  • 大回复(如KEYS *返回几万条key)会创建reply链表节点,每个节点是一个clientReplyBlock
  • sentlen字段用于跟踪当前正在发送的链表节点已经发了多少字节,避免每次从头开始。
# 查看输出缓冲区状态redis-cli CLIENT LIST|awk-F' ''{print $1, $(NF-2), $(NF-1)}'# 输出示例:id=3 obl=0 oll=0 omem=0# obl: 固定缓冲区已用字节数 (output buffer length)# oll: 输出链表中的对象数 (output list length)# omem: 输出链表占用的总内存 (output memory)

踩坑提示:如果omem这个值持续增长,说明有客户端订阅了大量频道或者执行了返回海量数据的命令(如没加LIMIT的KEYS *),Redis正在拼命往回复链表里塞数据。配置client-output-buffer-limit就是为了防这种情况:

# redis.conf 中的配置# 普通客户端的输出缓冲区限制client-output-buffer-limit normal000# 从库客户端的限制(更严格)# 格式:hard-limit soft-limit soft-secondsclient-output-buffer-limit replica 256mb 64mb60# 订阅客户端的限制client-output-buffer-limit pubsub 32mb 8mb60

配置含义:replica 256mb 64mb 60表示——硬限制256MB(超过立刻断开),软限制64MB(超过并持续60秒才断开)。这个"软硬兼施"的设计避免了偶发的突发流量导致断开,又能防止长期堆积。

客户端的"生老病死"

出生:createClient()

当一个TCP连接到达Redis服务器时,整个流程是这样的:

acceptTcpHandler (网络事件回调) │ ▼ anetTcpAccept() — accept()系统调用 │ ▼ acceptCommonHandler() │ ├── 检查 maxclients 限制 │ 超限 → 发送错误并关闭连接 │ ▼ createClient(conn) │ ├── calloc client结构体 ├── 设置默认属性(db=0, flags=0, authenticated=0) ├── 设置fd为非阻塞 ├── 关闭Nagle算法(TCP_NODELAY) ├── 设置KeepAlive ├── 创建读文件事件(绑定readQueryFromClient回调) ├── 更新connected_clients计数器 └── 添加到server.clients链表

部分核心代码:

client*createClient(connection*conn){client*c=zmalloc(sizeof(client));// 设置文件描述符if(conn){// 非阻塞 + TCP_NODELAYconnNonBlock(conn);connEnableTcpNoDelay(conn);// KeepAliveif(server.tcpkeepalive)connKeepAlive(conn,server.tcpkeepalive);// 注册读事件connSetReadHandler(conn,readQueryFromClient);}// 默认选择0号数据库selectDb(c,0);uint64_tclient_id=++server.next_client_id;c->id=client_id;// sds初始化(每次分配16KB)c->querybuf=sdsempty();// 链接到全局客户端链表listAddNodeTail(server.clients,c);returnc;}

一生:状态流转

客户端的主要状态变迁:

┌─────────┐ │ 新连接 │ └────┬────┘ │ createClient ▼ ┌─────────┐ ┌───→│ 普通状态 │ ← 执行命令、接收回复 │ └────┬────┘ │ │ MULTI BLPOP SUBSCRIBE │ ▼ ▼ ▼ │ ┌────────┐ ┌──────────┐ ┌────────┐ │ │ 事务中 │ │ 阻塞等待 │ │ 订阅中 │ │ └───┬────┘ └────┬─────┘ └───┬────┘ │ │ EXEC/DISCARD│ 超时/数据就绪 │ UNSUBSCRIBE │ └─────────────┴─────────────┘ │ │ └──────────────────────┘ │ ▼ ┌─────────┐ │ 关闭连接 │ └─────────┘

死亡:freeClient()的触发条件

Redis客户端有"九种死法"(不开玩笑):

序号死因触发条件相关参数
1客户端主动关闭客户端发送QUIT命令或TCP断开
2空闲超时lastinteraction超过timeout秒timeout(默认0=不超时)
3输入缓冲区超限querybuf超过硬限制client-query-buffer-limit(默认1GB)
4输出缓冲区硬限制reply_bytes超过hard-limitclient-output-buffer-limit
5输出缓冲区软限制超过soft-limit且持续soft-seconds同上
6maxclients超限连接数超过maxclientsmaxclients(默认10000)
7CLIENT KILL管理员主动KILL
8CLIENT PAUSE期间的新命令暂停期间非允许的命令
9服务器关闭SHUTDOWN或进程退出
# 最常用的客户端超时配置# redis.conftimeout300# 5分钟不活跃就断开# 设置为0表示永不超时(默认)# 查看和在线修改redis-cli CONFIG GETtimeoutredis-cli CONFIG SETtimeout600

CLIENT命令家族——管理连接的神器

CLIENT LIST:一窥连接全景

CLIENT LIST是运维人员用得最多的命令之一。它的输出一行为一个客户端:

id=3 addr=127.0.0.1:52341 laddr=127.0.0.1:6379 fd=8 name=myapp age=845 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=38808 events=r cmd=ping user=default redir=-1 resp=2

字段含义对照表:

字段含义关注场景
id客户端唯一ID(递增分配)精确定位某个连接
addr客户端IP:端口排查恶意连接来源
laddr服务器本地监听地址多网卡时区分入口
fd文件描述符编号-1表示伪客户端
name客户端名称CLIENT SETNAME设置
age连接已存活秒数发现长连接或频繁重连
idle空闲秒数发现僵尸连接
flags客户端标志N(普通)/M(master)/S(slave)/O(MONITOR)/b(blocked)等
db当前操作的数据库编号确认连接使用哪个DB
sub/psub订阅的频道/模式数量排查pubsub连接
multi事务队列中命令数-1表示不在事务中
qbuf输入缓冲区未消费字节数正常应该接近0
qbuf-free输入缓冲区剩余空间sds预分配了空间
obl/oll/omem输出缓冲区状态omem过大需警惕
cmd当前/最后执行的命令定位慢查询
respRESP协议版本2或3

CLIENT KILL:精准剪断连接

# 按地址杀CLIENT KILL addr192.168.1.100:52341# 按ID杀CLIENT KILLid3# 按类型批量杀CLIENT KILLtypenormal# 只杀普通客户端CLIENT KILLtypepubsub# 只杀pubsub客户端CLIENT KILLtypemaster# 杀主库连接(慎用!)CLIENT KILLtypeslave# 杀从库连接# 组合条件使用(Redis 2.8.12+)CLIENT KILL addr10.0.0.0/24typenormal

CLIENT PAUSE:服务器"暂停营业"

当需要升级Redis版本或切换主从时,让服务器暂停处理客户端命令:

# 暂停所有客户端命令,10000毫秒(10秒)CLIENT PAUSE10000# 仅暂停写命令(Redis 6.2+)CLIENT PAUSE10000WRITE# 查看是否处于暂停状态redis-cli INFO clients|greppaused# paused_clients:1

暂停期间,主从复制、心跳等内部通信不受影响。CLIENT UNPAUSE可以提前解除暂停。

CLIENT NO-EVICT:VIP客户免驱

如果你有"尊贵"的客户端不能被淘汰:

# 将这个连接标记为NO-EVICTCLIENT NO-EVICT ON# 即使超过maxmemory,这个客户端的连接也不会被淘汰

注意:NO-EVICT是Redis 7.0才引入的特性。

伪客户端——不需要socket的特殊客户端

Redis中有两类特殊的客户端,它们没有网络连接,fd = -1

伪客户端(Fake Client)家族 ┌───────────────────────┐ │ server.aof_client │ ← 载入AOF文件时使用 │ (AOF载入伪客户端) │ ├───────────────────────┤ │ server.lua_client │ ← 执行Lua脚本时使用 │ (Lua脚本伪客户端) │ ├───────────────────────┤ │ server.master/client │ ← 主从复制中的主/从端 │ (复制伪客户端变体) │ └───────────────────────┘

AOF伪客户端:载入AOF文件恢复数据时,Redis创建一个伪客户端,把AOF文件中的每条命令"塞"给这个客户端执行。这样就避免了为恢复数据而新建网络连接。

Lua伪客户端:执行EVAL命令时,Lua脚本中的redis.call()实际上是通过一个伪客户端来执行命令的。这种设计让脚本中的命令可以和普通命令走同一套执行路径。

踩坑提示:Lua脚本执行期间,Lua伪客户端会持有server.lua_client,这个伪客户端会绕过很多安全检查(比如不会触发CLIENT PAUSE的暂停逻辑)。这就是为什么长时间运行的Lua脚本会阻塞整个服务器——它在执行的是"伪客户端"的命令,但阻塞的是整个事件循环。

连接数监控——别让服务器撑死

INFO clients输出

redis-cli INFO clients# 输出示例:# connected_clients:152 ← 当前连接数# cluster_connections:0 ← 集群内部连接数# maxclients:10000 ← 最大连接数限制# client_recent_max_input_buffer:1024 ← 近期最大输入缓冲区(字节)# client_recent_max_output_buffer:51200 ← 近期最大输出缓冲区(字节)# blocked_clients:3 ← 被阻塞的客户端数# tracking_clients:0 ← 客户端跟踪数(Redis 6.0+)# clients_in_timeout_table:0 ← 等待超时的客户端数# total_connections_received:58432 ← 历史总连接数(含已关闭)

连接数监控脚本

#!/bin/bash# 监控Redis连接数,超过80%告警HOST="127.0.0.1"PORT="6379"connected=$(redis-cli-h$HOST-p$PORT INFO clients|grep"connected_clients"|cut-d:-f2)maxclients=$(redis-cli-h$HOST-p$PORT INFO clients|grep"maxclients"|cut-d:-f2)ratio=$(echo"scale=2;$connected/$maxclients* 100"|bc)echo"当前连接数:$connected/$maxclients($ratio%)"if(($(echo "$ratio>80"|bc-l)));thenecho"WARNING: 连接数超过80%!"redis-cli-h$HOST-p$PORTCLIENT LIST|\awk-F'[ =]''{print $3}'|sort|uniq-c|sort-rn|head-10fi

不同场景的连接数经验值

场景典型连接数调优建议
小规模应用< 100保持默认maxclients=10000即可
中型API服务100-500关注idle时间,设置timeout
大型微服务500-2000考虑使用连接池,设置合理maxclients
长连接Pubsub1000-10000监控omem,配置client-output-buffer-limit
超大集群10000+开启cluster模式,分散到多个节点

踩坑提示:一个常见误区是"连接数多=性能好"。实际上,大量空闲连接只会浪费文件描述符和内存。Redis的单线程模型决定了命令执行是串行的,500个连接同时发命令和5个连接发命令,实际的QPS差异并不大。真正需要关注的是"活跃连接数",而不是总连接数。

总结与最佳实践

让我们用一张图回顾今天的内容:

Redis 客户端全景 ┌─────────────────────────────────────────────────────────┐ │ │ │ 创建 运行中 关闭 │ │ ┌──────┐ ┌─────────────────────┐ ┌──────┐ │ │ │accept│───→│ • 输入缓冲(querybuf) │───→│超时 │ │ │ │ │ │ • 输出缓冲(buf/reply) │ │OOM │ │ │ │create│ │ • 命令执行(argc/argv) │ │QUIT │ │ │ │Client│ │ • 状态管理(flags) │ │KILL │ │ │ └──────┘ └─────────────────────┘ └──────┘ │ │ │ │ 管理工具:CLIENT LIST / KILL / PAUSE / SETNAME / INFO │ │ │ │ 特殊客户端:AOF/Lua伪客户端(fd=-1) │ │ │ └─────────────────────────────────────────────────────────┘

几个关键要点:

  1. client结构体是Redis连接管理的核心,囊括了网络、输入、输出、状态四个维度。
  2. 输入缓冲区querybuf使用sds动态扩容,正常情况下qbuf应为0(命令已被消费)。
  3. 输出缓冲区采用"固定16KB + 链表"双通道,小回复走buf,大回复走reply链表。omem持续增长是大问题。
  4. CLIENT LIST是排查连接问题的第一板斧,学会解读每个字段。
  5. 伪客户端(fd=-1)用于AOF载入和Lua脚本执行,它们绕过网络但复用命令执行路径。
  6. 合理配置timeoutmaxclientsclient-output-buffer-limit是运维的基本功。

下一篇文章我们将见证Redis服务器从redis-server命令启动到"ready to accept connections"的完整旅程——六个阶段,每个阶段都有很多你没注意到的细节。


上一篇【第35篇】Redis为什么这么快——单线程也能跑出10万QPS的秘密
下一篇【第37篇】Redis服务器启动全流程——从redis-server到ready to accept


http://www.rkmt.cn/news/1450329.html

相关文章:

  • 2026年想找有社区交流功能的手机阅读器?这些选择别错过!
  • Windows Defender完全移除终极指南:专业级系统性能优化与安全组件深度清理
  • 给物理模拟新手的Geant4保姆级入门:从第一个例子到看懂运行日志
  • 宠物帮扶信息平台宠物领养寻宠登记Java整套源码部署
  • 湘潭母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • 7个技巧:让你的普通鼠标在Mac上超越苹果触控板
  • SpringBoot开发宠物帮扶系统领养认领信息管理源码详解
  • 通辽CMA甲醛检测治理公司深度测评:绿居净环保稳居榜首 - 五金回收
  • 一站式社区养老平台Java康养疗养业务管理系统源码
  • 如何构建企业级智能数据采集系统:Crawl4AI的5个维度完整实现指南
  • 如何让B站视频观看体验更流畅?小电视空降助手帮你跳过所有广告片段
  • 武汉母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • Scroll Reverser完整指南:彻底解决macOS鼠标与触控板滚动方向冲突
  • 武汉母婴除甲醛CMA甲醛检测治理公司深度测评:清醛卫士稳居榜首 - 五金回收
  • 太原母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • Java后端开发康养平台疗养预约、日常管理模块源码解析
  • 复刻Korg MS20 MKII电压控制多模谐振滤波器:从OTA原理到PCB实战
  • 3个核心技巧:用Gofile下载器告别繁琐手动下载
  • 告别命令行恐惧:在Ubuntu 22.04上用CuteCom图形化搞定串口调试
  • 西安CMA甲醛检测治理公司深度测评:绿居净环保稳居榜首 - 五金回收
  • 铜川母婴除甲醛CMA甲醛检测治理公司深度测评:清醛卫士稳居榜首 - 五金回收
  • Mod Engine 2技术解析:运行时注入框架如何重塑魂系列游戏模组开发
  • 2026年一体化净水设备实测评测:污水处理设备/直饮水设备/综合水处理器/超纯水设备/软化水设备/一体化净水设备/选择指南 - 优质品牌商家
  • 设计模式入门:3. 装饰器模式详解 C++实现
  • 旅游MCN紧急预警:Sora 2已上线动态光影引擎,你的旧脚本将在72小时内批量过时,速查兼容性自查表
  • 【Claude机会点识别避坑清单】:12个被90%团队忽略的伪机会信号,含真实客户ROI对比数据
  • 印度电子维修市场行话解析:从COMB IC到PF,连接理论与实践的桥梁
  • 苏州母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • 锦州CMA甲醛检测治理公司深度测评:绿居净环保稳居榜首 - 五金回收
  • 营销人必抢的AI协同工作流(2024 Q2最新兼容矩阵已发布)