我们得说清楚,Redis源码这个系列走到第四十一篇,前面已经把内存、对象系统、字典、跳跃表、事件驱动这些硬骨头啃得差不多了,这期如果再不聊点“服务器怎么和客户端打交道”的东西,读者怕是要扔砖头了。所以这一篇我想把目光放在 Redis 的 networking 层,也就是从客户端连上来的那一刻开始,到命令读完、执行完、结果写回去、连接被关掉为止,这一整条路上代码到底在做什么。
为什么值得看这块?因为 Redis 虽然是以性能闻名的内存数据库,但它的性能瓶颈往往不在哈希算法、不在内存分配,而在网络处理这件事上。如果你遇到过线上突发的大流量下连接数暴涨、延迟飙升、或者某个客户端把输入缓冲撑爆导致内存飙升,那基本都能在 networking 层的代码里找到答案。把 netwoking.c 读明白,很多线上问题就能从“现象”变成“可解释的原因”,排查起来就不是靠猜了。
适合谁看?适合已经对 Redis 基本命令很熟、但想往里层走的开发者,也适合正在做性能调优、排查线上问题,或者准备面试想讲清楚 Redis 命令处理全链路的同学。就算你之前没系统读过 C,这个系列一直用的是“一行关键代码配一段白话解释”的风格,你也能跟上。这期我就不铺垫太多了,直接开看。
1. 从连接到命令:Redis 网络层到底在做哪些事
1.1 先给 networking 层画一张功能地图
Redis 的服务器核心其实是一个单线程事件循环:把文件事件(读事件和写事件)和定时事件放在一个循环里处理。每次客户端有数据发过来、或者有数据可以写回去,都会触发对应的事件回调。网络层代码就是围绕“事件到来之后怎么处理”这一件事展开的。
具体拆开,networking.c 和关联文件大概承担了这些事:
- 创建和初始化客户端对象:accept 一个新的 TCP 连接时,要给这个连接分配一个 client 结构体,设置缓冲区、标志位、默认数据库等。
- 读取并解析请求:把 fd 上读到的字节流攒进输入缓冲区,然后按照 RESP 协议解析出命令参数,逐条执行。
- 执行命令并把结果写回:调用命令对应的处理函数,把返回值编码成 RESP 格式,塞进输出缓冲区,注册写事件。
- 管理输出缓冲和内存:输出缓冲太大、客户端太慢、或者超过 client-output-buffer-limit 设置的限制时,决定是否断开连接。
- 定时与统计:更新连接的空闲时间、计算客户端的内存占用、拉取各种 stats 数据。
看起来功能很多,但代码的核心脉络其实非常集中。你只要先记住一个顺序:连接进来 -> 创建 client -> 读数据 -> 解析命令 -> 执行命令 -> 写回复 -> 关闭连接,整个生命周期就是这条线。后面所有细节都是在这条线上添加约束和判断。
1.2 为什么单线程还能扛住高并发
很多人一开始不理解:Redis 明明是单线程,为什么能处理十万甚至几十万的 QPS?其实关键在于它压根就不是“一台服务器一次只服务一个客户端”的模型,而是“一个进程在极短时间内把事件轮询一圈”的模型。
你可以把 Redis 里的主循环想象成一个前台小哥在同时接待很多顾客。小哥不会死守一个顾客把需求全部做完,而是每个人来了先听一句需求、记下来、告诉对方“稍等”,然后立刻去招呼下一个。等某个顾客的回答准备好了或者有新消息来了,再回头继续处理。这个切换动作极快,对用户来说感觉就像是被专人在服务一样。这就是典型的事件驱动模式。
所以网络层的代码设计原则就是:绝不阻塞。任何读取、写入、解析都不能因为一个客户端卡住而毁掉整个事件循环。真正的实现都在关心“这个客户端现在能不能读、能不能写”,不能读就等事件,不能写就把数据缓存在输出缓冲里,等可写事件到来时再补发。
1.3 三个核心结构体:client 的全貌
在开始看具体函数之前,建议先把 client 结构体这个“容器”理解清楚。几乎所有网络层操作都围绕它进行。
client 结构体中有几个非常关键的字段:
- fd:连接对应的文件描述符。fd = -1 表示这是一个假客户端,不是网络来的。
- querybuf:输入缓冲区,所有从网络读到的原始字节流都存在这里。
- argv 和 argc:解析好的一条命令参数数组,命令名 + 各个参数。
- buf 和 reply:输出缓冲区,buf 是一个固定长度的数组(默认 16KB),reply 是一个链表,负责塞更长的响应数据。
- flags:各种状态标记,比如 CLIENT_CLOSE_ASAP(异步关闭)、CLIENT_PENDING_WRITE(有数据待写回)等。
- c->cmd:指向 redisCommand 结构体,代表客户端当前要执行的命令元信息。
如果你读源码的时候感觉函数跳来跳去头很大,记住所有跳转最终都是为了给这些字段赋值、读取、清理。比如 readQueryFromClient 在做的事就是把 fd 里的数据往 querybuf 里面填;processInputBuffer 在做的事就是把 querybuf 里的数据往 argv/argc 里解析;processCommandAndResetClient 做的事是拿着 argv 去命令表里找 c->cmd 然后执行。
知道了这三个职责,你看代码的时候就不会迷路。
2. 一条命令从读到执行,代码走的完整路径
2.1 连接建立:从 accept 到 createClient
当客户端 TCP 连到 Redis 端口时,事件循环的读事件会触发 acceptTcpHandler,经过一层层包装,最终会走到 networking.c 里的 acceptCommonHandler,然后再调用 createClient。
这里要注意,createClient 不仅仅是分配内存那么简单,它做了几件很重要的事:
- 把 fd 设置成非阻塞模式,注册读事件。回调函数就是 readQueryFromClient。
- 初始化 querybuf 为一个空的 sds 字符串。
- 设置 client 默认的数据库编号为 0。
- 设置一些标志位和默认限制。
void *createClient(connection *conn) { client *c = zmalloc(sizeof(client)); if (conn) { connNonBlock(conn); connEnableTcpNoDelay(conn); if (connSetReadHandler(conn, readQueryFromClient) == C_ERR) { ... } c->fd = connGetfd(conn); // ... } c->buf = zmalloc(PROTO_REPLY_CHUNK_BYTES); // ... }这段代码里有几个细节值得划重点。connNonBlock 把 socket 设置成非阻塞,这是整个网络层能高效运转的基石。connEnableTcpNoDelay 会把 TCP_NODELAY 打开,意味着小包不等待合并,直接发出去。如果你是做低延迟服务的,注意这个细节:Netty、gRPC 在处理 Redis 协议时也会保留这个设置,因为它直接决定了一次 RPC 的延迟。
还有个容易忽略的点:createClient 里给输出缓冲 buf 分配了 PROTO_REPLY_CHUNK_BYTES 的固定大小。这不是随意选的,它对内存分配压力影响很大。如果默认响应缓冲太大,高并发下每个连接都要浪费一块内存;太小的话,长响应内容就要频繁走链表,性能受影响。Redis 选了 16KB,算是一个兼顾大多数场景的经验值。
2.2 读数据:readQueryFromClient 与输入缓冲区增长
TCP 本身是提供字节流的,不保证你在一次 read 里能拿到完整的一条命令,也不保证一次 read 里只有一条命令。所以 readQueryFromClient 必须能把零散的数据追加到 querybuf 里,留到后面慢慢解析。
核心流程大概是这样:
void readQueryFromClient(connection *conn) { client *c = connGetPrivateData(conn); int nread = connRead(c->conn, c->querybuf + sdslen(c->querybuf), readlen); // ... if (nread) { sdsIncrLen(c->querybuf, nread); processInputBuffer(c); } }看到 c->querybuf + sdslen(c->querybuf) 这个写法,是直接把新读到的数据追加到 sds 字符串末尾的位置。sdsIncrLen 把长度增长 nread 字节。然后立刻调 processInputBuffer 尝试解析。
这里的 readlen 有些讲究。如果输入缓冲已经很大,说明客户端可能正在传大体积数据,此时 readlen 会变大,避免每次 read 太小导致系统调用次数过多。这也算一种偷跑优化:根据现实场景动态调整单次读取大小,既省系统调用,又不至于在流量小时浪费内存。
底层的 connRead 包装了 read 系统调用,各种连接类型(TCP、TLS、Unix Domain Socket)会有不同实现,但对外接口统一。这也是 Redis 新版代码把 connection 抽象出来的价值所在:上层逻辑完全不用关心底层是加密还是明文,连接能力被统一表达。
输入缓冲区并非无限增长。虽然 Redis 主版本对 querybuf 没有限制到非常严格,但客户端传输的数据量极大时,内存依然可能被拖垮。实操中你可以在 INFO clients 里看到 client_recent_max_input_buffer 这个指标,它记录的是所有客户端中近端输入缓冲的最大值。如果这个值持续涨到几十 MB 甚至几百 MB,大概率是有客户端在批量灌数据,或者上游应用有大体积写入。
2.3 解析命令:processInputBuffer 和 multibulk 解析循环
querybuf 拿到原始数据之后,下一步是把字节流改成“有结构”的命令。Redis 在 processInputBuffer 里进入一个 while 循环,只要还有数据,就不断尝试解析。
这个循环有两个重点:解析协议、处理命令间的分隔。理论上,一个 TCP 包可以包含多条命令,比如客户端一次 write 发送两条 SET 命令。Redis 的循环能将它们从一个 querybuf 里拆出来,并逐条执行。
循环里频繁出现的一个函数是 processMultibulkBuffer。它的工作,就是把类似*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n的字节流解析成 argv 数组。
解析过程是有状态的,client 结构体里会保存当前解析到哪一步:
- c->multibulklen:还剩多少参数没解析完成
- c->bulklen:当前这个参数的内容长度是多少
- c->qb_pos:querybuf 里的当前扫描位置
可以把它理解成一个移动指针的“读卡机”:先看到*3知道后面有 3 个参数,然后一个个读$3、$5、$7知道每个参数的长度,接着把对应长度的字节抠出来放进 argv。等所有参数都读完了,一个命令的解析就结束了。
这里有一个细节值得注意:因为 TCP 分包,一次 readQueryFromClient 后,很可能 querybuf 里只包含一条命令的前半段,比如*3\r\n$3\r\nSE就没了。这时 processMultibulkBuffer 会把 c->multibulklen 设置为一个还差一部分的状态,但不会报错,而是退出循环,等下一个读事件再来时继续处理。所以解析状态机的正确性,直接关系到“数据被拆成任意碎片都能还原出完整命令”的能力,这也是网络编程最基础也最关键的能力之一。
2.4 执行与写回:processCommand 和输出缓冲策略
命令解析完成,argv 准备好了,接下来 processInputBuffer 会调用 processCommandAndResetClient,内部走到 processCommand。这里是所有命令的分发枢纽:拿 argv[0] 去命令表里查,找到对应的处理函数,检查认证、ACL、参数长度、过期 key 等,然后执行。
执行完之后,返回值要写回给客户端。注意 Redis 的执行是单线程,但写回的时候绝不能直接阻塞式 write。设想一下:如果某个客户端读得非常非常慢,而 Redis 往它的 socket 写数据时又必须等它读走才能继续,整个 Redis 都会卡在这个客户端身上,其他客户端全遭殃。
所以 Redis 采用了“先尝试写,如果写不完就缓存”的策略。在 afterCommand 里会有一次检查:如果 c->buf 里有数据或者 c->reply 链表里有数据,并且该客户端还没有注册写事件,就通过 clientInstallWriteHandler 注册 write 事件。后续事件循环发现这个 fd 可写了,再调 sendReplyToClient 把缓存的响应发出去。
int writeToClient(client *c, int handler_installed) { ssize_t nwritten = connWrite(c->conn, c->buf + c->sentlen, c->bufpos - c->sentlen); // ... }写完后如果全部发完,就移除写事件;如果还没发完,保留写事件等待下次触发。这样就把“慢客户端”隔离在了自己身上,不影响其他请求处理。你说这是不是有点像异步排队的思路?小步快跑、能发多少就发多少、发不完就等下一次机会,直到缓冲清空。
3. 输出缓冲的微观细节:异步断开不再是简单粗暴
3.1 三类客户端的不同限制策略
之前讲输出缓冲时提到过 client-output-buffer-limit 配置,这个配置实际上区分了三类客户端:普通客户端、从库客户端、Pub/Sub 客户端。它们各自的默认限制如下:
- 普通客户端:默认 0(不限制),但可以配置 hard limit 和 soft limit。
- 从库客户端:默认 hard 256MB,soft 64MB/60s。因为全量同步会产生巨大的 RDB 传输流量。
- Pub/Sub 客户端:默认 hard 32MB,soft 8MB/60s。因为订阅者消费速度跟不上生产者时,积压消息全堆在输出缓冲里。
为什么 Pub/Sub 的默认限制比正常客户端严格?因为订阅模式天然支持“慢消费者拖垮生产者”的场景。生产者发布消息是不知道自己订阅者消费能力的,如果不加限制,内存会被积压消息撑爆。Redis 选择在输出缓冲层做阻断:超过限制就把这个连接直接断开。每次断连,订阅端重新订阅即可,而服务端内存得到保护。
3.2 hard limit 与 soft limit 的判定逻辑
理解 hard limit 和 soft limit 的区别,是配置 Redis 输出缓冲的关键。
- hard limit:只要输出缓冲大小超过这个值,立即异步断开客户端。
- soft limit:输出缓冲超过这个值,先不急着断,但持续超过指定秒数(比如 60 秒)就触发断开。
为什么要设计两档?因为正常业务有时会有瞬时大响应,比如批量取大量数据,输出缓冲立刻飙升。如果只有一个硬上限,这类正常大请求也容易被误杀。软上限给了缓冲观察期,只要不是持续太久,就不做处罚。在源码里,这两者的判定逻辑在 checkClientOutputBufferLimits 里,每次事件循环检查时会比较 c->reply_bytes 和 limits。
值得一提的是 closeClientOnOutputBufferLimitReached 并不会在主线程流程里立刻杀掉客户端,而是给 client 打上 CLIENT_CLOSE_ASAP 标记,最终在 beforeSleep 或 afterSleep 阶段统一清理。这样做的原因是杀客户端的过程本身可能涉及锁、嵌套事件等复杂因素,异步统一处理能极大降低死锁和重入问题。
3.3 大响应如何绕过固定缓冲:reply 链表
前文说输出缓冲有两段:固定 buf 和可变链表 reply。设计实现上有取舍。固定 buf(16KB)的优点是分配快、免链表遍历开销,适合小响应;但遇到大响应,如果也硬塞进固定 buf,就会造成大量 memcpy。
所以协议写回复时,写函数会先判断响应大小是否超过固定 buf 容量。如果放得下,直接写进 buf;如果放不下,则使用另一个对象来表示大回复块,附加到 c->reply 链表中。这样“小响应快、大响应稳”,既减少了系统调用次数又避免了内存拷贝。
在 sendReplyToClient 发送时,代码先发 buf,再遍历 reply 链表。当链表节点都发完后,把节点释放回内存池。对长列表、大哈希这类 Bulk 字符串特别多的响应,这种链式缓冲实际上就是 Redis 高性能输出的底气。
4. 常见问题排查:从源码角度定位线上故障
4.1 输出缓冲限制导致的“神秘断连”
我最常见到的线上问题,就是业务反馈“Redis 连接时不时断掉,而且断的时候正好在拉大量数据”。查来查去,最后大概率就是输出缓冲超限被主动断开。
建议排查步骤:
- 先在
redis-cli config get client-output-buffer-limit查看配置,确认。普通客户端如果是 unlimited 就继续排查别的方向。 - 用
INFO clients看 output list length 和 output buffer 相关指标。 - 如果瞬间大响应频繁触发 soft limit,考虑把 soft limit 调大,或者把对应命令改成分批取数。
- 日志里出现 "Client closed connection" 或者 "Output buffer limit reached" 之类的信息时,对照时间点拉一下客户端行为。
有个经验是:大 key 操作很容易触发这个问题。比如用一次SMEMBERS拉一个有 100 万个元素的集合,响应体积轻松超过 100MB。这种情况单纯调大缓冲指标治标不治本,最佳实践是把大集合拆成多次SSCAN分批处理。源码层面的启发是:任意“一条命令产生超大响应”的设计,本质上都在考验输出缓冲的兜底能力。
4.2 输入缓冲过大的诊断思路
相比输出缓冲,输入缓冲的问题没那么常见,但一旦出现,危害同样很大。因为输入缓冲存放的是客户端发来的原始命令数据,没有上限的话,恶意或者失控的客户端可以无限往里面灌数据,把服务器内存打满。
源码里 readQueryFromClient 会给 querybuf 设置一个硬上限,如果超过,会打印 warning 并断开连接。这个上限其实和配置参数proto-max-bulk-len相关,默认 512MB,这个值通常不需要调整。
如果线上遇到内存占用过高,怀疑输入缓冲,可以用INFO clients查看client_recent_max_input_buffer,再通过CLIENT LIST查看每个客户端的具体输入缓冲占用。定位到具体 fd 之后,顺着客户端 IP 去查对应应用的状态,看看最近是不是有没有消费完的数据堆积在 Redis 侧。
4.3 命令解析异常:不是所有二进制数据都能当命令发
Redis 的 RESP 协议是文本换行协议。如果你用各种语言的 Redis 客户端库,不需要关心协议解析;但如果有人用裸 socket 拼协议,常见问题就是忘记在参数长度和参数内容间加\r\n,或者把$3\r\n写成了$3\n。
这类问题反映到源码层,就是 processMultibulkBuffer 解析失败。Redis 会返回协议错误信息,但如果客户端不复用连接,一般影响不大。真正要警惕的是写程序时硬编码了协议内容、却疏忽了长度计算,导致后续所有命令错位。排查时可以使用redis-cli --pipe测试协议格式,或者写个小脚本逐步发包验证。
4.4 CLIENT LIST 字段详情:一张表看清每个客户端状态
要在线上快速地靠直觉定位“哪个客户端有问题”,CLIENT LIST 是最好的入口。它输出的是每个 client 的现场快照,源码中对应 client 结构体的几个字段。
常用字段和含义我整理成了表:
| 字段 | 含义 | 排障用途 |
|---|---|---|
| id | 客户端唯一 ID | 配合日志追踪连接 |
| addr | 客户端 IP 和端口 | 定位来源机器 |
| fd | 文件描述符 | 关联系统层连接状态 |
| qbuf | 输入缓冲区字节数 | 判断是否有大体积命令堆积 |
| qbuf-free | 输入缓冲剩余字节 | 预留空间估计 |
| obl / oll / omem | 固定输出缓冲、链表长度、总输出字节 | 判断输出是否已爆掉 |
| cmd | 当前正在执行的命令 | 定位慢命令 |
| psendbuf | Pub/Sub 订阅积压 | 订阅场景下才出现 |
| age / idle | 连接已存在时间 / 空闲时间 | 判断是否该做空闲回收 |
比如你想找“输出缓冲最大的客户端”,直接看 CLIENT LIST 里 omem 最大的那一行,基本就是嫌疑对象。这是线上排查效率最高的手法之一,因为这比抓网络包、看监控曲线要直观得多。
5. 几个容易被忽略的设计细节与经验思考
5.1 为什么关闭连接要分“异步”和“同步”两种
不少初学者看源码时会被 freeClient 的各种调用位置搞晕:为什么有的直接 freeClient(c),有的却只打 CLIENT_CLOSE_ASAP 标志?其实逻辑很简单。
同步关闭用于“确定现在这个 client 已经彻底没人用了”的场景,比如命令执行时通过 CLIENT KILL 主动杀连接。异步关闭则用于“你可能还在某个事件回调栈里,直接 free 会导致 use-after-free”的场景。比如输出缓冲超限时,如果立刻 free client,而外层正在遍历 clients 链表,就可能导致内存安全问题。所以先标记,等主循环回到安全位置再统一清理。
这种思路其实都来自事件驱动编程的一个普适教训:在回调里尽量不要销毁回调上下文依赖的对象。你如果写过 Node.js 的异步回调,应该对这种“不能在一个异步函数里随意关闭调用方资源”的坑深有体会。
5.2 一个特别的客户端:fake client
有一类 client 很特殊,它的 fd = -1。这种 client 不承载任何网络连接,只负责处理内部命令,比如加载 AOF 文件时,Redis 会用一个 fake client 来模拟执行命令、更新数据结构。还有 Lua 脚本执行时的 client 也是特化处理。
理解 fake client 的好处在于,它提醒你 client 结构体其实是“一次命令执行上下文”的抽象,而不仅仅是“一个网络连接”。加载 AOF、主从复制时,数据会以“命令”的方式流经这个上下文。所以当你看到源码里频繁创建临时 client 时,不要觉得浪费,那是为了复用命令执行的整套逻辑。
5.3 单线程模型下的网络行为推断
既然 Redis 是单线程处理命令,许多网络层行为其实是可以用“顺序化”的思维去推断的。比如,命令执行和网络读取在同一线程内串行发生,死循环的 Lua 脚本会卡住所有客户端,这时你会看到所有连接都不响应,但它们并没有断开,只是“等待中”。这一切都和 Redis 的“事件处理完一个再处理下一个”模型直接相关。
这也意味着,调试网络层问题时,不要简单认为“没反应就是挂掉”了。理解模型才能用好 tools。比如当 Lua 脚本阻塞时,redis-cli也会卡住。而用redis-cli -p 6379 debug sleep 5这类会短暂阻塞的行为也能观察到 CLIENT 状态的 idle 变化,这些都需要你结合模型去理解,而源码就是模型的唯一权威。
5.4 针对慢客户端,官方怎么做的:client pause 与前向兼容
Redis 不止有断开机制,还提供了主动暂停客户端的命令 CLIENT PAUSE。这个命令会让所有客户端暂停指定毫秒数,主要用于持久化切换等情况下的平滑处理。它的实现原理是给所有 client 标记上一个暂停截止时间,命令处理循环里判断时间是否到期。这个功能和输出缓冲策略方向上是相反的:前者是“全局暂停一阵”,后者是“单独断开缓冲大户”。
做架构设计时,这类对比值得学习:同样是面对“客户端过多、流量过大”的维护问题,选择工具要看场景。要么你限制缓冲,要么你冻结服务,要么你直接断开。Redis 把三种手段都开放出来,就是要告诉你“没有万能银弹”,配置策略取决于业务容错能力。
6. 实际调优杂记:配置、实操与个人体会
6.1 调优 client-output-buffer-limit 时别拍脑袋
网上很多人直接说“把 client-output-buffer-limit 改成 1GB”,这其实很危险。输出缓冲大意味着内存占用大,尤其是 Pub/Sub 场景,真改成 1GB,几十个慢订阅者就可以把 Redis 内存撑到不可用。我曾经接过一个事故,就是某团队为了处理偶尔的大结果集,把 pubsub limit 调成了 unlimited,后来生产环境出现消息积压,单实例内存直接上涨到几十 GB,最后不得不重启清理。教训就是:限制的存在不是为了麻烦你,而是帮你守住内存底线。
正确的调优路径是:先确认业务峰值,再留 30% 的缓冲余量。比如业务最多会一次返回 20MB 数据,那普通客户端 soft limit 可以配 32mb 64 60,hard limit 配 64mb。从库同步大 RDB 时,可以适度放大 slave 的 hard limit,但一定要配合理的内存上限水位告警。
6.2 prot 协议层面的小坑:Bulk 长度与 memory 计费
还有一个值得注意的点:RESP 协议里 bulk 字符串带长度前缀,比如 $5\r\nhello\r\n。这种长度前缀会直接影响 key 的存储和内存计算。虽然日常都会有客户端库帮你拼,但如果你在写跨语言网关、做协议转换,一定不要自己解析长度时出低级错误。
测试协议正确性的方法很简单:用redis-cli --pipe < file导入一批协议命令,如果解析正确就能批量执行成功;如果某个$len和实际长度对不上,Redis 可能返回协议错误并断开连接。这类问题一旦出现,影响面通常很大,因为所有命令都会错乱。所以协议拼包若有任何改动,务必先在测试环境压一遍。
6.3 本地调试时,如何快速看网络层的实时状态
实战中我用得最多的三件套是:
redis-cli client list:随时看客户端和缓冲占用。redis-cli info stats | grep -E 'total_reads|total_writes|total_connections':看读写次数趋势,判断系统调用是不是成为瓶颈。redis-cli monitor:实时看当前执行的命令流,适合定位“哪条命令正在占用服务”。
其中 monitor 有一个大坑:它会显著降低 Redis 的性能,因为要额外把每条命令输出到终端。生产环境慎用,如果必须用,建议配合timeout 5 redis-cli monitor这样的短时用法,用完就关。
6.4 这期源码阅读之后,对你解决实际问题的帮助
如果认真读完了 networking.c 这条线,你再遇到任何 Redis 连接异常,脑子里应该会浮现一张图:哪一个环节出问题了?是 accept 阶段、协议解析阶段、命令执行阶段、还是写回阶段?每个阶段挂掉的表现各有不同,只有建立了“源码层模型”,才能快速定位到“哦,这部分代码在这里做了限制,所以出现了这个现象”。
我个人的体会是,Redis 源码对我最大的价值不是让我能背出某个函数的行数,而是给了我一整套网络编程的“标准答案”:异步与非阻塞怎么配合、缓冲与背压怎么设计、超时与异常怎么清理。这套东西带到任何语言、任何一个消息系统里,都是一脉相通的思路上限。
你要是想继续往深走,下一步可以重点看两个方向:一是 tcp 连接的读写事件在 aeEventLoop 里怎么被调度,二是阻塞命令(如 BRPOP)如何把 client 从一个全局列表摘下来又挂回去。这两个方向各有名的难点,但搞懂了之后,你会对“事件驱动 + 缓冲队列”这门手艺有更完整的体感。
就先聊到这,拿到源码,开一个调试终端,把 CLIENT LIST 和 INFO stats 打开,把断点下在 createClient 和 freeClient,自己动手玩一圈,收获比看十篇博客都大。