无题
在前面的章节中,我们已经详细介绍了 Redis 主从复制的核心原理,以及从库视角下主从复制的核心实现。从本节开始,我将和小伙伴们一起,分析一下主库视角下的主从复制,这样整个主从复制的实现就完整了。
在从库发起建连操作时,主库是无法立刻识别出该建连请求是来自从库的,会将其作为一个普通客户端进行处理,为其创建对应的 client 实例。这里注意 client 中的 replstate 字段,它记录了该从库状态的变更。在下图中,展示了主库与从库进行握手的核心流程,最右侧就是从库对应的 client->replstate 状态的变化流程:
在从库与主库建立连接之后,从库会向主库发送 PING 和 AUTH 命令进行探活和鉴权,此时从库在主库眼中与普通客户端无异,主库会正常地进行鉴权和响应。
接下来,从库会连续发送三条 REPLCONF 命令,将从库的 ip、port 以及从库支持的能力告知主库,在主库侧会根据 REPLCONF 命令的参数更新不同的字段,例如:
主库会将从库端口号记录到 client->slave_listening_port 字段中;
主库会将从库的 ip 记 ...
无题
在上一节中,我们已经详细介绍了复制缓冲区的设计演进过程以及主库视角下部分同步的核心。在这一节,我们继续来分析一下主库视角下全量同步实现。
全量同步通过上一节的分析我们知道,如果主从之间能进行部分同步,需要检查 Replication ID、Replication Offset 等一系列条件是否成立,如果部分同步的条件不成立,就会进入全量同步的逻辑。在进行全量同步的时候,主库首先执行与部分同步类似的状态更新操作:
将 client->replstate 状态为 WAIT_BGSAVE_START,表示主库要为全量同步执行一次 RDB 持久化。
向 client->flags 中设置 CLIENT_SLAVE 标记,标识这是一个从库 client 实例。
将 client 添加到 server.slaves 列表中。
如果当前主库之前的 backlog 缓冲区一直为空,那从库必然只能进行全量同步,此时会初始化 replBacklog、在 redisServer.replid 字段中填充新生成的 Replication ID 、清空 Secondary ID 和 Second ...
无题
Redis 最常见的应用场景就是缓存,我们在使用缓存的时候,一般不会存储 DB 里面全量的数据,而只用于缓存一部分 DB 热点数据,对于非热点数据,需要进行定期删除,防止 Redis 内存被撑爆,也就是我们常说“内存淘汰”机制。
在前面第 33 讲《内核解析篇:Redis 时间事件的二三事》介绍 serverCron() 函数的时候提到,其中会更新 LRU 时钟,使用 LRU 时钟的地方有两个:
一个是在客户端访问一个 Key 时,会使用 Value 值中的 lru 字段记录当前的 LRU 时钟;
另一个是在 estimateObjectIdleTime() 函数中,会通过前面记录的 LRU 值,推算该 Key 空闲了多久,Redis 会按照一定的淘汰算法将最久没被访问的 Key 删除掉,防止 Redis 内存超过 maxmemory 指定上限。
内存淘汰策略这里我们先来看 redisServer 中的 maxmemory 字段(对应 redis.conf 中的 maxmemory 配置项),它指定了 Redis 的最大内存,单位是 byte,当 Redis 内存占用达到这个值的时 ...
无题
在上一模块中,我们重点介绍了一个单机 Redis 是如何运行的,着重分析了单机 Redis 的线程模型、事件模型、整个请求-响应的处理流程等内容。但是,没有涉及到请求中命令的具体情况。这一模块我们就来重点介绍一下 Redis 是如何在底层数据结构之上,实现我们常用的命令的。
这一模块中,我们根据 Redis 命令底层实现的相关性,分成了下面几篇进行介绍:
通用命令在 Redis 命令分类里面, Generic 分类中的命令并不与任意一种数据结构对应,而是可以操作所有类型的结构。下图对 Generice 分类中的命令,做了进一步的分类:
这里我们就展开介绍实践中比较常用的 Generic 分类中的命令。
查看 Key 信息OBJECT 命令是 Redis 中用来查看一个 Key 元信息的命令,它有几个子命令。
最常用的就是 OBJECT ENCODING 命令,它会返回指定 Key 的编码方式,获取的是对应 redisObject 对象的 encoding 字段值。
OBJECT REFCOUNT 命令会返回指定 Key 被引用的次数,其实就是对应 redisObject 对象的 ...
无题
在上一节中,我们详细分析了 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 ...
无题
在我们常用的关系型数据库中,事务指的是一组 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 读写的数据都是存储在内存中的,那么一旦出现机器宕机、服务重启等情况,内存中的数据就丢失了。
为了让数据能够持久化地存储,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 ...
