You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/* Include the best multiplexing layer supported by this system. * The following should be ordered by performances, descending. */#ifdefHAVE_EVPORT#include"ae_evport.c"#else#ifdefHAVE_EPOLL#include"ae_epoll.c"#else#ifdefHAVE_KQUEUE#include"ae_kqueue.c"#else#include"ae_select.c"#endif#endif#endif
服务运行 ID :每个 Redis 服务都有自己的运行 ID(run ID),这个 run ID 在服务启动时生成,是一个 40 位的 16 进制字符串。初次复制时,主服务会将这个 ID 发送给从服务,从服务会将主服务的 ID 保存起来,当从服务断线之后重新连接上主服务时,会对比这个 run ID,如果和重连之后的主服务 ID 一致,则说明重连上的是之前的主服务,可以执行部分重同步;如果和之前保存的 run ID 不一致,则说明重连上了一台新的主服务,需要执行完全重同步。
92740:S 28 Jun 2022 16:05:50.168 * Background saving started by pid 93770
93770:C 28 Jun 2022 16:05:50.170 * DB saved on disk
93770:C 28 Jun 2022 16:05:50.171 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
92740:S 28 Jun 2022 16:05:50.221 * Background saving terminated with success
1 数据结构
根据使用场景的不同,我将 Redis 的数据结构分为外部数据结构和内部数据结构,其中外部数据结构是我们通过 redis-cli 访问 redis-server 时使用到的各种数据结构,比如 List、Hash、Set 等,内部数据结构是指实现这些外部数据结构时,在程序内部实际使用到的一些数据结构。
1.1 外部数据结构
外部数据结构包括:string、list、set、hash、sorted set(忽略了 bloom filter 等高级数据结构)。
其中:
关于这些数据结构的使用方法,参考 菜鸟教程。
1.2 内部数据结构
在 Redis 的内部,实现了以下数据结构:
这些数据结构是外部数据结构的基础。
1.2.1 SDS
Redis 使用 C 语言编写,为了避开 C 语言原生字符串的一些坑,Redis 自己实现了一个简单动态字符串结构来存放字符串,以其获得以下收益:
SDS 的基本结构为:
结构示例:
Redis 内部的许多 string 变量都通过 SDS 存储,它是 string 类型值的底层数据结构之一(也可能是 int 类型)。
1.2.2 LinkedList
Redis 内部实现的双向链表,是 List 结构的底层数据结构。
链表节点的基本结构:
链表的数据结构:
双向链表数据结构图示:
1.2.3 Dict
Dict 是 Redis 内部最重要的数据结构,除了用来实现 Hash 字典和 Set 集合之外,Redis 最基本的键值对数据库也是基于 Dict 构建的。Dict 底层又依赖 dictht(hash table) 来实现。
dictht 基础数据结构如下:
图示如下:
其中 table 是一个 dictEntry* 类型的数组,size 代表数组的大小,sizemask 代表用于通过 hash 计算数组索引的掩码,used 代表数组中已经存在的元素个数。
dictEntry 类型如下:
key 保存键值对中的键,v 保存键值对中的值。
next 指针的作用是在哈希碰撞时,多个存储在同一索引的 dictEntry 对象以链表形式进行存储。
Dict 结构如下:
其中的 ht 字段就依赖了上文提到的 dictht 结构,type 字段会针对不同的字典元素提供不同的工具函数集合(比如 hash 计算函数……)。
ht 字段之所以长度为 2,是因为两个 dictht 结构一个用于提供数据存储服务,一个用于在 rehash(重新散列)过程中存放数据。当没有在 rehash 过程中时,rehashidx 的值为 -1。
一个完整的 dict 结构如下:
dict 的 hash 计算方式:
计算出 index 值之后,就可以在 dictht 结构的 table 字段上进行位置索引了。
当使用 dict 结构作为 Redis 的全局键值对数据库实现基础时,hash 算法为 MurmurHash 2。
1.2.4 SkipList
跳表是有序集合的底层实现结构之一。
跳表的结构示意图如下:
跳表节点的数据结构如下:
其中:
1.2.5 IntSet
IntSet 是集合(Set)的底层数据结构之一。
encoding 表示当前数组元素的类型,虽然 contents 数组元素的类型为 int8_t,但是实际上它可以存储多种类型的元素,这取决于 encoding 的值。
contents 数组存储集合元素的值,且其中的元素是从小到大有序排列的。
length 代表 contents 数组的长度。
当新加入的元素数据长度大于当前数组元素的长度时,会进行“升级”操作,将数组中所有元素都进行类型转换,且移出这个长度太大的元素之后,数组也不会进行降级操作。
1.2.6 ZipList
压缩列表是 List 和 Hash 的底层数据结构之一,压缩列表是 Redis 为了减少内存使用而创建的类型。
压缩列表是一块连续的内存,其中可以存储不定长度的的元素,元素可以是字节数组或者整数数字。
压缩列表数据结构如下:
各个部分的含义:
由于压缩列表的特殊性(entry 节点会标记前一个节点的长度且标记位长度不定),会存在连锁更新的问题,但是这种问题出现的几率不大,所以一般不会对性能造成影响。
1.2.7 redisObject
redisObject 是 Redis 内部用来统一表示键和值的数据结构,其定义如下:
其中 type 表示不同的对象类型,比如 List、String、Hash、Set 或者 ZSet。
encoding 表示不同的编码,表示用来实现 type 这些数据结构的内部数据结构。
通过 type 命令可以查看 key 对应的 value 的数据结构(Hash、List、Set 等),通过 object encoding 命令可以查看 key 对应的 value 的数据结构的底层数据结构(SDS、IntSet 等)。
1.3 结构映射关系
内外部数据结构的基本映射关系如下:
2 线程模型
上方是 Redis 的线程模型,当多个客户端连接请求被接受时,会基于 IO 多路复用模型(事件驱动模型)进行监听,当事件发生时,连接对象会被依次放入队列中,然后一个一个派发给文件事件分派器。
文件事件分派器接受到连接对象之后,会根据事件类型将连接对象分配给不同的处理器,不同的处理器负责处理不同的时间(读、写、连接等)。
Redis 实现了 IO 多路复用的各种模型,在底层调用 select、evport、kqueue、epoll,最终在运行时根据不同的环境选择最优的实现:
源码中通过上面这段代码选择不同的 IO 多路复用模型。
3 Redis 集群
Redis 支持三种集群模式,分别是主从复制、哨兵模式和 Cluster 模式。
3.1 主从复制
Redis 通过数据持久化机制确保了在故障时也能将数据保存在磁盘上,但是如果机器磁盘故障,则持久化机制也无法确保数据不丢失。为了避免单机服务故障导致的数据丢失问题,Redis 支持在多个服务之间进行主从同步。
通过 SLAVEOF 命令可以将当前访问的 Redis 服务设置为指定 IP 和 端口对应的 Redis 服务的 Slave 服务,Slave 服务会从 Master 服务同步数据以确保主从服务之间的数据一致性。
当一个 Redis 被指定为另一个 Redis 服务的从服务后,数据同步也就开始了。数据同步步骤如下:
以上的一致状态只是暂时的,当主服务的数据发生变化后,仍然需要和从服务进行同步。所以为了让主从服务保持动态的数据一致性,每次主服务的数据发生变化后,都会将自己执行的导致数据发生变化的写命令广播给从服务,从服务执行对应命令之后,主从服务的数据便又达到了一致。
以上步骤适用于 Redis 2.8 之前的版本,因为这种同步机制对于部分重同步场景并不友好,所以在 Redis 2.8 版本在上面的同步机制的基础之上完善了部分重同步模式,并使用 PSYNC 命令代替了 SYNC 命令;在完全重同步的场景下,PSYNC 和 SYNC 命令的执行方式基本一致,区别在于部分重同步。
部分重同步由以下三个部分构成:
复制偏移量 :主从服务都会维护一个自己的复制偏移量,主服务每向从服务传递 N 个字节的数据,就会将自己的复制偏移量加上 N,从服务每次接受到主服务的 N 个字节的数据,就会将自身的复制偏移量加上 N。这样只要判断主从服务的偏移量是否一致就可以轻易知道主从服务的数据是否一致。
复制积压缓冲区 :复制积压缓冲区是由主服务维护的一个长度固定的先进先出队列,大小默认为 1 MB。当主服务进行命令广播时,不仅会将写命令发送给从服务,而且会将命令写入复制积压缓冲区,所以复制积压缓冲区中会保存着最近执行的鞋命令。
当从服务断线之后重新连接上主服务时,会将自身的复制偏移量发送给主服务,主服务会根据从服务的复制偏移量来确定执行完全重同步还是部分重同步:
服务运行 ID :每个 Redis 服务都有自己的运行 ID(run ID),这个 run ID 在服务启动时生成,是一个 40 位的 16 进制字符串。初次复制时,主服务会将这个 ID 发送给从服务,从服务会将主服务的 ID 保存起来,当从服务断线之后重新连接上主服务时,会对比这个 run ID,如果和重连之后的主服务 ID 一致,则说明重连上的是之前的主服务,可以执行部分重同步;如果和之前保存的 run ID 不一致,则说明重连上了一台新的主服务,需要执行完全重同步。
PSYNC 命令执行流程图:
主从复制的缺点:
3.2 哨兵机制
主从复制确保了数据的安全性,但是当主服务出现故障时,我们需要手动切换主从服务,这也是主从复制的一个缺陷,所以 Redis 官方支持了哨兵机制,哨兵机制基于主从复制模式,在其基础之上加上了哨兵进程来确保服务的可用性。
哨兵机制通过建立独立的 Sentinel 进程来监视主从服务,可以通过 redis-sentinel 或者 redis-server 命令启动一个 Sentinel 进程。
默认情况下,Sentinel 进程会每十秒一次通过 INFO 命令获取主从服务的服务信息(可以自己执行 INFO 命令查看输出信息),每两秒一次广播自身信息和主服务信息到 sentinel:hello 频道,每秒一次向所有建立了连接的主从服务节点和 Sentinel 节点发送 PING 命令获取实例的在线状态。所以一个 Sentinel 进程会监视所有的 Redis 服务节点和所有的其他 Sentinel 节点。
两个 Sentinel 节点监视三个 Redis 节点的示意图:
可以看到 Sentinel 节点之间也是互相监视的。
主观下线 :
Sentinel 配置文件中的 down-after-milliseconds 选项指定了 Sentinel 判断实例进入主观下线所需的时间长度:如果一个实例在 down-after-milliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所对应的实例结构,在结构的 flags 属性中打开 SRI_ S_ DOWN 标识,以此来表示这个实例已经进人主观下线状态。
客观下线 :
当 Sentinel 将一个主服务判定为主观下线之后,会询问集群中的其他 Sentinel 节点这个主服务是否已经下线(is-master-down-by-addr 命令),当它从其他 Sentinel 节点收集到足够多的关于这个主服务主观下线的消息之后,就会执行客观下线操作。
客观下线的条件:主观下线数量超过了配置中的 quorum 值。
故障转移 :
当一个主服务节点被客观下线时,对应的故障转移操作也就开始了。Sentinel 会首先选举出一个领头 Sentinel 节点,随后由这个领头 Sentinel 节点进行故障转移操作。故障转移操作步骤:
3.3 Cluster 模式
Sentinel 模式虽然解决了故障转移的问题,但是它仍然是基于主从复制模式的,这样会有一个缺陷就是集群中的每一个节点都存储了完整的所有数据,造成了存储空间的很大浪费。
为了解决这个问题,Redis 官方支持了 Cluster 模式,通过 CLUSTER MEET 命令即可创建一个 Redis 集群,Redis Cluster 基于分片存储数据。
一个 Redis 集群中可以有很多个 Redis 服务节点,每个节点都只存储一部分数据,它们会分配到不同的 key 去处理。为了达到这个目的,整个集群数据库被分成了 16384 个槽(slot),集群中的每个节点都需要分配一部分 slot 去处理,向集群中的节点发送 CLUSTER ADDSLOTS 即可添加当前节点要处理的 slot。
每个节点内部都有一个 slots 数组来存储 slots 的分配情况,其长度为 16384,每个数组元素都为 0 或者 1,1 代表对应的槽由当前节点处理,0 则反之。
当一条命令被发送给集群中的某个 Redis 节点时,流程如下:
如果对应的 key 不由当前的节点处理,则会返回 MOVED 错误,将客户端重定向到正确的节点进行处理。
计算 key 对应的 slot :
服务节点拿到一个 key 之后先基于 CRC16 算法计算校验和,然后映射到 0 - 16383 这个范围内。
4 持久化机制
Redis 基于内存读写数据,但是也提供了持久化机制以确保数据的一致性。
4.1 RDB 持久化
RDB 文件是一个经过压缩的二进制文件,里面存储 Redis 数据库中的数据。用户通过 SAVE 命令或者 BGSAVE 命令保存数据到 RDB 文件中。
SAVE 命令是一个同步执行命令,会阻塞主线程的执行,让 Redis 服务暂时不可用直到文件创建完毕,所以一般来说这个命令是不推荐使用的。
BGSAVE 命令可以从名字看出来是在后台进行保存,BGSAVE 命令会 fork 出来一个新的子进程,然后在子进程内部进行数据持久化操作:
保存好的 RDB 可以在服务下次启动时直接读入内存中:
4.2 AOF 持久化
除了 RDB 持久化方式之外,Redis 还支持 AOF(Append Only File) 持久化机制。
AOF 通过保存 Redis 服务执行的写命令来保存数据变更记录,Redis 的主线程是一个事件循环(类似于 JS 的 eventloop?),在每个循环的结束会调用 flushAppendOnlyFile() 方法进行数据的写入,这个方法的行为受配置文件中的 appendfsync 参数的影响。
appendfsync 有以下可能的值:
这里的写入和同步是两个概念,操作系统会将对文件的操作缓存在 buffer 当中,并不会立即同步到磁盘文件上,如果想要立即同步可以调用系统函数 fsync 或者 fdatasync。
AOF 数据还原时,会建立一个伪客户端(用以执行命令),然后读取 AOF 中的命令一条一条的执行直到执行完毕。
5 实践操作
5.1 主从同步
通过 redis-server 在本地起三个服务,分别使用 6379、6380 和 6381 端口,然后在 6380 和 6381 端口的 Redis 服务分别执行 SLAVEOF localhost 6379 命令指定它们称为 6379 节点的从服务。
执行完毕之后查看 6379 的 INFO:
# Replication role:master connected_slaves:2 slave0:ip=127.0.0.1,port=6380,state=online,offset=0,lag=1 slave1:ip=127.0.0.1,port=6381,state=wait_bgsave,offset=0,lag=0 master_failover_state:no-failover master_replid:4a7af4a743c0bc7a44d8d0096aec5826f202ab94 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:14 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:14
可以发现它已经有了两个从服务。
此时在主服务执行 SET name russ 添加字符串键,然后到从服务查询:GET name,即可得到刚刚设置好的值 “russ”。
向从服务发送 INFO 命令查看其节点状态:
# Replication role:slave master_host:localhost master_port:6379 master_link_status:up master_last_io_seconds_ago:6 master_sync_in_progress:0 slave_read_repl_offset:679 slave_repl_offset:679 slave_priority:100 slave_read_only:1 replica_announced:1 connected_slaves:0 master_failover_state:no-failover master_replid:4a7af4a743c0bc7a44d8d0096aec5826f202ab94 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:679 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:679
输出了它的身份信息(从节点)以及对应的主节点信息。
5.2 Sentinel
基于 5.1 的基础继续进行实践,在本地新建一个 sentinel.conf 文件:
然后在命令行运行 redis-sentinel 命令:redis-sentinel sentinel.conf,运行完可以看到 sentinel 进程已经运行起来了。
在 5.1 案例中,6379 端口的服务是主服务,其他两个是从服务,所以现在我们直接让 6379 的服务下线(命令行按 Control + C 即可)。下线之后由于我们设置的主观下线超时时间是 1s,所以 Sentinel 马上就将 6379 的主节点标记为主观下线;又由于我们将客观下线的标准设置为 1 个主观下线,所以 Sentinel 直接认为这个服务已经客观下线了,所以立马开始故障转移,此时 6380 的从服务替换成为了主服务,其 INFO 如下:
# Replication role:master connected_slaves:1 slave0:ip=127.0.0.1,port=6381,state=online,offset=12652,lag=0 master_failover_state:no-failover master_replid:d22e208251adf0822fad27ece0fac56781f3eec7 master_replid2:b2502e9696ba8111121f2c361b64f97c1b9ec261 master_repl_offset:12652 second_repl_offset:11232 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:12652
对应的 Sentinel 进程输出信息如下:
可以看到 Sentinel 将 6380 的服务替换成了主节点。
此时我们继续,将原来的 6379 服务启动起来模拟服务故障恢复的场景,运行 redis-server 命令,运行完之后,Sentinel 进程输出如下:
第一行说明 6379 的服务重新上线了,此时它是一个从服务;第二行将它转换成了从服务并且同步 6380 的数据。
此时查看 6379 端口服务的 INFO 信息:
# Replication role:slave master_host:127.0.0.1 master_port:6380 master_link_status:up master_last_io_seconds_ago:1 master_sync_in_progress:0 slave_read_repl_offset:51106 slave_repl_offset:51106 slave_priority:100 slave_read_only:1 replica_announced:1 connected_slaves:0 master_failover_state:no-failover master_replid:d22e208251adf0822fad27ece0fac56781f3eec7 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:51106 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:46054 repl_backlog_histlen:5053
确认了它是一个从节点,其主节点为 6380 的服务。
5.3 RDB
在客户端执行 BGSAVE 命令,服务端输出:
第一行说明了负责生成 RDB 文件的子进程 pid,第二行表示同步成功,第三行应该是表示整个过程中 CoW 复制的数据量大小。
同步出来的 RDB 文件内容:
因为是压缩后的二进制编码数据,所以打开之后会有乱码,但是还是可以隐约看到存储数据的内容。
5.4 AOF
在命令行中启动 redis- server 时,带上 --appendonly yes,然后在客户端执行两条命令:
此时查看 aof 文件内容:
可以发现除了第一条 SELECT 是系统自动执行的之外(选择 Redis 数据库),其他两条都是我执行之后记录到 aof 文件中的。
证实了 AOF 文件中存储的是执行的写命令,再次重启 redis-server,可以发现数据还在 Redis 中,没有因为退出而发生数据丢失。
The text was updated successfully, but these errors were encountered: