一、Redis支持五中数据类型:String、哈希(字典)、list(列表)、set(集合)以及zset(sorted set:有序集合)。
1、Redis的String的底层是实现是SDS(simple dynamic string)简单动态字符串。
struct sdshdr {
//记录buf数组中已经使用字节的数量,等于SDS所保存的字符串的长度。
int len;
//记录buf数组中未使用的字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
获取SDS长度的复杂度仅为O(1).而C中获取字符串长度的时间复杂度为O(n)。
SDS的空间分配策略杜绝了缓冲区溢出的可能性。
减少了修改字符串时带来的内存重分配次数。内存重分配设计复杂算法,并且肯需要执行系统调用,通常是一个比较耗时的操作。
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
空间预分配:用于SDS的字符串增长操作。
惰性空间释放:用于优化SDS的字符串缩短操作。
二进制安全的SDS。SDS API 都会以处理二进制的方式来处理SDS存放在buf数组里的数据。不会对其中的数据做任何的限制、过滤、或者假设。
SDS兼容部分C字符串函数。
2、Redis的List底层是实现是链表,说到链表不得不提的就是数组。那么链表和数组两种数据结构有什么不同呢?
数组是顺序存储的,在内存中的存储方式是连续的;链表是链式存储,在内存中是非连续性存储
访问数组中的数据的时间复杂度是O(1),而链表必须从表头顺序去访问,时间复杂度是O(n)
链表上任意节点删除效率比数组高
Redis的List类型底层是通过list和listNode两个结构体实现,代码见下图:
listNode是一个双向链表的节点,多个listNode通过prev和next指针组成双端链表。
使用list结构体来持有链表。list结构为链表提供了表头指针head、表尾指针tail,以及链表节点数量len,dup、free、match成员则是为了实现链表功能而提供的接口。
dup函数用于复制链表节点所保存的值
free函数用于释放链表节点所保存的值
match函数用于对比链表节点所保存的值和另一个输入值是否相等由list结构和listNode组成的链表如下图所示,这就是Redis中list类型的实现。
链表表头节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
3、字典的实现:
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算哈希值。
这种算法的优点是:即使输入的键是有规律的,算法仍能给出一个很好上午随机分布性,并且算法的计算速度也很快。
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。
在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新的哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的。
4、跳跃表
跳跃表是一种有序数据结构,他通过在每个节点中维持多个指向其他几点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单。
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
Redis使用跳跃表作为有序集合的底层实现之一。如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表作为有序集合键的底层实现。
Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。
5、整合集合
整数集合是集合键的底层实现之一,当一个集合只包含整数元素,并且集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
升级操作为整数集合带来了操作上的灵活,并且尽可能的节约了内存。、
整数集合只支持升级操作,不支持降级操作。
6、压缩列表
压缩列表是列表建和哈希键的底层实现之一。
当一个列表建只包含少量列表项,并且每个列表项要么是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现。
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含人员多个节点,每个节点保存一个字节数组或者一个整数值。
二、Redis数据库
1、Redis服务器的所有数据都保存在redisServer.db数组中,数据库的数量由redisServer.dbnum属性保存。
2、客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换数据库。
3、数据库主要由dict和expire两个字典构成,其中dict字典负责保存键值对,expire负责保存键的过期时间。因为数据库由字典构成,所有对数据库的操作都是建立在字典操作之上。
4、数据库的键总是一个字符串对象,而值则可以是任意一种Redis对象类型。String、哈希(字典)、list(列表)、set(集合)以及zset(sorted set:有序集合)。
5、expire字典的键指向数据库中的某个键,值记录了数据库键的过期时间,过期时间是一个以毫秒为单位的Unix时间戳。
6、Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
7、执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已经过期的键。
8、执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。
9、当一个过期键被删除后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。
10、从服务器即使发现过期键也不会自作主张删除它,而是等待主节点发来DEL命令,这种统一中心化的的过期键删除策略可以保证主从服务器数据的一致性。
11、当Redis命令对数据库进行修改后,服务器会根据配置向客户端发送数据库通知。
三、Redis的两种持久化方式:RDB持久化和AOF持久化
Redis是内存数据库,它将自己的数据库存在内存中,一但服务器进程退出,服务器中的数据都会丢失。
RDB持久化功能:将内存中的数据库状态保存到磁盘。
RDB持久化既可以手动执行也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。RDB文件是一个经过压缩的二进制文件。RDB文件保存在硬盘。根据RDB文件可还原数据库状态。
生成RDB文件的两条命令:SAVE和BGSAVE.
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。客户端发送的所有命令请求都会被阻塞。
BGSAVE:命令由子进程执行,子进程在创建RDB文件的过程中,Redis服务器仍然可以继续处理客户端的命令请求,但是在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时有所不同。
首先,服务器禁止SAVE命令和BGSAVE命令同时执行是为了父进程和子进程同时执行两个rdbSave调用,防止产生竞争条件。
其次,BGSAVE命令执行期间,客户端发送的BGSAVE命令也会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。
最后,BGREWRITEAOF和BGSAVE两个命令不能同时执行:
1、如果BGSAVE命令正在执行,客户端发送的BGREWRITEAOF会被延迟到BGSAVE命令执行完毕后执行。
2、如果BGREWRITEAOF命令正在执行,你们客户端发送的BGSAVE命令会被服务器拒绝。
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
RDB文件用于保存和还原Redis服务器所有数据库中的键值对数据。RDB文件是一个压缩的二进制文件。对于不同类型的键值对,RDB文件会使用不同的方式来保存他们。
RDB文件结构:
REDIS | db_version | databases | EOF | check_sum |
RDB文件开头是REDIS部分:长度五个字节,保存着"REDIS"五个字符,以此判断载入的文件是否RDB文件。
db_version占四个字节,它的值是一个字符串表示的整数,记录的是文件的版本号。比如"0006"就代表RDB文件的版本为第六版。
databases部分包含零个或任意多个数据库,以及数据库中的键值对数据。如果服务器的数据库状态为空。那么这个部分也为空。
EOF常量的长度为1字节,标志着RDB文件正文内容结束。
check_sum是一个8字节的无符号整数,保存着一个校验和。这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
AOF持久化功能实现
AOF持久化功能分三个步骤:命令追加(append)、文件写入、文件同步(sync)。
命令追加:AOF功能打开时服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
1、AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
2、AOF文件的所有命令都以Redis命令请求协议的格式保存。命令请求会先保存到AOF缓冲区里面,之后定期写入AOF文件。
3、appendfsync选项的不同值对AOF持久化功能的安全性以及Redis服务器的性能有很大影响。
4、服务器只要载入并重新执行保存在AOF中的命令,就可以还原数据库本来的状态。
5、AOF重写可以产生一个新的AOF文件,这个新的AOF 文件和原有的AOF文件所保存的数据库状态一样,单体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读写、分析和写入操作。
6、在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,是的新旧两个AOF文件保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,一次来完成AOF文件重写操作。
四、Redis事件
Redis服务器是一个事件驱动程序,服务器需要处理两类事件:文件事件和时间事件。
文件事件:服务器对套接字操作的抽象。
Redis基于Reactor模式开发了自己的网络事件处理器。称为文件事件处理器。
文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
时间事件:Redis服务器中的一些操作,需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
Redis时间事件分为:定时事件和周期性事件。
定时事件:让一段程序在指定时间之后执行一次。(一次)
周期性事件:让一段程序每隔一段时间就执行一次。(多次)
一个时间事件由3个属性组成:
id:(服务器为时间事件创建全局唯一ID)ID从小到大递增。
when:毫秒精度的Unix时间戳,记录事件的到达时间。
timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。
文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理实践的过程中不会进行抢占。
时间事件的实际处理时间通常比设定的到达时间晚一些。
五、客户端:
Redis是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端的请求,并向客户端返回命令回复。
通过使用由I/O多路复用计数实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络 通信。
六、Sentinel
Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:
- 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。
节点之间使用 Gossip 协议 来进行以下工作:
- 传播(propagate)关于集群的信息,以此来发现新的节点。
- 向其他节点发送 PING 数据包,以此来检查目标节点是否正常运作。
- 在特定事件发生时,发送集群信息。
- 主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。
- 客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)
如果一个服务器没有在 master-down-after-milliseconds 选项所指定的时间内, 对向它发送 PING 命令的 Sentinel 返回一个有效回复(valid reply), 那么 Sentinel 就会将这个服务器标记为主观下线。
在默认情况下, Sentinel 使用 TCP 端口 26379 (普通 Redis 服务器使用的是 6379 )。
- 每个 Sentinel 会以每两秒一次的频率, 通过发布与订阅功能, 向被它监视的所有主服务器和从服务器的 __sentinel__:hello 频道发送一条信息, 信息中包含了 Sentinel 的 IP 地址、端口号和运行 ID (runid)。
- 每个 Sentinel 都订阅了被它监视的所有主服务器和从服务器的 __sentinel__:hello 频道, 查找之前未出现过的 sentinel (looking for unknown sentinels)。 当一个 Sentinel 发现一个新的 Sentinel 时, 它会将新的 Sentinel 添加到一个列表中, 这个列表保存了 Sentinel 已知的, 监视同一个主服务器的所有其他 Sentinel 。
- Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。
- 在将一个新 Sentinel 添加到监视主服务器的列表上面之前, Sentinel 会先检查列表中是否已经包含了和要添加的 Sentinel 拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的 Sentinel , 如果是的话, Sentinel 会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的 Sentinel , 然后再添加新 Sentinel 。
有两种方式可以和 Sentinel 进行通讯:
- 第一种方法是通过直接发送命令来查询被监视 Redis 服务器的当前状态, 以及 Sentinel 所知道的关于其他 Sentinel 的信息, 诸如此类。
- 另一种方法是使用发布与订阅功能, 通过接收 Sentinel 发送的通知: 当执行故障转移操作, 或者某个被监视的服务器被判断为主观下线或者客观下线时, Sentinel 就会发送相应的信息。
一次故障转移操作由以下步骤组成:
- 发现主服务器已经进入客观下线状态。
- 对我们的当前纪元进行自增(详情请参考 Raft leader election ), 并尝试在这个纪元中当选。
- 如果当选失败, 那么在设定的故障迁移超时时间的两倍之后, 重新尝试当选。 如果当选成功, 那么执行以下步骤。
- 选出一个从服务器,并将它升级为主服务器。
- 向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为主服务器。
- 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。
- 向已下线主服务器的从服务器发送 SLAVEOF 命令, 让它们去复制新的主服务器。
- 当所有从服务器都已经开始复制新的主服务器时, 领头 Sentinel 终止这次故障迁移操作。
Sentinel 使用以下规则来选择新的主服务器:
- 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
- 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
- 在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器。
Sentinel 状态的持久化
Sentinel 的状态会被持久化在 Sentinel 配置文件里面。
每当 Sentinel 接收到一个新的配置, 或者当领头 Sentinel 为主服务器创建一个新的配置时, 这个配置会与配置纪元一起被保存到磁盘里面
Redis 集群规范:Redis 集群规范 — Redis 命令参考
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
Redis 集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现: 一个 Redis 集群包含 16384 个哈希槽(hash slot), 数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
集群中的每个节点负责处理一部分哈希槽。 举个例子, 一个集群可以有三个哈希槽, 其中:
- 节点 A 负责处理 0 号至 5500 号哈希槽。
- 节点 B 负责处理 5501 号至 11000 号哈希槽。
- 节点 C 负责处理 11001 号至 16384 号哈希槽。
因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞, 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线。
键分布模型
Redis 集群的键空间被分割为 16384 个槽(slot), 集群的最大节点数量也是 16384 个。
推荐的最大节点数量为 1000 个左右。
每个主节点都负责处理 16384 个哈希槽的其中一部分。
当我们说一个集群处于“稳定”(stable)状态时, 指的是集群没有在执行重配置(reconfiguration)操作, 每个哈希槽都只由一个节点进行处理。
重配置指的是将某个/某些槽从一个节点移动到另一个节点。
一个主节点可以有任意多个从节点, 这些从节点用于在主节点发生网络断线或者节点失效时, 对主节点进行替换。