ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis源码剖析:从事件循环到命令执行的完整链路

Redis源码剖析:从事件循环到命令执行的完整链路 动手读过Redis源码的人多半是被一些表象问题勾过去的为什么单线程还能扛住十万级QPS为什么某条命令会把整个实例卡住为什么明明连接数不高输出缓冲却撑爆了内存这些问题翻文档翻不出答案但顺着源码走一遍命令的完整生命周期基本都能找到落脚点。这篇文章就把Redis的命令处理机制从头到尾拆开看从网线那头的一条SET命令开始一路跟到事件循环、协议解析、命令表查找、执行器调用再到结果写回把每一段关键路径上的源码和设计意图都过一遍。适合对Redis有一定使用经验、想往深走一层的后端开发者。1. 从一条SET命令的出生看清全链路地图1.1 网络上传过来的不是SET而是一串字节先抛一个很多人忽视的事实当你在命令行敲下SET foo bar回车真正经过TCP连接送出去的并不是人眼看到的六个字符那么简单而是这么一串东西*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n这是Redis的RESP协议在传输层的真实样貌。*3表示后面有三个参数$3表示接下来这个参数的长度是3字节后面跟着参数内容。之所以聊命令处理机制要先说这个是因为整个服务端解析流程的起点就是这串字节。很多人在源码里找命令是怎么识别的找半天找到processCommand函数觉得这就是入口其实这已经是后半程了。真正的前半段发生在更底层的网络层数据从内核的socket缓冲区被读出来放进Redis自己管理的内存缓冲区然后一层层剥出参数。这串字节没被解析成argc和argv之前服务端眼里它只是一堆等待处理的二进制流。1.2 服务端到客户端的完整路径速览我把这条命令从进门到出门经过的主要函数列出来后面每个章节再逐个展开阶段关键函数作用事件等待aeMain/aeProcessEvents通过epoll等待可读事件读入缓冲readQueryFromClient从socket读数据到querybuf协议解析processInputBuffer/processMultibulkBuffer把字节流拆成参数数组命令查找processCommand/lookupCommand根据argv[0]查命令表安全检查processCommand内部的层层校验认证、ACL、OOM、集群重定向等真正执行call→setCommand调用命令的具体实现回复准备addReply/prepareClientToWrite把结果写入输出缓冲区写回客户端handleClientsWithPendingWrites/writeToClient把缓冲数据通过socket发出这条链路里最精巧的部分不在某个单一函数而在各层级之间的解耦。网络层不关心你传的是SET还是GET协议层不关心你要操作哪个key命令表不关心你的参数值是什么执行器也不关心结果怎么返回给客户端。每层只做好一件事然后通过精心设计的结构体把上下文传给下一层。理解了这条链路的整体形状后面看每个环节都会有种果然如此的感觉——因为每个函数的存在都是有明确理由的不是代码堆砌。2. 事件驱动的心脏aeEventLoop如何决定现在该做什么2.1 单线程的优势恰恰是没有锁聊Redis命令处理就绕不开事件循环。Redis不是每来一个连接就开一个线程而是用一个进程里的一个主线程循环处理所有事件。这个事件循环的实现在ae.c里核心结构叫aeEventLoop。先看服务端启动时的调用关系。main函数在完成initServer之后执行了这么一行aeMain(server.el);aeMain本身就是一个死循环只有收到关闭信号或发生致命错误才会退出void aeMain(aeEventLoop *eventLoop) { eventLoop-stop 0; while (!eventLoop-stop) { aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }很多人把aeProcessEvents理解成处理所有事件这个理解没错但会忽略一个关键点它不只是处理已经发生的事件还要主动等待事件。aeProcessEvents内部会调用aeApiPoll而aeApiPoll在各个平台上有不同实现Linux上就是对epoll的封装会阻塞在那里等待内核通知。它返回后说明至少有一个fd上有事件发生了这时才进入事件的处理阶段。为什么单线程反而是优势因为整个命令执行过程中Redis内部所有数据结构——哈希表、链表、跳表、字符串对象——都处在单线程的访问模型下不需要任何锁。多线程引入的锁竞争、上下文切换、缓存失效成本在Redis这种以内存操作为主的场景里往往比并行执行带来的收益更显著。这也是为什么Redis 6.0引入多线程I/O时特意把线程限制在网络读写层面命令执行仍然是主线程串行完成的。2.2 读事件与写事件的分工逻辑aeEventLoop里维护着两种关键事件文件事件file event和时间事件time event。文件事件对应的是网络连接的可读可写状态时间事件则对应周期性任务比如serverCron。文件事件有一个很重要的设计同一时刻一个fd上的读事件和写事件是分开处理的。aeProcessEvents会先通过aeApiPoll拿到一批就绪的文件事件然后逐个调用注册的回调函数。对于监听socket上的可读事件回调是acceptTcpHandler用来接受新连接对于客户端socket上的可读事件回调是readQueryFromClient也就是下一章的主角。这里有个值得注意的细节Redis通常只关心读事件不主动注册写事件。为什么因为写事件和读事件不一样——读事件是有数据了才触发写事件是缓冲区空闲就触发。如果一个客户端连上来但不怎么发数据你却一直注册着它的写事件那么每次事件循环都会因为这个fd可写而被唤醒白白消耗CPU。Redis的做法是先尝试直接写写不完返回EAGAIN才注册写事件等socket可写了再继续写。这个被动注册写事件的思路在后面的输出缓冲章节还会再见到。2.3 时间事件里的见缝插针serverCron是Redis的时间事件默认每100ms执行一次。它管的事情特别杂过期key的抽样删除、重新计算内存峰值、更新统计信息、检查持久化持久化状态、处理客户端超时等等。这些任务不需要精确到毫秒级100ms的粒度非常合适。时间事件和文件事件在aeProcessEvents里的关系是先找出最近要执行的时间事件算出离它还有多少毫秒然后把这个时间作为aeApiPoll的超时时间。如果这段时间内有网络事件来了就优先处理网络事件处理好之后再看时间事件到没到点如果没有网络事件等到超时就把时间事件先执行了。这套调度逻辑非常优雅它保证了网络请求再密集时间事件也总有机会执行网络请求很稀疏的时候时间事件也不会被饿死。理解了这个模型再看那些为什么Redis每秒会做一次后台操作为什么毫秒级定时不选这个循环之类的问题就都很清楚了。3. 读请求现场readQueryFromClient把数据放到了哪里3.1 从socket到querybuf的搬运工当客户端连接上有数据可读时事件循环会触发readQueryFromClient。这个函数名字相当直白它的任务就是从连接里把数据读出来放进客户的输入缓冲区。在Redis 7.x里连接层抽象成了connection结构底层可以是TCP套接字、TLS套接字或者Unix socket。readQueryFromClient的前几行长这样void readQueryFromClient(connection *conn) { client *c connGetPrivateData(conn); ... readlen PROTO_IOBUF_LEN; /* 默认16KB */ ... nread connRead(c-conn, c-querybuf c-querybuf_cur_pos, readlen); }这里的c-querybuf就是客户端的输入缓冲区类型是SDS字符串而不是普通的char*数组。用SDS的好处是长度信息现成、二进制安全——别忘了RESP协议里可能出现\r\n之外的任意字节靠\0判断结尾是会出事的。每次读操作的目标长度是16KBPROTO_IOBUF_LEN。为什么是16KB这是典型的性能折中太小会导致read系统调用次数变多、上下文切换开销变大太大则单次read占用时间过长影响事件循环的响应速度。16KB覆盖了绝大多数Redis命令的请求体实测下来吞吐和延迟都很均衡。3.2 输入缓冲区的扩容与上限读到数据之后readQueryFromClient会做两件事一是可能对输入缓冲进行合理扩容二是调用processInputBuffer进入解析阶段。扩容走的是SDS的常规路径如果当前剩余空间不够16KB就自动扩展但Redis也不是让输入缓冲无限膨胀它会检查客户端的输入缓冲是否超过硬性限制。这个限制在哪里server.client_max_querybuf_len默认是1GB。为什么需要这个保护设想一个场景某个客户端连接因为网络问题或者客户端自身bug一直发数据但服务端处理不过来了querybuf就会不断堆积。如果没有上限内存会直接被拖垮。超过限制后Redis会记录日志然后断开这个客户端并加一条说明让运维知道哪个客户端把连接搞坏了。源码里的日志大概长这样Closing client that reached max query buffer length尽管1GB这个默认值看着很大但在多租户共享实例或者存在异常客户端的场景里它确实是一道保命防线。笔者在实际运维中曾见过因为慢客户端导致内存暴涨的case当时就是从querybuf长度异常这个指标发现了问题。3.3 多线程I/O在这里如何介入如果你配置了io-threadsreadQueryFromClient里还有一个隐藏分流逻辑。Redis 6.0之后多线程I/O可以在读阶段介入主线程读了一部分连接的数据之后可以把解析工作也分配给I/O线程吗不是的——解析仍然是主线程干的活I/O线程只做数据的connRead读取。在readQueryFromClient里主线程判断如果当前连接属于I/O线程的处理范围就把这次读取记账到待读列表由后台线程并发读取到各自的querybuf里主线程则先处理其他不在I/O线程范围内的连接等到读任务全部完成后再统一进入解析阶段。这里要澄清一个常见误解Redis的多线程I/O不是多线程处理命令而是多线程搬运数据。真正执行命令的call过程依然只有一个线程。所以如果你的Redis瓶颈在网络读写本身开io-threads可能有效如果瓶颈在命令实现的CPU计算上开多少I/O线程都白搭。这也是为什么官方文档反复强调4核机器上默认关闭多线程I/O因为收益不明显还会增加复杂度。4. 协议解析processInputBuffer与RESP的逐字节博弈4.1 两条分支inline命令与multibulk命令processInputBuffer是命令解析的主控函数。它在一个while循环里持续处理querybuf中已有的数据直到缓冲区里已经没有一条完整的命令为止。每次进入循环Redis会先判断当前客户端使用哪种协议格式。判断依据非常朴素看querybuf的第一个字节是不是*。如果是按multibulk格式解析如果不是按inline格式解析。在Redis 7.x里这个状态缓存在c-reqtype里避免每条命令都重新判断。inline格式是什么Redis支持一种极简协议客户端直接发SET foo bar\r\n用空格分隔参数。这种格式在telnet裸连Redis的时候特别有用——你不需要知道RESP协议细节敲命令回车就能执行。但官方明确不推荐生产环境用inline格式因为它无法覆盖二进制安全的场景——如果某个key或value里恰好有空格或换行inline格式根本没法表达。生产环境默认走的都是multibulk分支也就是RESP协议的标准格式。在processInputBuffer的循环里每次调用processMultibulkBuffer如果能解析出一条完整命令就会设置好c-argc和c-argv然后交给processCommandAndResetClient去执行执行完再回来继续解析下一条。这就是管道pipelining能够生效的底层原因——一次事件触发读入大量命令while循环把同批命令一条条全部执行完性能自然比一问一答高得多。4.2 processMultibulkBuffer中间态multibulklen与bulklenprocessMultibulkBuffer是整个协议解析里最值得细看的函数因为它维护着两个比较难懂的状态量c-multibulklen和c-bulklen。multibulklen表示当前命令还剩多少个参数没读bulklen表示当前这个参数还剩多少字节没读。为什么需要这两个状态因为TCP是流式协议一次read拿到的数据量完全不确定。可能一条命令分三个包到也可能一个包里塞了十条命令。processMultibulkBuffer必须做到这次被调进来时如果上一条命令没读完能从上次的位置继续读而不是从头重来。实际过程分两个阶段。第一阶段当c-multibulklen 0时说明要开始读一条新命令了先解析*count\r\n里这个count把它赋给multibulklen同时给c-argv分配好数组长度。第二阶段进入while (c-multibulklen)循环逐个解析参数。每个参数先解析$len\r\n得到长度然后等待len 2字节的内容参数本体加结尾的\r\n。bulklen初始为-1表示还没读到$的长度读到长度后变成具体数值并开始从querybuf拷贝字节。这个分阶段推进的写法本质是在流式数据上做增量解析。碰到querybuf里数据不够的情况函数直接返回C_ERR但已经解析的中间状态都保存在client结构体里等下一批数据到达时继续。如果不这么设计就得每次把没读完的半条命令缓存下来拼接完整再解析性能和代码复杂度都会差很多。4.3 协议异常处理什么情况会被直接断开processMultibulkBuffer里有一类很防御性的代码专门应对畸形协议。比如*后面的数字不是合法整数、参数个数超过PROTO_MAX_MULTIBULK_LEN1024*1024也就是百万级别、$后面的长度是负数但又不是-1等。这些情况下协议基本可以判断为不可信。碰到协议错误时Redis不会尝试猜客户端意图而是调用setProtocolError给客户端返回一个-ERR Protocol error开头的错误消息然后把querybuf清空、重置解析状态。如果错误特别严重协议格式错到连错误回复都可能写不出去就直接关闭连接。这里有个安全层面的考虑如果Redis对这种畸形协议过于宽容恶意客户端可以通过制造大量协议错误来耗尽CPU或者占满输出缓冲。所以协议解析的逻辑是宁可断开不可纵容。5. 命令查找从字符串命令名到redisCommand结构体5.1 命令表里到底存了什么命令解析完成后c-argv[0]就是一个SDS字符串比如set。下一步要在命令表里把它查找成真正的命令结构体redisCommand。Redis的命令表定义在server.c中名叫redisCommandTable它是一个超大的静态数组。数组里的每一项在启动时都会被插入到server.commands这个字典结构里key是命令名的小写形式value是redisCommand*。看一个命令定义的样子会有更直观的感受以SET为例{set, setCommand, -3, write use-memory string, 0, NULL, 0, CMD_DENYOOM|CMD_WRITE, ...}这里的字段包括命令名set、处理函数setCommand、参数个数规范-3、命令类别描述、ACL分类、键提取规范、命令标志位。arity是-3的含义是至少3个参数负数表示不小于绝对值正数则要求参数个数必须精确等于这个值。SET最少需要三个参数命令名、key、value所以是-3像GET就是2命令名加key固定两个。CMD_WRITE和CMD_DENYOOM是命令标志位里比较核心的两个。CMD_WRITE代表这是一个写命令在从库上执行时会被拒绝或做特殊处理CMD_DENYOOM代表当Redis内存超过maxmemory限制时这条命令会被直接拒绝并返回OOM错误。这套标志位设计得很精巧——它把这个命令有什么性质这个命令在不同环境下能不能执行这类问题从命令实现中抽离了出来变成了命令表的元数据。新增一个命令时开发者只需要声明标志位而不需要在执行路径里到处写特殊判断。5.2 lookupCommand与误写命令名lookupCommand的实现非常直接查字典。Redis把命令名统一转成小写再作为字典key所以SET、Set、set找到的都是同一个命令。命令表里的原始名称用大写但字典查询一律走小写这也解释了为什么Redis命令大小写不敏感。可能有人会问为什么不直接用strcasecmp线性遍历所有命令Redis的命令数量只有两百多个线性遍历其实也能接受。但关键在于lookupCommand不只被普通请求调用在COMMAND、COMMAND INFO、ACL权限校验等场景都会被频繁调用用哈希表查找能把时间复杂度降到O(1)在高QPS场景下这是实打实的收益。5.3 processCommand执行前的层层关卡找到命令结构体之后命令并没有立刻执行。processCommand里有一长串前置校验像安检口一样把不合理请求挡下来。我简单梳理一下顺序和逻辑先做基本检查。查不到命令返回未知命令错误参数个数不符合arity要求返回参数错误。然后是认证和ACL检查如果开启了requirepass且客户端还没认证只放行AUTH等少数命令ACL则细粒度到命令级别和key级别Redis 7.x在ACL阶段甚至可以解析出命令访问了哪些key。再往下是集群模式下的槽位检查。如果Redis以cluster模式运行命令涉及的key不在当前节点的槽位中就会返回MOVED或ASK重定向错误客户端需要根据错误里携带的地址重新请求正确的节点。这一步在单机模式下是跳过的。接着是内存相关检查。如果配置了maxmemory且当前内存超限对于带CMD_DENYOOM标志的命令在尝试内存淘汰之后仍不满足条件则直接返回OOM错误。注意这里有个很容易被误解的点触发内存淘汰发生在processCommand里而不等命令真正执行。因为等命令执行完再淘汰可能已经晚了——写命令可能已经把内存推到非常危险的境地。之后还有持久化状态检查如果Redis正在从RDB等持久化文件中加载数据除了INFO等少数命令其他命令一律拒绝这是为了保证加载过程中数据视图的一致性。此外还有从库只读检查、磁盘错误检查等等。这一连串检查的顺序是有讲究的。越廉价的检查越靠前成本高的检查越靠后。比如查命令表是哈希查找成本低放最前ACL检查涉及用户和key的匹配相对复杂集群重定向可能需要计算CRC16之前已经提前算好就还好。这样设计能让绝大多数不合法请求在最早期就被拒绝而不会白白耗费昂贵的解析和内存操作。6. call()大戏命令真正执行的那一瞬6.1 执行前后的快照dirty、监控与慢查询所有前置检查通过后processCommand会调用call(c, CMD_CALL_FULL)。call函数才是命令真正执行的舞台。call的第一步动作很有意思先拍一张状态快照。它记录当前server.dirty的值到dirty_before。server.dirty是Redis全局的一个计数器记录从上次持久化以来键空间被修改了多少次。命令执行后用新的dirty减去dirty_before就能知道这条命令到底改了多少个键。这个数值会被AOF追加、主从复制和INFO stats里的dirty指标用到。call还会检查是不是有MONITOR客户端在监听。如果有这条命令的参数会被格式化并推送给所有monitor连接。这也解释了为什么MONITOR会影响性能——每条命令执行前都要额外做一次格式化发送在高QPS下开销不小。另外call中还有一个看似不起眼但很重要的计数server.stat_numcommands;简单一句话但被很多人忽视。Redis每秒能处理多少命令就是靠这个计数来统计的。你在INFO stats里看到的total_commands_processed就是从这里累加出来的。做性能压测时观察这个计数的增长速率比看客户端测出的延迟更接近服务端真实处理能力。6.2 proc函数被调用时的上下文在执行命令之前call会用redisCommand里的proc函数指针做一次调用。以SET为例proc就是setCommand。在Redis 7.0之前命令函数返回int成功返回C_OK失败返回C_ERR由调用方统一处理从Redis 7.0开始命令函数的签名变成了void不再有返回值执行结果一律通过addReply系列函数输出。这个改动看似只是签名变化实际上是架构思路的转变以前有一部分命令会在返回C_ERR后由call补充错误响应导致错误响应格式不统一改成void后命令自己负责把所有要写给客户端的响应通过addReply输出要么是正确数据要么是错误信息。这样call就不需要关心命令执行的具体结果了只负责调度。在执行命令期间c-cmd和c-argv都已经准备就绪命令实现可以直接从argv里取参数。这里还要注意一点call对命令执行前后的客户端状态做了检查比如执行前c-flags里可能带有CLIENT_PENDING_WRITE之类的标志执行过程中命令产生的输出会挂到客户端的输出缓冲上而不是直接写socket。6.3 命令执行后的善后工作命令执行完call还要做几件收尾的事。第一件是慢查询日志记录命令执行前的时间点执行后计算耗时差如果超过slowlog-log-slower-than配置的阈值就把这条命令的相关信息命令名、参数、耗时、客户端地址存入慢查询队列。这里有个小坑慢查询日志记录的是命令执行本身的时间不包含等待socket可读、排队的时间所以某些场景下客户端感知到的延迟可能远大于慢查询日志中的值。第二件是针对写命令的脏数据统计。就像前面说的计算server.dirty - dirty_before如果大于0说明这条命令修改了数据那么在主从复制的上下文里这个命令就需要被写入复制积压缓冲区replication backlog供从节点增量同步同时如果开启了AOF这只命令也要被追加到AOF缓冲区等待后续刷盘。第三件是信号传播call会在命令真正执行前设置server.in_exec标志这能防止在命令执行过程中因为某些副作用比如键过期事件再次调用call导致重入问题。这种重入场景在Redis里有明确的禁入设计避免出现递归执行命令的混乱状态。从这里能看出call的名字虽然朴素但它实际上承担了命令监控、统计、持久化联动、复制联动这样一个兜底汇聚点的角色。理解了call的位置就理解了为什么Redis能优雅地把性能监控和持久化功能塞进同一条命令执行路径而不用侵入各个命令的实现。7. 回复之路addReply到writeToClient的最后一公里7.1 两级输出缓冲快速路径buf与兜底路径reply命令执行完后客户端对象c的输出缓冲里已经积累了一堆要发给客户端的数据。写到socket之前这些数据先要经过Redis自己管理的输出缓冲区。Redis针对每个客户端维护了两级输出结构。一级是c-buf一个固定大小的字节数组默认16KBPROTO_REPLY_CHUNK_BYTES另一级是c-reply一个链表每个节点是一块clientReplyBlock内存。addReply的逻辑很聪明能塞进c-buf就塞进去塞不下的部分挂到c-reply链表上。为什么设计成两级而不是直接用一个动态扩张的缓冲区因为大多数命令的回复都很小OK、PONG、一个整数、一个短字符串16KB完全装得下。用固定数组可以避免小回复时做内存分配——堆上分配和释放的成本虽然不高但在每秒百万级别的回复量下就非常可观了。只有回复特别大比如MGET返回几百个长字符串、LRANGE返回一个超大列表才会走链表路径。这里有一个面试中经常问到的点addReply末尾会调用prepareClientToWrite它的作用不是真的去写数据而是登记这个客户端需要被写回。具体来说分几种情况如果回复能完整塞进c-buf那就直接标记一下准备在事件循环退出前的beforeSleep阶段统一写如果回复很大需要走c-reply链表同样也要登记到待写列表里。prepareClientToWrite是写回机制的统一入口它决定了这个客户端是放到server.clients_pending_write链表里还是因为某些原因比如客户端已被标记为要关闭干脆不写。7.2 事件循环退场前的大扫除这里需要解释一下clients_pending_write的作用。Redis早期版本是立刻注册写事件写到不可写为止后来发现这样对事件循环的唤醒次数太多了。优化后的模式是所有产生了回复的客户端先加入待写列表在beforeSleep里统一处理。beforeSleep是aeMain每次进入aeApiPoll阻塞等待之前都要调用的钩子函数。它在Redis源码中的位置很特殊——介于处理完一批事件和继续等待下一批事件之间。beforeSleep会调用handleClientsWithPendingWrites这个函数遍历clients_pending_write列表对每个客户端尝试把输出缓冲里的数据写出去。这种攒一批、写一批的模式极大地减少了系统调用次数。设想一个场景客户端一次pipeline发送1000条命令服务端分几次read全部读入并逐条执行每执行一条都会产生回复。如果没有批量写机制就要注册1000次写事件、可能触发1000次系统调用有了beforeSleep批量写之后1000条回复在同一个写回调里尽量一次性写出效率完全不是一个量级。7.3 writeToClient的背压与截断保护writeToClient是真正通过socket发送数据的函数。它优先处理c-buf里的数据写完再遍历c-reply链表把每个节点里的数据尽量写出去。写的过程中如果socket的发送缓冲区满了write会返回EAGAINRedis会记录当前写不完的位置然后真正注册一个写事件等下次socket可写时从断点续传。这里还有一个非常重要的保护机制client-output-buffer-limit。它分三档——普通客户端、从节点客户端、发布订阅客户端。如果某个客户端的输出缓冲数据量超过配置限制比如普通客户端限制为normal 0 0 0表示不限制但如果设置了比如normal 256mb 64mb 60当输出缓冲超过256MB或者持续60秒超过64MBRedis会直接断开这个客户端。为什么这个保护很重要设想一个消费缓慢的客户端它的socket发送缓冲区一直满服务端writeToClient一直写不完回复就源源不断堆叠在服务端的c-reply链表里。如果客户端持续不消费这些堆积会占满内存最终拖垮整个Redis实例。这就是典型的慢客户端拖垮服务端场景。理解了输出缓冲的机制再去看CLIENT LIST里omemoutput memory字段就会明白为什么这个字段能用来定位慢客户端问题了。写回完成后handleClientsWithPendingWrites会检查是否全部写完如果还有数据没写完说明socket处于拥塞状态这时才注册可写事件如果都写完了就无需注册让客户端继续安静地等待下一条命令。这就是我之前提到被动注册写事件的落地实现也是Redis在事件调度上节省CPU的关键一手。我在追这段源码时最感慨的一点是Redis并没有用多高的技术每个环节都是朴素的工程手段——批量写、被动唤醒、分级缓冲、背压保护。但这些手段组合在一起就构成了一套极端情况下依然能稳定工作的系统。尤其当你在线上看到omem暴涨、实例延迟抖动时脑子里如果能映射到这个链条定位问题的速度会比瞎猜快很多。源码探究的真正价值也就在于此不是为了读懂一段代码而是给自己建立一张出问题时去哪一查的地图。
返回列表