无题
在前面详细分析了 Redis 中的核心结构体以及底层数据结构,这些结构体类似于 Java 中的 domain 类,我们要想让这些结构体发挥作用,还缺少两块拼图:一个 Redis 如何使用这些 domain 类,就类似 Java 中的 Service 层;另一个是 Redis 的线程模型。
这一节,我们主要介绍一下 Redis 的线程模型。
Redis 线程模型演进史我们常说的“Redis 是一个单线程应用”指的是 Redis 在处理客户端的请求时,都是由唯一的主线程进行处理的,其中包括了请求的读取和解析、命令的执行以及响应的返回。这个描述在 Redis 4.0 版本之前,是比较准确的。
从 4.0 版本开始,Redis 就已经不是纯粹的单线程应用了。除了主线程外,Redis 开始使用后台线程处理一些比较耗时的操作,例如,清理脏数据、释放超时连接、删除大 key 等,但是网络读写、执行命令还是只使用单线程来处理。Redis 4.0 以及之前版本的核心线程模型如下图所示:
Redis 之所以使用单线程是因为 Redis 执行的是纯内存的操作,Redis 服务的瓶颈不在 CPU,而是在网络 ...
无题
介绍完 IO 多路复用的好处以及 Linux 系统中 epoll 的基本使用之后,我们接下来就展开分析一下 Redis 对 I/O 多路复用模块的封装。
正如前面在 epoll 示例中看到的,我们的程序代码其实是围绕 epoll 监听到的各种事件展开的,也就是我们常说的事件驱动。为了统一多种 IO 多路复用器的实现,Redis 构建了一个 ae 库,全称叫 a simple event-driven programming library,如下图所示:
aeApiState 解析在 ae 这个库里面,Redis 通过 aeApiState 结构体对 epoll、select、kqueue、evport 四种 IO 多路复用的实现进行了适配,让上层调用方感知不到不同系统在 I/O 多路复用实现上的差异性。对上述四种 I/O 多路复用的适配分别对应 ae_epoll.c、ae_select.c、ae_kqueue.c、ae_evport.c 四个文件,后面我们依旧以最常用的 epoll 为例来介绍。
首先来看 aeApiState 结构体,其中维护了 epo ...
无题
通过前面两节的介绍,Redis 对网络事件、时间事件这两种事件的抽象和核心处理框架,我们已经都有所了解了。但是,我们现在还有几个细节部分是缺失的,例如:
Redis 通过 initServer() 之后,已经在 6379 端口号上进行监听了,那 Redis 客户端发来建连请求的时候,Redis Server 是如何处理的呢?
Redis 客户端与 Redis Server 创建连接成功之后,这些新建连接又是如何再注册到 I/O 多路复用模块上的呢?上面这两个问题呢,都可以在之前注册的 acceptTcpHandler() 回调函数中找到对应的答案。
客户端与 Redis Server 建连之后,必然是要执行命令的,前面介绍 Redis 线程模型的时候也说过,这些命令是由 IO 线程读取并解析的,具体的解析逻辑是什么样的?解析的命令是怎么交给主线程执行的呢?主线程执行完命令之后,是怎么把返回值交给 IO 线程的?IO 线程又是怎么返回到 Redis 客户端的呢?
上面这些问题我们将用接下来四节的篇幅,全部说清楚,并且在说明这些问题的时候,也会将之前留的坑填好,比如说: ...
无题
通过上一节的介绍,我们已经了解了 Redis 在收到客户端建连请求时的核心处理逻辑。在建连成功之后,会封装相应的 connection 以及 client 对象,还会在 IO 多路复用器上注册可读事件的监听,为读取客户端发来的请求做好准备。
这一节,我们就重点来看 Redis 是如何读取和解析客户端发来的请求。
connSocketEventHandler() 与 readQueryFromClient()在 Redis 客户端发来请求的时候,相应的底层连接会触发可读事件,通过上一节对建连过程的分析我们知道,客户端连接上可读事件的处理函数是 CT_Sokcet->ae_handler,它实际指向了 connSocketEventHandler() 函数。这也是我们本节第一个要介绍的函数。
connSocketEventHandler() 函数里可以同时处理可读事件和可写事件,默认会先处理可读事件,然后再处理可写事件。调用方可以在 connection->flags 字段中设置 CONN_FLAG_WRITE_BARRIER 标志位,来翻转可读可写事件的处理顺序,这与第 28 ...
无题
在上一节中,我们详细分析了 IO 线程的一些内容以及 readQueryFromClient() 函数的逻辑,这些都是我们理解 Redis 多线程模式下读取客户端请求核心所在。
在 readQueryFromClient() 函数中读取到 client->querybuf 缓冲区的都是一个个的字节,Redis Server 接下来要做的就是,把这个 byte 数组中的内容,按照一定的规则,解析成 Redis Server 能够理解的命令。这部分逻辑就是在 readQueryFromClient() 函数最后调用的 processInputBuffer() 函数中完成的。
RESP 协议基础知识不过,在开始 processInputBuffer() 函数的介绍之前,我们需要先说一些 Redis 命令解析的基础知识。
第一个基础知识点是 Redis 客户端的请求类型,对应的是 client->reqtype 字段,它有两个可选值 PROTO_REQ_INLINE、PROTO_REQ_MULTIBULK。其中,INLINE 是内联请求类型,一般是 Telnet 这种客户端发出来的 ...
无题
在上一节中,我们详细介绍了 Redis 在 IO 多线程模式下,命令解析和命令执行的核心逻辑。小伙伴们可能会产生这样一个疑问:我们调用的 redisCommand->proc() 函数的时候,是没有返回值的,那命令执行产生的返回值是怎么返回给客户端的呢?
下面我们就来详细分析下这个问题。
数据返回通过前面的介绍我们知道,Redis 在 IO 多线程模型下,命令产生的返回值是通过 IO 线程写回给客户端的,那既然 redisCommand->proc() 函数没有返回值,我们就会猜测 proc() 函数里面会把返回值写入到某个指定的地方,然后 IO 线程会去这个地方取该结果值,然后返回给客户端。
这里我们以 GET 命令为例进行分析,GET 命令对应的 proc 处理函数是 getGenericCommand() 函数,其核心逻辑如下:
123456789int getGenericCommand(client *c) { robj *o; // 从Redis DB里面中查找value值 if ((o = lookupKeyReadOrReply( ...
无题
通过前面几节的介绍我们知道,Redis 中的事件驱动中,除了网络事件之外,还有时间事件,但是在前文的介绍中,我们完全没有提及到这部分内容。因此,在这一节中,我们就来补齐 Redis 时间事件的相关内容。
不过,在这之前,我们先一起来回顾一下 Redis 是如何处理时间事件的。你可以把 Redis 中的时间事件,理解成定时任务,正如第 28 讲《内核解析篇:Redis 事件驱动核心框架解析》所说,这些时间事件与维护在 aeEventLoop->timeEventHead 链表中的 aeTimeEvent 实例一一对应。
在第 28 讲《内核解析篇:Redis 事件驱动核心框架解析》介绍 aeProcessEvents() 函数的时候我们看到,在它最后,会调用 processTimeEvents() 函数去处理时间事件,其核心逻辑就是遍历 aeEventLoop->timeEventHead 链表。在遍历过程中,先会检查每个 aeTimeEvent 元素的 id 值是否为 AE_DELETED_EVENT_ID(-1),以及 refcount 是否为 0。如果满足这两个条件,表 ...
无题
在使用 Redis 做缓存之类的非持久化存储时,我们一般会给 Key 设置一个过期时间,在 Key 到期之后,Redis 就会把这个 Key 自动删除掉,我们之后就再也拿不到这个 KV 数据了。
那 Redis 是如何将过期 Key 清理掉的呢?常见的过期 Key 清理方式(也被称为“过期策略”)有三种:定时过期、惰性过期以及定期过期,我们简单介绍一下三者的核心区别以及 Redis 采用的策略。
定时过期策略:该策略需要为每个 Key 关联一个定时器(或是一个全局定时器)记录 Key 的过期时间,当 Key 到期时由定时器触发过期事件,触发执行 Key 的清理逻辑。定时过期策略可以立刻清理过期 Key,释放内存,但是需要额外维护定时器这种复杂的结构。
惰性过期策略:该策略是在客户端访问一个 Key 的时候,判断目标 Key 是否已经到期,如果到期了,就会将其删除,并且返回给客户端 Key 不存在。除此之外的其他时间不会主动去清理 Key。惰性过期策略实现比较简单,不会占用单独的 CPU 时间去执行 Key 过期的操作,而是平摊到了每次 Key 的访问中,但是,如果客户端长时间不访问 ...
无题
在前面几讲中,我们重点讲解了 ziplist 和 listpack 这两个结构,它们都是连续的内存空间,也是构成 Redis List 的关键结构之一。
Redis List 底层的另一个关键结构是 quicklist,本节我们就来重点分析 quicklist 的核心结构以及控制 quicklist 特性的关键配置,另外,还会介绍 quicklist 迭代器的设计思想和具体实现。
注意哦,本节介绍的 Redis 7.0 版本的 quicklist 实现,与 Redis 6 以及之前的主要差别就是底层把 ziplist 换成了 listpack。
quicklist 核心概述quicklist 是一个类似于 Java 里面 LinkedList 的双向链表,大概结构如下图所示:
quicklist 里面的节点是 quicklistNode 类型,quicklistNode 里面维护了 next、prev 指针,指向前后两个 quicklistNode 节点;然后还有一个 entry 指针,指向了一个 listpack 实例。真正的元素是存储在这个 listpack 里面的,那就是说 ...
无题
在上一讲中,我们重点介绍了 quicklist 的核心结构,一起分析了 quicklist 里面关键的结构体定义以及关键的配置含义。本节我们将继续重点介绍 quicklist 的核心函数,主要包括:插入数据、弹出数据以及查询数据这三方面的函数实现。
创建 quicklist首先来看创建 quicklist 实例的 quicklistNew() 函数,可以用 CLoin 调用链的视图看一下它都调了哪些函数。
可以看到,quicklistNew() 函数先会调用 quicklistCreate() 创建一个空 quicklist 实例,里面就是走 malloc 分配内存,然后通过 quicklistSetCompressDeth() 和 quicklistSetFill() 函数来初始化 compress 和 fill 两个字段。
插入数据向 quicklist 插入一个元素的入口函数是 quicklistPush() 函数,其核心代码片段如下:
123456789void quicklistPush(quicklist *quicklist, void *value, const si ...
