无题
在上一节中,我们详细分析了 Redis 里面通用命令和 String 命令在实现方面的一些注意点。这一节,我们接着来介绍一下 Hash 和 Set 两个集合结构相关命令的实现。
哈希表相关命令从实现的角度看,我们这里把哈希表命令的关键点分成了底层存储转换、添加键值对、读取键值对和删除键值对四个部分。
底层存储转换前面第 26 讲《内核解析篇:Redis 核心结构体精讲》我们介绍 redisObject 结构体的时候提到,Redis 中的哈希表结构(type 为 OBJ_HASH)的底层存储结构有两种:一个是 dict(encoding 为 OBJ_ENCODING_HT),一个是 listpack(encoding 为 OBJ_ENCODING_LISTPACK)。
当同时满足“键值对数量小于 hash-max-ziplist-entries 配置值,且每个键值对中的 Field 和 Value 长度都小于 hash-max-ziplist-value 配置值”这两个条件的时候,使用 listpack 这个连续空间作为底层存储。随着键值对的插入和修改,在不满足上述两个条件的时候,Red ...
无题
通过前面第 6 讲《实战应用篇:List 命令详解与实战(上)》的介绍我们知道,Redis 中的 List 抽象了一个双端 List 的数据结构,Redis 提供了丰富的 List 命令来从队列两端读写命令。另外,Redis 中没有 Stack 这种数据结构,我们可以通过只操作 List 的一端来模拟 Stack 结构的特性。
从实现角度来看,我们把 List 相关的实现逻辑分成了写入数据、弹出数据以及阻塞操作三大部分。这也是我们本节要介绍的三个核心内容。
写入数据在前面我们已经详细介绍过 LPUSH、RPUSH 这类写入数据的命令,下面来这些 PUSH 命令的底层实现,如下图调用栈所示,它们底层都依赖于 pushGenericCommand() 这个公共函数:
pushGenericCommand() 函数中有三个参数,含义如下:
1234567void pushGenericCommand(client *c, // 发送命令的客户端 int where, // 对List的左端还是右端进行操作,例如,LPUSH命令中该值为0 int xx // 在List不存在的时候,是否自 ...
无题
在前面第 11 讲《实战应用篇:Sorted Set 命令详解与实战》和第 26 讲《内核解析篇:Redis 核心结构体精讲》中,我们已经详细分析了 Sorted Set 数据类型的底层实现、命令使用以及应用场景,这一节,我们就来详细介绍一下 Sorted Set 相关的命令实现。
从实现角度,我们可以把 Sorted Set 相关的命令分为单元素操作和范围查询。
单元素操作在 Sorted Set 中,我们最常用命令就是 ZADD 命令了,它对应的处理函数是 zaddGenericCommand() 函数,下面就来看看它的核心逻辑。
插入元素首先,zaddGenericCommand() 解析并检查 ZADD 命令中各项参数,这里会将 NX、XX、GT 等参数转换成对应的临时变量,代码片段如下,后续会根据这些临时变量值调整 zaddGenericCommand() 函数的行为。
123456789int incr = (flags & ZADD_IN_INCR) != 0; // 新score会增加到原score中,而非覆盖int nx = (flags & ZADD ...
无题
通过前面章节的介绍我们知道,Redis 之所以快,主要是因为 Redis 读写的数据都是存储在内存中的,那么一旦出现机器宕机、服务重启等情况,内存中的数据就丢失了。
为了让数据能够持久化地存储,Redis 分别提供了 RDB 和 AOF 两种持久化方式。从这一节开始,我们将首先重点来介绍 Redis 中的 RDB 持久化。
RDB 特性RDB 持久化就像是给 Redis 的整个内存做了一个快照,然后把这个快照持久化到一个 .rdb 文件中。Redis 在重启时,可以通过加载 RDB 文件快速恢复 Redis 内存数据,但是需要说明的是,由于 RDB 持久化的快照特性,Redis 会丢失最后一次 RDB 文件到重启之间的数据,如下图所示,蓝色部分的数据在 RDB 持久化的时候已经被保存下来了,但是红色部分的数据,会因为宕机而丢失。所以需要后面介绍的另一种持久化方式,也就是 AOF,来辅助实现 Redis 崩溃后的完整数据恢复。
触发 RDB 持久化了解了 RDB 持久化的特性之后,我们来看如何触发 RDB 持久化。
首先是手动触发方式,主要是通过 SAVE 和 BGSAVE 两个命令。 ...
无题
在上一节中,我们介绍了 RDB 文件持久化的关键流程,其中涉及到触发 RDB 持久化的条件、RIO 层的抽象以及写入 RDB 文件的流程框架以及相关优化点。
这一节,我们就开始深入 rdbSaveRio() 函数,详细分析一下 RDB 文件写入的具体流程,在分析具体代码实现的同时,我们还会介绍一下 RDB 文件的组成结构。
OpCode在 RDB 文件中,有一个 OpCode 的概念,说白了就是一些特殊字节,这些字节用来表示紧跟其后一段字节存储的是什么内容。
下面是我们需要重点关注的几个 OpCode:
OpCode
Desc
0xFA
在 0xFA 后面紧跟的是一个 AUX 键值对,用来在 RDB 文件头中记录一些元数据信息
0xFE
在 0xFE 后面紧跟的是数据库的编号,用来标记后续数据归属的 redisDb
0xFB
在 0xFB 后面紧跟的是数据库中 Key 的个数以及设置了过期时间的 Key 的个数
0xFD、0xFC
这两个 OpCode 后面紧跟的是 Key 的过期时间,0xFD 后面紧跟的是秒级时间戳,0xFC 后面紧跟的是毫秒级时间戳
0 ...
无题
分析完写入 RDB 文件的格式以及 RDB 持久化的核心实现之后,我们回到触发 RDB 持久化的地方会发现,除了 SAVE 命令之外,其他 rdbSave() 函数调用都是来自 rdbSaveBackground() 函数,如下图调用栈所示:
rdbSaveBackground() 函数的核心在于,使用 fork() 调用创建一个子进程,并在子进程中调用rdbSave() 函数,完成后台的 RDB 持久化操作。在 redisServer 中有一个 child_pid 字段,它用来记录当前 Redis 创建的子进程 id,这个子进程可以是用来进行 RDB 持久化的,也可以用来做 AOF Rewrite 操作的,或是其他 Module 需要的后台操作。但是,Redis 同一时刻只能有一个子进程,这个子进程的 id 会被记录到 child_pid 字段中。
创建 RDB 子进程下面我们就先来看看用于 RDB 持久化的子进程是怎么创建出来的,下面是 rdbSaveBackground() 函数的核心逻辑。
它首先会调用 hasActiveChildProcess() 函数来检查 server ...
无题
前文我们已经详细介绍了 Redis 中 RDB 持久化的相关内容,本节我们开始介绍一下 Redis 中另一种持久化方式 —— AOF 持久化。
通过前面章节的介绍我们知道,RDB 是一个类似于快照的持久化方式,它会一次性将 Redis 内存中的全部数据写入到 RDB 文件中。AOF(Append Only File)持久化则是类似于增量的持久化,其核心思路是将 Redis 执行过的每条修改命令都保存到 AOF 文件中,从而实现持久化效果。当故障恢复的时候,Redis 可以根据 AOF 文件回放曾经执行过的每一条命令,这样的话,Redis 中的数据也就恢复到故障前的状态了。
在实际生产环境中,一般会使用 AOF + RDB 的混合持久化方案来达到最高效的持久化效果,这个方案是 RDB 定时全量持久化,这样在故障恢复时就可以将 Redis 恢复到 RDB 创建时的状态,然后回放 RDB 创建时间点到故障时间点之间的 AOF 日志,将 Redis 从 RDB 文件快照下的状态恢复到故障时间点的状态。
这样做主要从两个方面考虑。
第一方面是:RDB 持久化这种全量持久化方式,是个比较耗时的操 ...
无题
在我们常用的关系型数据库中,事务指的是一组 SQL 语句,这一组 SQL 要么全部执行成功,要么全部执行失败,这一组 SQL 语句是一个不可分割的单位。关系型数据库中的事务需要满足原子性、一致性、隔离性和持久性四个特性,也就是常说的 ACID 特性。
但是,Redis 是一个 KV 类型的 NoSQL 数据库,并不是一个关系型数据库,而且 Redis 并没有完整支持 ACID 特性,所以关系型事务特性这里不做过多讨论,我们来专注于 Redis 中的事务实现。
Redis 中与事务相关的命令有 MULTI 、 DISCARD 、 EXEC 和 WATCH 四条命令。
首先,我们使用 MULTI 命令用来开启一个事务,然后就可以开始往 Redis 发送命令,这些命令都属于一个事务。注意,这些 Redis 命令在到达 Redis Server 之后,并没有被立即执行,而写入到 client 实例中的一个缓冲队列里面暂存。
在我们把这个事务里面全部的命令都发送到了 Redis Server 之后,就可以再发送一条 EXEC 命令来提交事务了。在 Redis Server 收到 EXEC 命 ...
无题
通过前面两节的介绍,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 ...
