redis设计与实现 笔记

这里写目录标题

redis集群原理

客户端发送key的命令请求,先计算key数据哪个槽,再获得处理该槽的节点,命令最终由该节点执行并返回执行结果。

命令

使用OBJECT ENCODING命令可以查看一个数据库键的值对象编码。例:object encoding a  查看a的对象编码。

在这里插入图片描述

type命令查看数据库键对于的值对象类型。例:type a  查看a的值类型。

在这里插入图片描述
可通过OBJECT REFCOUNT 命令查看键A的值对象的引用计数。 OBJECT REFCOUNT A。
OBJECT IDLETIME命令查看给定键的空转值。
select 切换数据库
flushdb 刷新数据库
dbsize 查看数据库元素个数
info 查看redis详细信息,后面可接查询出的域名查看某个域信息 如:info server 查看redis服务器详细信息
TTL PTTL 查看键剩余存活时间
PERSIST 移除键的过期时间。
SLAVEOF host port 成为指定主机的从服务器,例:SLAVEOF 127.0.0.1 6379 成为这台rdis服务器的从服务器。
CLUSTER KEYSLOT 查看该key属于集群哪个槽。
SUBSCRIBE 订阅频道 subscribe “news.it” 订阅news.it频道
PSUBSCRIBE 订阅模式 psubscribe “news.*” 订阅所有符合 news. 开头的频道, psubscribe “news.[ie]t” 订阅 news.it和news.et频道
PUBLISH 向频道发布消息。 publish "news.it " “java” 向news.it 频道发送 消息 java

字符串

redis的字符串使用SDS(redis自己实现的字符串)实现, C字符串strcat(拼接字符串)前不会检测字符串长度是否足够而发生溢出问题。SDS在执行sdscat(字符串拼接)前会检测空间是否足够而实现分配好空间。 C字符串计算长度需遍历字节长度,SDS可以直接访问len属性获取长度。

在这里插入图片描述
SDS用free属性记录当前字符串数组未使用的字节数。
修改SDS时,当需要扩容时会增加SDS修改后的字节数组大小(原长度1,改成成5,扩容成12,(1+5)*2),不需扩容则不需要;当需要缩容时,不会直接释放空间,而是留空,以应对后续的修改扩容操作,等下次再调用时再进行空间释放。
C字符串以识别到空字符 '/0’就判断此字符串结束,故不能使用此字符串保留二进制文件。SDS以处理二进制的方式处理存放在buf数组的数据,可以存储文件。SDS字符串也以空字符 ‘/0’ 结尾,这是为了可以重用string.h的一部分函数,通过对空字符的转义实现共用。

在这里插入图片描述
在这里插入图片描述

链表

双链:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1).。
无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
带表头长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。 多态:链表节点使用void* 指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点只设置类型特定函数,所以链表可以用于保存各种不同类型的值。

在这里插入图片描述

head:表头节点 	
tail:表尾节点 	
len:链表所包含的节点数量 	
dum函数:用于复制链表节点所报保存的值。 	
free函数:用于释放链表节点所保存的值。 	
match函数:用于对比链表节点所保存的值和另一个输入值是否相等。

hash

hash组成:两个数组,一个数组指向一个链表数组,链表数组存储元素。另一个数组扩容使用。
hash计算负载因子方式:used(元素个数) / size(链表数组长度),大于1则扩容,小于0.1则缩容。hash维护了两个链表数组,一个使用,另一个rehash(扩容)使用,扩容完舍弃第一个数组,第二个提到第一个,再创建一个第二个数组。扩容为第一个数组已使用长度两倍的第一个2的n次方(例:长度是5,则扩容后长度为5*2,离他最近的2的n次发是16,故扩容后长度是16),缩容操作为第一个大于大于已使用长度的2的n次方(例:已使用长度为3.则缩容后长度为4)。
hash的负载因子计算方式为:元素数 / 链表数组长度,也就是hash的链表数组下标一个元素,多一个直接扩容。
当以下条件的任意一个被满足时,程序会自动开始对哈希表执行扩容操作:
	1)、服务器目前没有在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
	2)、服务器目前正在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
hash在扩容时,采用写时复制(copy and write)来优化子进程的使用效率,因此在BGSAVE期间,尽可能避免进行hash扩容操作。
扩容不是一次完成,而是分多次、渐进式的rehash。

跳跃表

一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,redis就会使用跳跃表来作为有序集合键的底层实现。
redis只在两个地方用到了跳跃表:实现有序集合键,在集群节点中用作内部数据结构。

在这里插入图片描述
最左边的zskiplist结构:
header:指向跳跃表的表头节点。
tail:指向跳跃表的表尾节点。
level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点不计算在内)
length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)
右边是4个zskiplistNode:
层(Level):节点中用L1、L2、L3等字样标记节点的各个层。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾反向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。
后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的后退指针。后退指针在程序从表尾向表头遍历时使用。
分值(score):节点中的1.0、2.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
成员对象(object):各个节点中o1、o2是节点所保留的对象成员。

整数集合

redis的整数集合编码方式根据传入元素决定,每当要插入新元素并且新元素的类型比现有元素都长时,整数集合要先进行升级,然后才能添加。
升级整数集合并添加新元素共分为三步进行:
	1)、根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
	2)、将底层数组现有的所以元素都转换成与新元素相同的元素,并将类型转换后的元素放置到正确的位置,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
	3)、将新元素添加到底层数组里面。
整数集合不支持降级操作,一旦进行了升级操作,编码就会一直保存升级后的这条。

压缩列表

	压缩列表(ziplist)是列表键和哈希键底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
	压缩列表是redis为了节约内存而开发的。是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组(字符串)或者一个整数值。
	每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成。
	previous_entry_length属性以字节为单位,记录压缩列表中前一个节点的长度(可以通过这个属性来得到前一个节点)。previous_entry_length属性的长度可以是1字节或者5字节。
	节点的encoding属性记录了节点的content属性所保存数据类型以及长度。一字节、两字节或者五字节长,值的最高位为00、01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组长度由编码去除最高两位之后的其他为记录。

在这里插入图片描述
一字节长,值得最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码去除最高两位之后的其他位记录:
在这里插入图片描述
连锁更新概念:当压缩列表中所有节点的previous_entry_length都是定好的长度的值,而表头插入了一个过长的节点时,表头下一节点的previous_entry_length无法保存该长度,则表头下一节点的previous_entry_length需要扩容,下一节点扩容,下下一节点也保存不下,也需要扩容,直到最后一个节点。redis在这种特殊情况下的连续多次扩展空间操作称为连锁更新。除了添加会触发连锁更新,删除也会。
总结:
a)、压缩列表是一种为节约内存而开发的顺序型数据结构。
b)、压缩列表被用作列表键和哈希键的底层实现之一。
c)、压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
d)、添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的机率不高。

对象

字符串对象

redis基于这些数据结构创建了一个对象系统,这个系统包含了字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每周对象都用到了至少一种我们前面所介绍的数据结构。redis的对象系统还实现了基于***引用技术技术***的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被释放;另外,redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启动了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
redis中的每一个对象都由一个redisobject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性。

在这里插入图片描述
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
字符串对象,int保留整数,raw(使用SDS)保存超过32字节的字符串。embstr保存32字节以下的字符串。
embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redis Object结构和SDSHDR结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。
浮点数在redis中用字符串保存。int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
redis没有为embstr编码的字符串对象编写任何相应的修改程序,所有embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象指向任何修改命令时,程序会先将对象的编码由embstr转换为raw,然后执行修改命令。因为这个原因,embstr编码的字符串在执行修改命令之后,总会变成一个raw编码的字符串对象。

列表对象

列表对象的编码可以是ziplist或者linkedlist(3.2版本使用quicklist,由ziplist组成的双向链表)。
ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩节点保存了一个列表元素。
lonkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
当列表对象可以同时保存以下条件,列表对象使用ziplist:
	a)、列表对象保存的所有字符串元素的长度都小于64字节
	b)、列表对象保存的元素数量小于512个。
不能满足的列表对象需使用linkedlist。

哈希对象

哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当由新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值得压缩列表节点推入到压缩列表表尾。因此:
	a)、保存了同一键值对得两个节点总是紧挨在一起,保存键的节点在前,保存值得节点在后。
	b)、先添加到哈希对象中的键值会被放在压缩列表得表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表得表尾方向。

在这里插入图片描述
hashtable编码得哈希对象使用字典作为底层实现,哈希对象这每个键值对都使用一个字典键值来保存:
a)、字典的每个键都是一个字符串对象,对象中保存了键值对的键。
b)、字典的每个值都是一个字符串对象,对象中保存了键值对的值。
在这里插入图片描述

当哈希对象可以同时满足以下两个条件时,使用ziplist编码:
	a)、哈希对象保存的所有键值对的键和值的长度都小于64字节。
	b)、哈希对象保存的键值对数量小于512.
	否则哈希对象需使用hashtable编码。

集合对象

集合对象的编码可以是intset或者hashtable。
inset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。

在这里插入图片描述

hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL(类似java的hashset实现通过hashmap的键唯一性)。

在这里插入图片描述

当集合对象可以同时满足以下两个条件时,可以使用intset编码:
	a)、集合对象保存的所有元素都是整数值。
	b)、集合对象保存的元素数量不超过512个。

有序集合

有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。

在这里插入图片描述
skiplist编码的有序集合对象使用zset对象结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
在这里插入图片描述
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表哦节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性。
有序集合的每个元素都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
有序集合使用跳跃表和字典两种数据结构是为了查找元素为O(1)复杂度,还可以执行范围型操作。
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
a)、有序集合保存的元素数量小于128个。
b)、有序集合保存的所有元素成员的长度都小于64字节。
不能满足的将使用skiplist编码。

类型检查

类型特性命令所进行的类型检查是通过rediaObject结构的type属性来实现的:
	在执行一个类型特性命令之前,服务器会先检查输入数据库键的值对象是否为执行命令所需的命令,如果是的话,服务器就对键执行指定的命令。
	否则,服务器将拒绝执行命令,并向客户端返回一个类型错误。

内存回收

redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
每个对象的引用计数信息由redisObject结构的refcount属性记录

在这里插入图片描述

对象的引用计数信息会随着对象的使用状态而不断变化:
	a)、在创建一个新对象时,引用计数的值会被初始化为1.
	b)、当对象被一个新程序使用时,它的引用计数值会被增一。
	c)、当对象不再被一个程序使用时,它的引用计数值就会减一。
	d)、当对象的引用计数变为0时,对象所占用的内存就会被释放。
对象的整个生命周期可以划分为创建、操作、释放对象三个阶段。

对象共享

在redis中,让多个键共享一个值对象需要执行以下两个步骤:
	a)、将数据库键的值指针指向一个现有的值对象。
	b)、将被共享的值对象的引用计数增一。
目前来说,redis会在初始化服务器时,创建0~9999一万个字符串对象,当服务器需要使用到时,直接使用这些共享对象。
可通过OBJECT REFCOUNT 命令查看键A的值对象的引用计数。  OBJECT REFCOUNT A。
redis不共享字符串对象:验证对象是否相同消耗的CPU不稳定。

对象空转时长

redisObject包含lru属性记录了对象最后一次被命令程序访问的时间。
可通过OBJECT IDLETIME命令查看给定键的空转值。这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得出的。

数据库

redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDB结构代表一个数据库。
在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库。
dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,所以redis服务器默认会创建16个数据库。

在这里插入图片描述

默认情况下,redis客户端的目标数据库为0号数据库,但客户端可以通过select命令来切换目标数据库。
在服务器内部,客户端状态redisClient结构的db属性记录了客户端的目标数据库,这个属性是一个指向redisDb结构的指针。

在这里插入图片描述

数据库键空间

redis的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个键值对称为键空间(key space):

在这里插入图片描述

	键空间和用户所见的数据库是直接对应的:
		键空间的键也就是数据库的键,每个键都是一个字符串对象。
		键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种redis对象。
	命中率:通过info命令的keyspace_hits和keyspace_misses属性相除得到命中率(hits / misses)。
读写键空间时的维护操作:
	a)、在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的命中(hit)次数或空间不命中(miss)次数。命中率:通过info命令的keyspace_hits和keyspace_misses属性相除得到命中率(hits / misses)。
	b)、在读取一个键之后,服务会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间
	c)、如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才指向余下的其他操作。
	d)、如果由客户端使用WATCH命令监视了某个键,那么在服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
	e)、服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化和复制操作。
	f)、如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发生相应的数据库通知。

设置键的生存时间或过期时间

通过命令EXPIRE 或者 PEXPIRE命令,设置以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定时间后,服务器就会自动删除生存时间为0的键:EXPIRE KEY 5   PEXPIRE KEY 100。
SETEX命令可以设置一个字符串键的同时为键设置过期时间,因为这个命令是一个类型限定的命令(只能用于字符串键)。
通过EXPIREAT PEXPIREAT命令以秒或者毫秒精度给数据库的某个键设置过期时间。
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个为过期字典。

在这里插入图片描述
键空间的键和过期字典的键都指向同一个键对象,所以不会出现任何重复对象,也不会浪费任何空间。
PERSIST命令可以移除一个键的过期时间。
TTL命令以秒为单位返回键的剩余生存时间,PTTL命令以毫秒为单位返回键的剩余生存时间。
过期键的判定:
a)、检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
b)、检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。

过期键删除策略

定时删除:创建定时器,在键的过期时间来临时,立即执行键的删除操作。
惰性删除:每次获取键是,检查是否过期,过期删除,不过期返回该键。
定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。

定时删除对内存最友好,对CPU不友好。
惰性删除:对CPU友好,对内存不友好。
定期删除:前两种策略的折中。每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。

redis的过期键删除策略

实际使用惰性删除和定期删除。
惰性删除由db.c/expirekfNeeded函数实现,所以读写数据库的redis命令在执行之前都会调用expireifNeeded函数对输入键进行检查。
定期删除策略由redis.c/activeExpireCycle函数实现,每当redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用。它在规定时间内,分多次遍历服务器中的各个数据库,从数据库的expire字典随机检查一部分键的过期时间,并删除其中的过期键。

AOF、RDB和复制功能对过期键的处理

在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
载入RDB文件时,如果服务器以主服务器模式运行,载入RDB文件时,程序会对文件中保存的键进行检查,过期键会被忽略。如果以从服务器运行,载入时,不论是否过期,全部载入数据库。不过,因为主从服务器在进行数据同步时,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没被惰性删除或者定期删除,那么AOF问价不会因为这个过期键收到任何影响。
当过期键被删除时,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。
AOF重写过程,程序会对数据库中的键进行检查,已过期的键不会被保存到重写地AOF文件中。
当服务器在复制模式下运行时,从服务器的过期删除由主服务器控制:
	a)、主服务器在删除一个过期键之后,会显示地向所有从服务器发生一个DEL命令,告知从服务器删除这个过期键
	b)、从服务站执行客户端发送地读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期地键一样来处理过期键。
	c)、从服务器只有在接到主服务器发来的del命令之后,才会删除鼓起键。
通过主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据地一致性。

数据库通知

键空间通知:监控某个键,如键被读或者改触发。
键事件通知:监控某个事件,如监控get时间,当某个键被get,则触发。

重点回顾

a)、redis服务器地所有数据库都保存在redisServer.db中,而数据库地数量则由redisServer.dbnum属性保存。
b)、客户端通过修改目标数据库指针,让它执行redisServer.db数组中的不同元素来切换不同地数据库。
c)、数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。
d)、因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
e)、数据库的键总是一个字符串对象,而值则可以是任意一种redis对象类型。
f)、expires字典的键指向了数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间戳。
g)、redis使用惰性删除和定期删除两种策略来删除过期键。
h)、执行SAVE或者BGSAVE命令所产生的新RDB问价不会包含已过期的键。
i)、执行BGREWRITEAOF命令所产生的重新AOF文件不会包含已经过期的键。
j)、当一个过期键被删除之后,服务器会追加一条DEL命令到现有的AOF文件的末尾,显式的删除过期键。
k)、当主服务器删除一个过期键之后,它会向所以从服务器发送一条DEL命令,显式的删除过期键。
l)、从服务器即使发现过期键也不会删除,而是等待主服务器发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
m)、当redis命令对数据库进行修改之后,服务器会根据配置项客户端发送数据库通知。

RDB持久化

将redis在内存中的数据库状态保存到磁盘里面。即可手动执行,也可根据服务器配置选项定期执行。
两个可用于生成RDB文件的命令:SAVE和BGSAVE。
	a)、SAVE命令会阻塞redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
	b)、BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令。
RDB文件的载入工作是在服务器启动时自动执行的,所以redis并没有专门用于载入RDB文件的命令,只要redis服务器在启动时检测到RDB文件存在,就会自动载入。
因为AOF的更新频率通常比RDB文件更新频率高,所以如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
在BGSAVE命令执行期间,客户端发送的SAVE和BGSAVE命令会被拒绝。BGREWRITEAOF不能和BGSAVE同时执行,客户端发送了会被延迟到BGSAVE执行完毕之后执行。如果BGREWRITEAOF正在执行,客户端发送的BGSAVE会被拒绝。因为BGSAVE和BGREWRITEAOF两个命令的实际工作都由子进程执行,那么这两个命令在操作方面并没有上面冲突的地方,不能同时执行只是一个性能方面的考虑——并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作。
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
RDB的自动执行BGSAVE命令:
	save 900 1900执行了1次写入则save
	save 300 10300执行了10次写入则save
	save 60 1000060执行了10000次写入则save
服务器程序会根据save选项所设置的保存条件设置服务器状态redisServer结构的saveparams属性:

在这里插入图片描述
saveparams属性是一个数组,数组中的每个元素都是一个saveparam结构,每个saveparam结构都保存了一个save选项设置的保存条件。
在这里插入图片描述
除了saveparams数组之外,无法去状态还维持着一个dirty计数器以及一个lastsave属性:
dirty记录数记录距离上一次成功执行save或者BGSAVE命令之后,服务器对数据库状态(所以数据库)进行了多少次修改(包括写入、删除、更新等操作)。
lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE或者BGSAVE命令的时间。
在这里插入图片描述
当服务器成功执行了一个数据库修改命令之后,程序就会对dirty计数器进行更新:命令修改了多少次,dirty计数器的值就增加多少。
redis的服务周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的一项工作就是检查save选项所设置的保存条件是否已满足,如果满足就执行BGSAVE命令。

RDB文件结构

RDB文件结构
最开头的REDIS部分,长度为5字节,保存REDIS五个字符。通过这五个字符,程序在载入文件时,快速检查所载入的文件是否是RDB文件。
db_version长度为4字节,值是一个字符串表示的整数,记录RDB文件的版本号。
databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据,如果数据库状态为空,这个值也为空。
EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,
check_num是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对前4个部分的内容计算得出。服务器在载入RDB文件时,会将载入数据所计算出的校验和于check_num所记录的校验和进行对比,一次来检查RDB文件是否有出错或者损坏的情况出现。
带有两个非空数据库的RDB文件示意

RDB文件着数据库结构

在这里插入图片描述
SELECTDB常量字节的长度为1字节,当读入程序遇到这个值时,它知道接下来要读入的将是一个数据库号码。
db_number保存着一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字节、2字节或者5字节。当程序读到db_number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库交换,使得之后读入的键值对可以载入到正确的数据库中。
key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件的不同,key_value_pairs部分的长度也会有所不同。
RDB文件中的数据库结构示例
key_value_pairs部分保存了一个或以上数量的键值对,如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内。
不带过期时间的键值对在RDB文件中由TYPE、key、value三部分组成。
不带过期时间的键值对
TYPE代表一种对象类型或者底层编码。
带有过期时间的键值对
EXPIRETIME_MS常量的长度为1字节,它告知读入程序,接下来要读入的将是一个以毫秒为单位的过期时间。
ms是一个8字节长的带符号整数,记录着一个以毫秒为单位的UNIX时间戳这个时间戳就是键值对过期的时间。

字符串对象

TYPE值为DIS_RDB_TYPE_STRING,就是保存的字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT或者REDIS_ENCODING_RAW。
字符串对象编码为REDIS_ENCODING_INT说明对象中保存的是长度不超过32位的整数,这种编码的对象将以下图结构保存

INT编码字符串对象的保存结构
其中ENCODING的值可以是REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16或者REDIS_RDB_ENC_INT32三个常量中的一个。,分别代表RDB文件使用8位、16位或者32位来保存整数值intefer。
如果字符串编码位REDIS_ENCODING_RAW,那么说明对象所保存的是一个字符串值,根据字符串长度的不同,由压缩和不压缩两种方法来保存这个字符串:
如果字符串长度小于等于20,原样保存。大于20,被压缩后保存。(此条件是在服务器打开了RDB文件压缩功能的情况下进行)
无压缩字符串的保存结构
压缩后字符串的保存结构
REDIS_RDB_ENC_LZF表示字符串已被LZF算法(http://liblzf.plan9.de)压缩,读入程序碰到这个常量时,会根据之后的三个部分,对字符串进行解压缩,compressed_len记录字符串被压缩后长度,origin_len记录字符串原有长度,compressed_string记录被压缩后的字符串。

列表对象

TYPE值为REDIS_RDB_TYPE_LIST,那么value保存的就是一个REDNS_ENCODING_LINKEDLIST编码的列表对象,RDB保存这种对象的结构如下图:

LINKEDLIST编码列表对象的保存结构
list_length记录了列表的长度,读入程序可以通过这个长度知道自己应该读入多少个列表项。下图展示一个包含三个元素的列表:
在这里插入图片描述
第一个3是列表长度,之后跟着第一个列表、第二个列表、第三个列表,分别记录长度和内容。

集合对象

TYPE值为REDIS_RDB_TYPE_SET,value保存的就是一个REDIS_ENCODING_HT编码的集合对象,结构如下图:

HT编码集合对象的保存结构
set_size是集合对象的大小,记录集合保存了多少个元素,读入程序可以知道应该读入多少个元素。
elem开头的代表集合的元素:
保存HT编码集合的例子
第一个4为集合大小,后面为集合元素的长度和内容。

哈希表对象

TYPE值为REDIS_RDB_TYPE_HASH,value保存REDIS_CODING_HT编码的集合对象,结构如下图所示:

HTb编码哈希表对象的保存结构
hash_size记录了哈希表的大小,即这个哈希表保存了多少键值对,读入程序可以知道应该读入多少个键值对。
key_value_pair开头的代表哈希表中的键值对
键值对的保存情况
保存HT编码哈希表的例子
第一个数字2为哈希表的长度,第一个键值对的键为长度为1的"a",值是长度为5的"apple"。

有序集合对象

TYPE值为REDIS_RDB_TYPE_ZSET,value保存一个REDIS_ENCODING_SKIPLIST编码的有序集合对象,结构如下图:

SKIPLIST编码有序集合对象的保存结构
sorted_set_size记录了有序集合的大小,即这个有序集合保存了多少元素,读入程序需根据这个值来决定读入都是有序集合元素。
以element开头的是元素,每个元素分为成员(member)和分值(score)两部分,程序在保存RDB文件时会先将分值转换成字符串对象,然后再用保存字符串对象的方法将分值保存起来。
保存SKIPLIST编码有序集合的例子
第一个数字2记录有序集合的元素数量,之后紧跟两个元素。
第一个元素是长度为2的字符串"pi",分值是转换成字符串后长度为4的字符串 “3.14”。

INTSET编码的集合

TYPE位置REDIS_RDB_TYPE_SET_INTSET,value保存一个整数集合对象,RDB保留这种对象的方法是,先将整数集合转换为字符串对象,然后将这个字符串对象保存到RDB文件。

ZIPLIST编码的列表、哈希表或者有序集合

TYPE值为REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST或者REDIS_RDB_TYPE_ZSET_ZIPLIST,value保存的就是一个压缩列表对象,RDB保存这种对象的方式是:
	a)、将压缩列表转换成一个字符串。
	b)、将转换所得的字符串对象保存到RDB文件。如果程序在读入RDB文件的过程中,碰到由压缩列表对象转换成的字符串对象,那么程序会根据TYPE值的只是,进行以下操作:
		1)、读入字符串对象,并将他转换成原来的压缩列表对象。
		2)、根据TYPE的值,设置压缩列表对象的类型如果TYPE的值为REDIS_RDB_TYPE_LIST_ZIPLIST,那么压缩列表对象的类型为列表,如TYPE的值为REDIS_RDB_TYPE_HASH_ZIPLIST,那么压缩列表对象的类型为哈希表;如果TYPE的值为REDIS_RDB_TYPE_ZSET_ZIPLIST,那么压缩列表对象的类型为有序集合。
	RDB文件可以使用 od 命令读取,给定参数-c可以以ASCII编码的方式打印输入文件,给定-x参数可以以十六进制的方式打印输入文件

重点回顾

RDB文件用于保存和还原redis服务器所有数据库中的所有键值对数据。
SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
BGSAVE命令由子进程执行保存操作,所以该命令不会阻塞服务器。
服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。
RDB文件是一个经过压缩的二进制文件,由多个部分组成。
对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。

AOF持久化

AOF持久化通过保存redis服务器所执行的写命令来记录数据库状态。
被写入AOF文件的所有命令都是以redis的命令请求协议格式保存的。
服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓存器的末尾:

在这里插入图片描述
redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行向serverCorn函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓存器里面,所以在服务器每次结束一个事件循环之后,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓存器中的内容写入和保存到AOF文件里面。flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定:
在这里插入图片描述
appendfsync默认值是everysec。
为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓存区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正将缓存器中的数据写入到磁盘。这种做法虽然提高了效率,但是也为写入数据带来了安全问题,如果发生停机,那么存在在内存缓存器里面的写入数据将丢失。
reids读取AOF文件并还原数据库状态的详细步骤如下:
a)、创建一个不带网络连接的伪客户端。
b)、从AOF文件中分析并读取一条写命令。
c)、使用伪客户端执行被读出的命令。
d)、一直执行步骤b和c。直到AOF文件中的所以写命令都被处理完毕为止。
AOF重写功能:redis服务器创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含冗余命令。
redis将AOF重写程序放到子进程执行。为了解决AOF重写的数据不一致问题,redis服务器设置了一个AOF重写缓冲区,这个缓存区在服务器创建子进程之后开始使用,当redis服务执行完一个写命令之后,它会同时将这个写命令发送给AOF缓存区和AOF重写缓存器。
在子进程执行重写期间,服务器进程需要执行以下工作:执行客户端发来的命令、将执行后的写命令追加到AOF缓冲区、将执行后的写命令追加到AOF重写缓存区。
AOF重写完成后,会向父进程发送一个信号,父进程接收到该信号后,会调用一个信号处理函数,并执行:将AOF重写缓存器的所有内容写入到新AOF文件、对新AOF文件进行改名并原子的覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
在整个AOF后台重写过程中,之只有在信号处理函数执行时会对服务器进程(父进程)造成阻塞。

重点回顾

AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
AOF文件中的所有命令都以redis命令请求协议的格式保存。
命令请求会先保存到AOF缓存区里面,之后再定期写入并同步到AOF文件。
appendfsync选项的不同值对AOF持久化功能的安全性以及redis服务器的性能有很大的影响。
服务器只要载入并重新执行保存AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但是体积更小。
AOF重写是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。
在执行BGREWRITEAOF命令时,redis服务器会维护一个AOF重写缓存区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据状态一致。

事件

redis服务器是一个是一个事件驱动程序,主要处理两类事件:
	a)、文件事件:redis通过套接字和客户端(或者其他reedis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务端于客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
	b)、时间事件:redis服务器中的一些操作需要在给定的时间点执行,而时间时间就是服务器对这类定时操作的抽象。

文件事件

redis基于reactor模式开发了自己的网络事件处理器:这个处理起被称为文件事件处理器:
	a):文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
	b)、当被监听的套接字准备好执行连接应答(accept) 、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理起就会调用套接字之前关联好的事件处理器来处理这些事件。

文件事件处理器的四个组成部分
文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发的出现。
I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。

时间事件

时间事件分为两类:
	a)、定时事件
	b)、周期性事件
一个时间事件主要由以下三个属性组成:
	a)、id:服务器为时间事件创建的全局唯一ID。ID号按从小到大的顺序递增。
	b)、when:毫秒精度的UNIX时间戳,记录了时间事件的到达时间。
	c)、timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。
服务器将所有时间事件都放到一个无序链表中,每当时间事件执行器运行时,它将遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

重点回顾

reedis服务器是一个事件驱动程序,服务器处理的事件分为时间事件和文件事件两类。
文件事件处理器是基于reactor模式实现的网络通信程序。
文件事件是对套接字操作的抽象:每次套接字变为可应答(acceptable)、可写(writeable)、可读(readable)时,相应的文件事件就会产生。
文件事件分为AE)READABLE事件(读事件)和AE_WRITEABLE事件(写事件)两类。
时间时间分为定时事件和周期性事件:定时事件只在指定的事件到达一次,而周期性事件则每隔一段时间到达一次。
服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件。
文件事件和时间事件是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程也不会进行抢占。
时间事件的实际处理时间通常会比设定的到达时间晚一些。

客户端

redis服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,其中包括:
	a)、客户端套接字描述符。
	b)、客户端的名字。
	c)、客户端的标志值。
	d)、指向客户端正在使用的数据库的指针,以及该数据库的号码。
	e)、客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针。
	f)、客户端的输入缓冲区和输出缓冲区。
	g)、客户端的复制状态信息,以及进行复制所需的数据结构。
	h)、客户端执行BRPOP、BLPOP等列表阻塞命令时使用的数据结构。
	i)、客户端的事务状态,以及执行WATCH命令时用到的数据结构。
	j)、客户端执行发布于订阅功能使用到的数据结构。
	k)、客户端的身份验证标志。
	m)、客户端的创建时间,客户端和服务器最后一次通信的时间,以及客户端的输出缓冲区大小超过软性限制的时间
	redis服务器状态结构的clients属性是一个链表,这个链表保存了所有于服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历client链表来完成。
	客户端的fd属性记录了客户端正在使用的套接字描述符:根据客户端类型的不同,fd属性的值可以是-1或者时大于-1的整数:
		a)、伪客户端(fake client)的fd属性的值为-1:为客户端处理的命令请求来源于AOF文件或者lua脚本,而不是网络,所以这种客户端不需要记录套接字描述。目前redis服务器会在两个地方用到伪客户端,一个载入AOF文件,一个是执行lua脚本中包含的redis命令。
		b)普通客户端的fd属性的值为大于-1的整数:普通客户端使用套接字来与服务器进行通信,所以服务器会哟个fd属性来记录客户端套接字的描述符。
	执行CLIENT list命令可以列出目前所有连接到服务器的普通客户端。命令输出中的fd域显示了服务器连接客户端所使用的套接字描述符。
	默认情况下,连接到服务器的客户端是没有名字的,可通过client list的name域查看名字,可以使用client setname命令为客户端设置一个名字。
	客户端的标志属性flags记录了客户端的角色(role),以及客户端目前所处的状态。值可以是单个标志,也可以是多个标志的二进制或,每个标志使用一个常量表示,一部分标志记录了客户端的角色:
	a)、在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。REDIS_MASTER标志标识客户端代表的是一个主服务器,REDIS_SLAVE标志标识客户端代表的是一个从服务器。
	b)、REDIS_PRE_PSYNC标志表示客户端代表的是一个版本低于redis2.8的从服务器,主服务器不能使用PSYNC命令与这个从服务器进行同步。这个标志只能在REDIS_SLAVE标志处于打开状态是使用。
	c)、REDIS_LUA_CLIENT标识表示客户端是专门用于处理lua脚本里面包含的redis命令的伪客户端。
	另外一部分标志则记录客户端目前所处的状态:
	d)、REDIS_MONITER标志表示客户端正在执行MONITER命令。
	e)、REDIS_UNIX_SOCKET标志代表放眼望去使用UNIX套接字来连接客户端。
	f)、REDIS_BLOCKED标志表示客户端正在被BRPOP、BLPOP等命令阻塞。
客户端状态的输入缓冲区(querybuf属性)用于保存客户端发送的命令请求。输入缓冲区的大小会根据输入内容动态地缩小或扩大,最大不能超过1G,否则服务器会关闭这个客户端。
在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的argv和argc属性:
	argv属性是一个数组,数组中的每个项都是一个字符串对象,其中argv[0]是要执行的命令,而之后的其他选项则是传给命令的参数。
	argc属性则负责记录argv数组的长度。

argv和argc属性示例
当服务器从协议内容中分析并得出argv和argc属性的值之后,服务器会根据argv[0]的值,在命令表中查找命令所对应的命令实现函数。
执行命令所得到的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的:
a)、固定大小的缓存去用于保存那些长度比较小的回复。
b)、可变大小的缓冲区用于保存那些长度比较大的回复。
客户端的固定大小缓冲区由buf和bufpos属性组成。buf是一个大小为REDIS_REPLY_CHUNK_BYTES字节的字节数组,bufpos属性记录buf数组目前已使用的字节数量。REDIS_REPLY_CHUNK_BYTES默认值是16*1024,也就是说buf数组默认大小为16kb。
当buf数组的空间用完,或者回复太大没办法放进buf数组,服务器就会使用可变大小缓冲区。
可变大小缓冲区由reply链表和一个或多个字符串对象组成。
通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复。
客户端状态的authenticated属性用于记录客户端是否通过了身份验证。为0表示未通过,1为已通过。当该值为0时,客户端发送的所有其他命令都会被服务器拒绝。
客户端还有几个和时间有关的属性:
a)、ctime属性记录创建客户端的时间,可用来计算客户端与服务器已经连接了多少秒。
b)、lastinteraction属性记录了客户端与服务器最后一次进行互动的时间,互动即客户端向服务器发送命令请求或者服务器向客户端发送命令回复。用来计算客户端的空转(idle)时间,即距离客户端与服务器最后一次进行互动以来,已经过去了多少秒。
c)、ouf_soft_limit_reached_time属性记录了输出缓冲区第一次到达软限制的时间。

客户端创建与关闭

创建通过网络连接与服务器进行连接的普通客户端,客户端使用connect函数连接到服务器时,服务器会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个客户端状态添加到服务器状态结构client链表的末尾。

服务器状态结构的clients链表
一个普通客户端可以因为多个原因被关闭:
a)、客户端进程退出或者被杀死。
b)、客户端发送带有不符合格式的命令请求。
c)、客户处成为了client kill命令的目标。
d)、用户配置了timeout配置选项,当客户端空转时间超过了timeout。有例外情况。
e)、客户端发送的命令请求大小戳了输入缓冲区的限制大小(1GB)。
f)、发送给客户端的命令回复大小超过了输出缓冲区的限制大小。
为了避免客户端的回复过大,占用过多点的服务器资源,服务器会时刻检查客户端的输出缓冲区大小,并在缓冲区的大小超出范围时,执行相应的限制操作。
服务器使用两种模式来限制客户端输出缓冲区大小:
a)、硬性限制(hard limit):如果输出缓冲区的大小超过了硬性限制所设置大小,那么服务器立即关闭客户端。
b)、软性限制(soft limit):如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态结构的ouf_soft_limit_reached_time属性记录下客户端达到软性限制的起始时间;之后服务器会继续监视客户端,如果输出缓冲区的大小一致超过软性限制,并且持续时间超过了服务器设定的时长,那么服务器将关闭客户端;相反地,如果输出缓冲区地大小在指定时间内,不再超出软性限制,那么不会关闭并且ouf_soft_limit_reached_time会被清零。
使用client-output_buffer_limit选项可以为普通客户端、从服务器客户端、执行发布与订阅功能地客户端设置不同的软性限制和硬性限制,该选项的格式为:
client-output-buffer-limit
例:client-output-buffer-limit normal 0 0 0
client-output-buffer-limit salve 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
服务器会在初始化时创建负责执行lua脚本中包含的redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中。
lua_client伪客户端在服务器运行的整个生命期中会一直存在,只有在服务器被关闭时才被关闭。
服务器在载入AOF文件时,会创建用于执行AOF文件包含的redis命令的伪客户端。并在载入完成之后,关闭这个伪客户端。

重点回顾

服务器状态结构使用clients链表连接多个客户端‘状态,新添加的客户端状态会被放到链表的末尾。
客户端状态的flags属性使用不同的标志来表示客户端的角色,以及客户端当前所处的状态。
输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不能超过1GB.
命令的参数和参数个数会被记录在客户端状态的argv和argc属性里面,而cmd属性则记录了客户端要执行的实现函数。
客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用,其中固定大小缓冲区的最大大小为16KB,而可变缓冲区的最大大小不能超过服务器设置的硬性限制值。
输出缓冲区限制值有两种,如果输出缓冲区的大小超过了服务器设置的硬性限制,那么客户端会被立即关闭;除此之外,如果客户端在一定时间内,一直超过服务器设置的软性限制,那么客户端也会被关闭。
当一个客户端通过网络连接上服务器时,服务器会为这个客户端创建相应的客户端状态。
处理lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。
载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

服务器

命令请求的执行过程

redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
命令请求从发送到获得回复的过程:
	a)、客户端向服务器发送命令请求
	b)、服务器接收起区并处理客户端发来的命令请求,在数据库中进行设置操作,并产生命令回复OK。
	c)、服务器将命令回复OK发送给客户端。
	d)、客户端接收服务器返回的命令,并将这个回复打印。
当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。
当客户端与服务器之间的连接套接字因为客户端的写入而变的可读时,服务器将调用命令请求处理器来执行:
	a)、读取套接字协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
	b)、对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端端状态的argv和argc属性里面。
	c)、调用命令执行器,执行客户端指定的命令。
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面(cmd指针指向redisCommand结构)。
命令表是一个字典,间事命令名,值是redisCommand结构:

redisCommand结构的主要属性
sflags属性的标识
执行预备操作:检查客户端状态的cmd指针是否指向NULL、根据客户端cmd属性指向的redisCommand结构的arity属性(检查命令请求所给定的参数个数是否正确)、检查客户端是否已通过身份验证、如果服务器打开了maxmemory功能还要检查服务器内存占用情况……

serverCron函数

redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。
redis服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用为了减少系统调用的执行次数,服务器状态中的unixtime(保存了秒级精度的系统当前时间unix时间戳)属性和msttime(保存了毫秒级精度的系统当前unix时间戳)属性被用作当前时间的缓存。因为serverCron函数默认会以每100毫秒一次的频率更新unixtime属性和msttime属性,所以这两个属性记录的时间精确度并不高。对于为键设置过去时间、添加慢查询日志这种需要高精度时间的功能服务器还是会再次执行系统调用。
服务器状态中的lruclock(默认每10秒更新一次的时钟缓存,用于计算键的空转时长)属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime和msttime属性一样,都是服务器时间缓存的一种。
每隔redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间。
当服务器要计算一个数据库键的空转时间(也就是数据库键对应的值对象的空转时间),程序会用服务器的lrolock属性记录的时间减去对象的lru属性记录的时间,得出的计算结果就是这个对象的空转时间。

服务器初始化

初始化服务器第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。主要工作:
	a)、设置服务器的运行id。
	b)、设置服务器的默认允许频率。
	c)、设置服务器的默认配置文件路径。
	d)、设置服务器的允许架构。
	e)、设置服务器的默认RDB持久化条件和AOF持久化条件。
	f)、初始化服务器的LRU时钟。
	g)、创建命令表。
服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。
服务器在载入用户指定的配置选项,并对server状态进行更新之后,服务器就可以进入初始化的第三个阶段--初始化服务器数据结构。如:
	a)、server.client链表
	b)、server.db数组
	c)、用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
	d)、用于执行lua脚本的lua环境server.lua
	e)、用于保存慢查询日志的server.slowlog属性。
初始化服务器进行到这一步,服务器将调用initServer函数,为以上提到的数据结构分配内存,并在有需要时,为这些数据结构设置或者管理初始化值。
在完成了服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
在初始化最后一步,服务器开始执行服务器的事件循环。

重点回顾

一个目录请求从发送到完成主要包括以下步骤:1)、客户端将命令请求发送给服务器;2)服务器读取命令请求,并分析出命令参数;3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端。
serverCron函数默认每隔100毫秒执行一次,它的工作主要包括要更新服务器状态信息,处理服务器接受的SIGTERM信号,处理服务器,管理客户端资源和数据库状态,检查并执行持久化操作等等。
服务器从起点到能够处理客户端的命令请求需要执行以下步骤:1)、初始化服务器状态;2)、载入服务器配置;3)初始化服务器数据结构;4)、还原数据库状态;5)执行事件循环。

复制

在redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,我们称呼为被复制的服务器为主服务器(master),而对主服务器进行复制操作则被称为从服务器(slave)。
redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作:
	a)、同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
	b)、命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。
从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成,以下是SYNC命令的执行步骤:
	a)、从服务器向主服务器发送SYNC命令。
	b)、收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从限制开始执行的所以写命令。
	c)、当主服务器的BGSAVE命令执行完毕时。主服务器会将BGSAVE命令生成的RDB文件发送给服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
	d)、主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。

主从服务器在执行SYNC命令期间的通信过程
在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,但这种一致并不是一成不变的,每当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不再一致。
为了让注册服务器再次回到一致状态,注册服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。
旧版复制:初次辅助使用同步,断线之后不是通过命令传播实现同步而是再次同步。效率不高。
SYNC是一个非常消耗资源的操作:
a)、主服务器执行BGSAVE。
b)、主服务器将RDB文件发给从服务器。
c)、从服务器载入RDB文件。

新版复制

redis从2.8开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:
	a)、完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
	b)、而部分重同步则用于处理短线后重复制情况:当从服务器在短线后重新连接主服务器,如果条件允许,主服务器可以将注册服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。

主从服务器执行部分重同步过程
部分重同步功能由以下三个部分构成:
a)、主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。
b)、主服务器的复制挤压缓冲区(replication backlog)。
c)、服务器的允许ID(run ID)。
执行复制的双方主服务器和从服务器会分别维护一个复制偏移量:
a)、主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
b)、从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。
从服务器短线之后向主服务器发送PSYNC命令,报告主服务器当前复制偏移量。
复制挤压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小1MB。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制挤压缓冲区。
当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器根据这个偏移量来决定对从服务器执行何种同步操作。
实现部分重同步还需要用到服务器运行ID:每个redis服务器,不论主服务器还是从服务,都有自己的运行ID。运行ID在服务器启动时自动生成,由40个随机的16进制字符串组成。
当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。
当从服务器短线并重连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID:
a)、如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器短线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。
b)、相反的,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。

PSYNC命令

PSYNC命令的调用方法有两种:
	a)、如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVE no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC?-1命令,主动请求主服务器进行完成重同步。
	b)、相反的,如果从服务器以及复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判定应该对从服务器执行那种同步操作。
根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:
	a)、如果返回+FULLRESYNC <runid> <offset>回复,执行重同步。runid是主服务器运行ID,offset是主服务器当前的偏移量,从服务器将这个值作为主机的初始化偏移量。
	b)、如果主服务器返回+CONTINUE回复,那么主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将主机缺少的那部分数据发送过了就可以了。
	c)、如果返回-ERR回复,那么标识主服务器的版本低于2.8,识别不了PSYNC命令,从服务器执行SYNC命令,执行完整同步。

PSYNC执行完整重同步和部分重同步时可能遇到的情况

复制的实现

SLAVEOF <naster_ip> <master_port>
当客户端向从服务器发送SLAVEOF命令时,从服务器首先要做的就是将客户端给定的主服务器IP地址和端口保存到服务器状态的masterhost和masterport属性里面。

从服务器的服务器状态
SLAVEOF命令是一个异步命令,在完成masterhost和masterport属性的设置工作之后,从服务器将向发送SLAVEOF命令的客户端返回OK,表示复制指令以及被接收,而实际的复制工作将在OK返回之后真正开始执行。
在SLAVEOF命令执行之后,从服务器将根据命令所设置的IP地址和端口,创建连向主服务器的套接字连接。
如果从服务器创建的套接字能超过连接到主服务器,那么从服务器将为这个套接字关联一个专用于处理复制工作的文件事件处理器,这个处理器将负责执行后续的复制工作,比如RDB文件,以及接收主服务器传播来的写命令。
而主服务器在接受(accept)从服务器的套接字连接之后,将为该套接字创建相应的客户端状态,并将从服务器看作是一个连接到主服务器的客户端来对待,这时从服务器将同时具有服务器(server)和客户端(client)两个身份:从服务器可以向主服务器发送命令请求,而主服务器则会向从服务器返回目录回复。
从服务器成为主服务器的客户端之后,做的第一件事就是向主服务器发送一个PING命令,这个PING命令有两个作用:
a)、虽然主从服务器成功建立起了套接字连接,但双方并未使用该套接字进行过任何通信,通过发送PING命令可以检查套接字的读写状态是否正常。
b)、因为复制工作接下来的几个步骤都必须在主服务器可以正常处理命令请求的状态下才能进行,通过发送PING命令可以检查主服务器能否正常处理命令请求。
PING命令返回结果:
a)、从服务器不能在规定时限(timeout)内读取出命令回复的内容,表示主从网络连接状态不佳。从服务器断开并重新创建连向主服务器的套接字。
b)、返回一个错误,表示不能执行复制工作,从服务器断开。
c)、从服务器读取到 PONG 回复,表示主从服务器网络连接正常,可以继续执行复制工作。
从服务器在收到主服务器返回 PONG 回复之后,下一步要做的就是决定是否进行身份验证。
在身份验证之后,从服务器将执行命令 REPLCONF listeningport ,向主服务器发送从服务器的监听端口号。
主服务器在接收到这个命令之后,会将端口号记录在从服务器所对应的客户端状态的slave_listening_port属性中。
然后进行同步操作,并将主机的数据库更新至主服务器数据库当前所处的状态。
值得一提的是,在同步操作执行之后,只有从服务器是主服务器的客户端,但是在执行同步操作之后,主服务器也会成为从服务器的客户端:
a)、如果PSYNC命令执行的是完整重同步操作,那么主服务器需要成为从服务器的客户端,才能将保存在缓冲区里面的写命令发送给从服务器执行。
b)、如果PSYNC命令执行的是部分重同步操作,那么主服务器需要成为从服务器的客户端,才能向从服务器发送保存在复制积压缓冲区里面的写命令。
完成了同步之后,主从服务器就会进入命令传播阶段,这是主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保存一致了。

心跳检测

在命令传播节点,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>。replication_offset是从服务器当前的复制偏移量。
发送此命令的作用:检测主从服务器的网络连接状态、辅助实现min-slaves选项、检测命令丢失。
通过向主服务器发送INFO replication命令,在lag栏,可以看到从服务器最后一次向主服务器发送REPLCONF ACK命令距离现在过了多少秒。
redis的min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令。
	例:min-slaves-to-write 3
	min-slaves-max-lag 10
	那么在从服务器的数量少于3个或者三个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令,这里的延迟值就是info replication命令的lag值。
	通过此命令带上复制偏移量,可以得到从服务器和主服务器的偏移量差异,从而检测命令丢失,补发缺失的数据。。
主服务器向从服务器补发缺失数据这一操作的原理和部分重同步操作的原理非常相似,这两个操作的区别在于,不发缺失数据操作在主从服务器没有断线的情况下执行,而部分重同步操作则在主从服务器断线并重连之后执行。

重点回顾

redis2.8以前的重复制功能不能高效处理断线后的重复制情况,但redis2.8新添加的部分重同步功能可以解决这个问题。
部分重同步通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分来实现。
在复制操作刚开始的时候,从服务器会成为主服务器的客户端,并通过向主服务器发送命令请求来执行复制操作,而在复制操作的后期,主从服务器会互相成为对方的客户端。
主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致,而从服务器则通过向主服务器发送命令来进行心态检测,以及命令丢失检测。

Sentinel

sentinel(哨岗、哨兵)是redis的高可用性(high availability)解决方案:由一个或多个sentinel实例组成的sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替以下线的主服务器继续处理命令请求。

服务器和sentinel系统
当server1的下线时长超过用户设定的时长上限时,sentinel系统就会对server1执行故障转义操作:
a)、sentinel挑选server1属下的其中一个从服务器,并将这个被选中的从服务器升级为新的主服务器。
b)、sentinel系统会向server1属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。
c)、sentinel会继续执行已下线的server1,并在他重新上线时,将它设置为新的主服务器的从服务器。
启动一个sentinel可以使用命令:redis-sentinel /path/to/your/sentinel.conf 或者 redis-server /path/to/your/sentinel.conf --sentinel。
当一个sentinel启动时,它需要执行以下步骤:
a)、初始化服务器。
b)、将普通redis服务器使用的代码替换成sentinel专用代码。
c)、初始化sentinel状态。
d)、根据给定的配置文件,初始化sentinel的监视主服务器列表。
e)、创建连向主服务器的完了连接。

启动并初始化sentinel

sentinel本质只是一个运行在特殊模式下的redis服务器,所以启动sentinel的第一步,就是初始化一个普通的redis服务器。
不过因为sentinel执行的工作和普通redis服务器执行的工作不同,所以sentinel的初始化工作和普通redis服务器的初始化过程并不完全相同。

sentinel模式下的redis服务器主要功能的使用情况

启动sentinel的第二个步骤就是将一部分普通redis服务器使用的代码替换成sentinel专用代码,如:改端口,sentinel默认使用 26379、sentinel使用sentinel.c/sentinelcmds作为服务器的命令表。
第三步服务器会初始化一个sentinel.c/sentinelState结构(后面简称sentinel状态),这个结构保存了服务器中所有和sentinel功能有关的状态。

在这里插入图片描述
再是初始化sentinel状态的masters属下,sentinel状态中的masters字典记录了所有被sentinel监视的主服务器的相关信息,其中:
字段监视被监视服务器点的名字,值是被监视主服务器对应的sentinel.c/sentinelRedisInstance结构。

typedef struct sentinelRedisInstance {
    //标识值,记录了实例的类型,以及该实例的当前状态
    int flags;
    //实例的名字
    //主服务器的名字由用户在配置文件中设置
    //从服务器以及Sentinel的名字由Sentinel自动设置
    //格式为ip:port,例如"127.0.0.1:26379"
    char *name;
    //实例的运行ID
    char *runid;
    //配置纪元,用于实现故障转移
    uint64_t config_epoch;
    //实例的地址
    sentinelAddr *addr;
    // SENTINEL down-after-milliseconds选项设定的值
    //实例无响应多少毫秒之后才会被判断为主观下线(subjectively downmstime_t down_after_period;
    // SENTINEL monitor <master-name> <IP> <port> <quorum>
选项中的quorum参数
    //判断这个实例为客观下线(objectively down)所需的支持投票数量
    int quorum;
    // SENTINEL parallel-syncs <master-name> <number>选项的值
    //在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs;
    // SENTINEL failover-timeout <master-name> <ms>选项的值
    //刷新故障迁移状态的最大时限
    mstime_t failover_timeout;
    // ...
} sentinelRedisInstance;
每个sentinelRedisInstance结构(后面简称实例结构)代表一个被sentienl监视的redis服务器实例,这个实例可以是主服务器、从服务器,或者另外一个sentinel。
初始化sentinel的最后一步是创建连向被监视主服务器的网络连接,sentinel将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。
对于每个被sentinel监视的主服务器来说,sentinel会创建两个连向主服务器的异步网络连接:
	a)、一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。
	b)、另一个是订阅连接,这个连接专门用于订阅主服务器的 __sentinel__:hello频道。

获取主服务器信息

sentinel默认以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息:
	a)、主服务器本身的信息。
	b)、主服务器下的从服务器的信息。sentinel无需用户提供从服务器的地址信息,就可以自动发现从服务器。
根据INFO返回的信息,sentinel将对主服务器的实例结构进行更新。从服务器信息也会被用于更新主服务器实例结构的slaves字典,这个字典记录了主服务器属下从服务器的名单:
	键是sentinel字典设置,格式为ip:port,值是从服务器对于的实例结构。
sentinel在分析INFO命令中包含的从服务器信息时,会检查从服务器对应得实例结构是否已经存在于slaves字典,已存在则更新实例结构,否则添加一个实例结构。

主服务器和它的三个从服务器
主服务器点的flags属性值为SRI_MASTER,从服务器得为SRI_SLAVE。
主服务器得name属性只是用户使用sentinel配置文件设置得,从服务器name属性由sentinel根据ip:port自动设置。

获取从服务器信息

在sentinel发送主服务器有新的从服务器出现时,sentinel除了会为这个新的从服务器创建相应得实例结构之外,sentinel还会创建连接到从服务器得命令连接和订阅连接。

sentinel与各个从服务器建立命令连接和订阅连接
在创建命令连接之后,sentinel在默认情况下,会以每十秒一次得频率通过命令连接向从服务器发送INFO命令,获得以下信心:从服务器得运行ID run_id、从服务器的角色role、主服务器的IP地址master_host,以及主服务器的端口号master_port、主从服务器的连接状态master_link_status、从服务器的优先级slave_priority、从服务器的复制偏移量slave_repl_offset。
根据这些信息,sentinel会对从服务器的实例结构进行更新。

向主服务器和从服务器发送信息

在默认情况下,sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
这条命令是向服务器的 __sentinel__:hello频道发送了一条信息,信息的内容由多个参数组成:
	其中s开头的参数记录的时sentinel本身的信息。

信息中和sentinel有关的参数
而m_开头的参数记录的是主服务器的信息。如果sentinel正在监视主服务器,那么这些参数记录的就是主服务器的信息,如果sentinel记录的是从服务器,那么这些参数就是从服务器正在复制的主服务器的信息。
信息中和主服务器有关的参数

接收来自主服务器和从服务器的频道信息

当sentinel与一个主服务器或者从服务器建立起订阅连接之后,sentinel就会通过订阅连接,向服务器发送以下命令:SUBSCRIBE __sentinel__:hello
sentinel对__sentinel__:hello频道的订阅会一直持续到sentinel与服务器的连接断开为止。
也就是说:对于每个与sentinel连接的服务器,sentinel既通过命令连接向服务器的__sentinel__:hello频道发送信息,又通过订阅连接从服务器的__sentinel__:hello频道接收信息。
对于监视同一个服务器的多个sentinel来说,一个sentinel发送的信息会被其他sentinel接收到,这些信息会被用于更新其他sentinel对发送信息sentinel的认知,也会被用于更新其他sentinel对被监视服务器的认知。
sentinel为主服务器创建的实例结构中的sentinel字典保存了除sentinel本身之外,所有同样监视这个主服务器的其他sentinel的资料。
当一个sentinel接收到其他sentinel发来的信息时(称发送信息的sentinel为源sentinel,接收信息的sentinel为目标sentinel),目标sentinel会从信息中分析并提取出与sentinel有关的参数和与主服务器有关的参数。
根据信息中提取出的主服务器参数,目标sentinel会在自己的sentinel状态的masters字典查找相应的主服务器实例结构,然后根据提取出的sentinel参数,检查主服务器实例结构的sentinel字典中,源sentinel的实例结构是否存在,存在即更新,否则将其添加到sentinel字典中。
因为sentinel可以通过分析接口收到的频道信息来获知其他sentinel的存在,并通过发送频道信息来让其他sentinel知道自己的存在,所以用户在使用sentinel的时候并不需要提供各个sentinel的地址信息,监视同一个主服务器的多个sentinel可以自动发现对方。
当sentinel通过频道信息发现一个新的sentinel时,它不仅会为新sentinel在sentinel字典中创建相应的实例结构,还会创建一个连向新sentinel的命令连接,而新sentienl也同样会创建连向这个sentinel命令连接,最终监视同一主服务器的多个sentinel将形成相互连接的网络。
sentinel之间只会创建命令连接,不会创建订阅连接。因为sentinel需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新sentinel,所以才需要建立订阅连接,而相互已知的sentinel只要使用命令连接来进行通信就足够了。

检测主观下线状态

在默认情况下,sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否存在。
sentinel配置文件中的down_aftermilliseconds选项指定了sentinel判断实例进入主观下线所需的时间长度:如果一个实例在down_aftermilliseconds毫秒内,连续向sentinel返回无效回复,那么sentinel会修改这个实例所对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,以此来表示这个实例以及进入主观下线状态。
down_aftermilliseconds不仅用来判断主服务器的主观下线状态,还会被用来判断主服务器属下的所有从服务器,以及所有同样监视这个主服务器的其他sentinel的主观下线状态。

检查客观下线状态

当sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以时主观下线或者客观下线)。当sentinel从其他sentinel那里接收到足够数量的已下线判断之后,sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。
sentinel使用
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
命令询问其他sentinel是否同意主服务器已下线,参数意义如下

在这里插入图片描述
当一个sentinel(目标sentinel)接收到了另一个sentine(源sentinel)发来的 SENTINEL is-master-down-by-addr 命令时,目标sentinel会分析并取出命令请求中包含的各个参数,并根据其中的主服务器IP和端口,检查主服务器是否已下线,然后向源sentinel返回一条包含三个参数的Multi Bulk回复作为SENTINEL is-master-down-by-addr 命令的回复
在这里插入图片描述
当认为主服务器已经进入下线状态的sentinel的数量,超过sentinel配置中设置的quorum参数的值,那么该sentinel就会认为主服务器已经进入客观下线状态。

sentinel moniter master 127.0.0.1 6379 2 
上方配置的2就是quorum值。

选举领头sentinel

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个sentinel会进行协商,选举出一个sentinel,并由领头的sentinel对下线主服务器执行故障转移操作。
以下为选举领头sentinel的规则和方法:
	a)、所有在线的sentinel都有被选为领头sentienl的资格。
	b)、每次进行领头sentinel选举之后,不论选举是否成功,所有sentinel的配置纪元的值都会自增一次。
	c)、在一个配置纪元里面,所有sentinel斗鱼一次将某个sentinel设置为局部领头sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。
	d)、每个发现主服务器进入客观下线的sentinel都会要求其他sentinel将自己设置为局部领头sentinel。
	e)、当一个sentinel(源sentinel)向另一个sentinel(目标sentinel)发送SENTINEL is-master-down-by-addr命令,并且命令中的runid蚕食不是*符号而是源sentinel的运行ID时,这表示源sentinel要求目标sentinel将前者设置为后者的局部领头sentienl。
	f)、sentinel设置局部领头sentinel的规则是先到先得:最先向目标发送sentinel发送设置要求的源sentinel将成为目标sentinel的局部领头sentinel,而之后接收到的所有设置要求都被拒绝。
	g)、目标sentinel在接收到SENTINEL is-master-down-by-addr命令之后,将向源sentinel返回一条命令回复,回复中的loader_runid参数和loader_epoch参数分别记录了目标sentinel的局部领头sentinel的运行ID和配置纪元。
	h)、源sentinel在接收到目标sentinel返回的命令回复之后,会检查回复中loader_epoch参数的值和自己的配置纪元是否相同,相同则取出回复中的loader_runid参数,如果该值与源sentinel的运行ID一致,那么表示目标sentinel将源sentinel设置成了局部领头sentinel。
	i)、如果有某个sentinel被半数以上的sentinel设置成了局部领头sentinel,那么这个sentinel成为领头sentinel。
	j)、因为领头sentinel的产生需要半数以上sentinel的支持,并且每个sentinel在每个配置纪元里面只能设置一次局部领头sentinel,所以在一个配置纪元里面,只会出现一个领头sentinel。
	k)、如果在规定时限内,没有一个sentinel被选举为领头sentinel,那么各个sentinel将在一段时间之后再次进行选举,直至选出领头sentinel。

故障转移

在选举出领头sentiinel之后,领头setinel将对已下线的主服务器执行故障转移操作,该操作包括以下三个步骤:
	a)、在已下线主服务器属下的所有从服务器里面,挑出一个从服务器转换为主服务器。
	b)、让已下线主服务器属下的所有从服务器改为复制新的主服务器。
	c)、将已下线主服务器设置为新的主服务器的从服务器。
新主服务器挑选规则:
	a)、删除列表中所有处于下线或者断线状态的从服务器,这可以保证列表中剩余的从服务器都是正常在线的。
	b)、删除列表中最近五秒内没有回复过领头sentine的INFO命令的从服务器,保证列表中的从服务器都是最近成功进行过通信的。
	c)、删除所有与已下线主服务器连接断开超过down_aftermilliseconds*10毫秒的从服务器。
	之后,**领头sentinel将根据服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器。如果具有相同优先级,选取从服务器的复制偏移量最大的。如果有多个复制偏移量最大的,根据运行ID进行排序。**
新的主服务器出现之后,领头sentinel下一步要做的是已下线主服务器属下的所有从服务器去复制新的主服务器,这一动作可以通过向从服务器发送SLAVEOF命令来实现。
最后要做的是,将已下线的主服务器设置为新主服务器的从服务器。

重点回顾

sentinel只是一个运行在特殊模式下的redis服务器,它使用了呵呵普通模式不同的命令表。
sentinel会读入用户指定的配置文件,为每个要被监视的主服务器创建相应的实例结构,并创建连续主服务器的命令连接和订阅连接,其中命令连接用于向主服务器发送命令请求,而订阅连接则用于接收指定频道的消息。
	sentinel通过向主服务器发送INFO命令来获得主服务器属下所有从服务器的地址信息,并为这些从服务器创建相应的实例结构,以及连向这些从服务器的命令连接和订阅连接。
	在一般情况下,sentinel以每十秒一次的频率向被监视的主服务器和从服务器发送INFO命令,当主服务器处于下线状态,或者sentinel正在对主服务器进行故障转移操作时,sentinel向从服务器发送INFO命令的频率会改为每秒一次。
	对于监视同一个主服务器和从服务器的多个sentinel来说,它们会以每两秒一次的频率,通过向被监视服务器的 __sentinel__:hello 频道发送消息来向其他ssentinel宣告自己的存在。
	每个sentinel也会从 __sentinel__:hello 频道接收其他sentinel发来的信息,并根据这些信息为其他sentinel创建相应的实例结构,以及命令连接。
	sentinel只会与主服务器和从服务器创建命令连接和订阅连接,sentinel与sentinel之间则只创建命令连接。
	sentinel以每秒一次的频率向实例(包括主服务器、从服务器、其他sentinel)发送PING命令,并根据实例对PING命令的回复来判断实例是否在线,当一个实例在指定的时长中连续向sentinel发送无效回复时,sentinel会将这个实例判断为主观下线。
	当sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他sentinel进行询问,看它们是否同意这个主服务器以及进入主观下线状态。
	当sentinel收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。

配置redis哨兵模式

写一个sentinel.conf,里面写上配置 sentinel monitor mymaster 127.0.0.1 6381 1   即可
															sentinel monitor 集群名字 ip port  多少个机器认定掉线即掉线
./bin/redis-server ./sentinel.conf --sentinel  启动sentinel
搭建一个主机为sentinel monitor指定ip的redis主从集群。

集群

一个redis集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组件一个真正的可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
连接各个节点的工作可以使用CLUSTER MEET命令来完成,该命令的格式如下:
CLUSTEER MEET <ip> <port>
向一个节点发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。
一个节点就是一个运行在集群模式下的redis服务器,redis会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的IP地址和端口号等等。
每个节点都会使用一个clusternode结构来记录自己的状态,并为集群这所有其他节点(包括主节点和从节点)都创建一个相应的clusterNode状态,以此来记录其他节点的状态:
struct clusterNode {
    //创建节点的时间
    mstime_t ctime;
    //节点的名字,由40个十六进制字符组成
    //例如68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN];
    //节点标识
    //使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    //以及节点目前所处的状态(比如在线或者下线)。
    int flags;
    //节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;
    //节点的IP地址
    char ip[REDIS_IP_STR_LEN];
    //节点的端口号
    int port;
    //保存连接节点所需的有关信息
    clusterLink *link;
    // ...
};
cluster结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区:
typedef struct clusterLink {
    //连接的创建时间
    mstime_t ctime;
    // TCP套接字描述符
    int fd;
    //输出缓冲区,保存着等待发送给其他节点的消息(message)。
    sds sndbuf;
    //输入缓冲区,保存着从其他节点接收到的消息。
    sds rcvbuf;
    //与这个连接相关联的节点,如果没有的话就为NULL
    struct clusterNode *node;
} clusterLink;
每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含了多少个节点,集群当前的配置纪元:
typedef struct clusterState {
    //指向当前节点的指针
    clusterNode *myself;
    //集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;
    //集群当前的状态:是在线还是下线
    int state;
    //集群中至少处理着一个槽的节点的数量
    int size;
    //集群节点名单(包括myself节点)
    //字典的键为节点的名字,字典的值为节点对应的clusterNode结构
    dict *nodes;
    // ...
} clusterState;

在这里插入图片描述

通过向节点发送 CLUSTER MEET 命令。客户端可以让接收命令的节点A将发送命令的节点B添加到接收命令节点A所在的集群。
收到命令的节点A将于节点B进行握手(handshake),以此来确认批次的存在,并为将来的进一步通信打好基础:
	a)、节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterStatus.nodes字典里面。
	b)、之后,节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息(message)。
	c)、如果一切顺利,节点B将接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterStatus。nodes字典里面。
	d)、之后,节点B将向节点A返回一条PONG消息。
	e)、节点A接收到节点B的PONG消息,通过这条PONG消息节点A可以知道节点B成功的接收了自己发送的MEET消息。
	f)、节点A向节点B返回一条PING消息。
	g)、节点B收到节点A返回的PING消息,握手成功。

节点的握手过程
redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为18384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
当数据库的16384个槽都有节点在处理时,集群处于上线状态(ok),相反的,如果数据库中由任何一个槽没有得到处理,那么集群处于下线状态(fail)。
通过向节点发送 CLUSTER ADDSLOTS命令,我们可以将一个或多个槽指派给节点负责:

CLUSTER ADDSLOTS <SLOT> [SLOT ...]
例: CLUSTER ADDSLOTS 0 1 2 34个槽分配给当前节点
可通过 sluster slots 命令查看槽分配信息
clusterNode结构的slots属性和numslot属性记录了节点负责处理那些槽:
struct clusterNode {
    // ...
    unsigned char slots[16384/8];
    int numslots;
    // ...
};
slots属性是一个二进制数组(bit array)	,这个数组长度为 16384 / 8=2048字节,共包含16384个二进制位。
redis以0位起始索引,16384为终止索引,多slots数组中的16384个二进制位进行编号,并根据索引 i  上的二进制位的值来判断节点是否负责处理槽 i :
	如果slots数组在索引 i 上的二进制位的值为1,那么表示节点负责处理槽 i 。相反,为 0 表示节点不负责处理槽 i 。
	numslots属性记录节点负责处理的槽的数量,也即是slots数组中的值为1的二进制数量。
	一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
	clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:
typedef struct clusterState {
    // ...
    clusterNode *slots[16384];
    // ...
} clusterState;
slots数组包含了16384个项,每个数组项都是一个指向clusterNode结构的指针,指向NULL表示槽未指派,指向一个clusterNode结构,表示槽已指派。

clusterState结构的slots数组
clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode机构所代表的节点的槽指派信息,这是两个slots数组的关键区别所在。

在集群中执行命令

在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点胡计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:如果键所在槽指派给了当前节点,节点直接执行。否则向客户端返回MOVEN错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。
redis提供 CLUSTER KEYSLOT <key> 来查看给定键数据哪个槽。例:cluster keyslot a 。
当计算出键所属槽 i 之后,节点就会检查自己在clusterStatu.slots数组的项 i ,判断键所在的槽是否由自己负责:如果clusterStatus.slot[i] 等于 clusterState.myself,那么说明槽 i 由当前节点负责节点可以执行客户端发送的命令。否则返回MOVED错误,指引客户端转向至正在处理槽 i 的节点。
MOVED错误的格式为:MOVED <slot> <ip>: <port>
slot为所在槽,ip和port为负责处理槽slot的节点ip地址和端口号。
当客户端接收到节点返回的MOVED错误时,客户端会根据MOVED错误中提供的IP地址和端口号,转向至负责处理槽slot的记得,并向该节点重新发送之前想要执行的命令。

节点数据库的实现

节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库,单机redis服务器则没有这一限制。
另外,除了键键值对保存在数据库里面之外,节点还会用clusterState结构中的slots_to_keys跳跃表来保存槽和键的关系。
typedef struct clusterState {
    // ...
    zskiplist *slots_to_keys;
    // ...
} clusterState;
slots_to_keys跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员都是一个数据库键:
	a)、每当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到slots_to_keys跳跃表。
	b)、当节点删除数据库中的某个键值对时,节点就会在slots_to_keys跳跃表接触被删除键与槽号的关联。

重新分片

redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分配操作可以在线进行。
redis集群的重新分片操作是由redis的集群管理软件redis-srib负责执行的,redis提供了进行重新分片所需的所有命令,而redis-trib则通过像源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽进行重新分片的步骤如下:
	a)、redis-trib对目标节点发送cluster setslot <slot> importing <source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
	b)、redis-trib对源节点发送cluster setslot <slot> migrating <target_id> 命令,让源节点准备好将数据槽slot的键值对迁移(migrate)至目标节点。
	c)、redis-trib向源节点发送cluster getkeysinslot <slot> <count> 命令,获取最多count个数据槽slot的键值对的键名。
	d)、对于步骤3获得得每个键名,redis-srib都向源节点发送一个MIGRATE <target_ip> <target-port> <key_name> 0  <timeout> 命令,将被选中得键原子地从源节点迁移至目标节点。
	e)、重复执行步骤3和步骤4,直到源节点地所有属于槽slot地键值对都被迁移至目标节点为止。
	f)、redis-trib向集群中的任意一个节点发送cluster setslot <slot> node <target_id> 命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中地所有节点都会知道槽slot已经指派给了目标节点。

迁移键的过程

ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这一一种情况:数据被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
	a)、源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
	b)、相反的。没有找到,源节点将向客户端返回一个ASK错误,指引客户端转向这在导入槽的目标节点,并再次发送之前想要执行的命令。
	clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽:
typedef struct clusterState {
    // ...
    clusterNode *importing_slots_from[16384];
    // ...
} clusterState;
如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从cluster所代表的节点导入槽 i 。
CLUSTER SETSLOT <i> IMPORTING <source_id>
将目标节点clusterState.importing_slots_from[ i ]的值设置位source_id所代表节点的clusterNode结构。
clusterState的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽:
typedef struct clusterState {
   // ...
   clusterNode *migrating_slots_to[16384];
   // ...
} clusterState;
如果migrating_slots_to[ i ] 的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在将槽 i 迁移至clusterNode所代表的节点。
CLUSTER SETSLOT <i> MIGRATING <target_id>
将源节点clusterState.migrating_slots_to[ i ]的值设置为target_id所代表节点的clusterNode结构。
如果节点接收到一个key的命令请求,并且key所属的槽 i ,正好就指派给了这个节点,节点会尝试查找,如果找到了,节点会直接执行客户端发送的命令,否则会查找clusterState.imgrating_slot_to[ i ],看key所属的槽 i 是否正在进行转移,如果槽 i 的确在进行迁移的话,那么节点会向客户端发送一个ASK错误,引导客户端到正在导入槽 i 的节点去查找键key。
ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识。
客户端向节点发送一个槽 i 的命令,如果节点的clusterState.importing_slots_from[ i ]显示节点正在导入槽 i ,并且发送命令的客户端带有redis_asking标识,那么节点将破例执行这个关于槽 i 的命令一次。
当客户端接收到ASK错误并转向正在导入槽的节点时,客户端会先向节点发送一个ASKING命令,然后才重新发送想要执行的命令,这是因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,那么客户端发送的命令会被节点拒绝执行,并返回MOVEN错误。
客户端的REDIS_ASKING标识是一个一次性标识,当节点执行了一个带有REDIS_ASKING标识的客户端发送的命令之后,客户端的REDIS_ASKING标识就会被移除。
ASK错误和MOVED错误区别:
	请求的键所属槽不在当前节点,返回moved错误,重定向到该槽指派的节点。代表槽的负责权已经从一个节点转移到另一个节点。
	请求的键所属的槽应该属于当前节点,但是现在该槽已被分片指派到新的节点,并且该键在当前节点不存在(要么是被迁移了,要么是一直都不存在),返回ask错误,重定向到新节点。只是两个节点在迁移槽的过程中使用的一种临时措施。

集群和故障转移

redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替主节点继续处理命令请求。
CLUSTER REPLICATE <node_id>
让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制:
	接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点:
struct clusterNode {
    // ...
    //如果这是一个从节点,那么指向主节点
    struct clusterNode *slaveof;
    // ...
};
然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SALVE标识,表示这个节点已经由原来打的主节点变成了从节点。
最后,节点会调用复制代码,并根据clusterState.myself.salveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制,因为节点的复制功能和单机redis服务器的复制功能使用了相同的代码,所以让从节点复制主节点相当于向从节点发送命令SLAVEOF。
一个节点成为从节点,并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点。
集群中的所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单:
struct clusterNode {
    // ...
    //正在复制这个主节点的从节点数量
    int numslaves;
    //一个数组
    //每个数组项指向一个正在复制这个主节点的从节点的clusterNode结构
    struct clusterNode **slaves;
    // ...
};
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果节点PING消息地节点没有在规定地时间内向发送PING消息地节点返回PONG消息,那么发送PING消息的节点就会键接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态消息,例如某个节点是处于在线状态、疑似下线状态(PFAIL)、还是已下线状态(FAIL)。
将下线报告添加到clusterNode结构的fail_reports链表里面:
struct clusterNode {
    // ...
    //一个链表,记录了所有其他节点对该节点的下线报告
    list *fail_reports;
    // ...
};
每个下线报告由一个clusterNodeFailReprt机构表示:
ruct clusterNodeFailReport {
    //报告目标节点已经下线的节点
    struct clusterNode *node;
    //最后一次从node节点收到下线报告的时间
    //程序使用这个时间戳来检查下线报告是否过期
    //(与当前时间相差太久的下线报告会被删除)
    mstime_t time;
} typedef clusterNodeFailReport;
一个集群中,半数以上负责处理槽的主节点都将某个主节点报告为疑似下线,那么这个主节点将被标记为已下线(FAIL),将主节点标记为已下线的节点会向集群广播一条关于主节点的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点标记为已下线。
当一个节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
	a)、复制下线主节点的所有从节点里面,会有一个从节点被选中。
	b)、被选中打的从节点会执行slaveof no one命令,成为新的主节点。
	c)、新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
	d)、新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
	e)、新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
新节点选举方法:
	a)、集群的配置纪元是一个自增计数器,它的初始值为0。
	b)、当集群例的某个节点开始以此故障转移操作时,集群配置纪元的值会被增一。
	c)、对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
	d)、当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
	e)、如果一个主节点具有投票权(他正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点(主节点只有一次投票权)。
	f)、每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
	g)、如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
	h)、因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
	i)、如果一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入到一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

消息

集群中的各个节点通过发送和接收消息(message)来进行通信,我们称为发送消息的节点为发送者(sender),接收消息的节点为接收者(receiver)。
节点发送的消息主要有五种:
	a)、MEET消息:当发送者街道客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
	b)、PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后以此收到节点B发送的PONG消息的时间,距离当前时间已经戳了节点A的cluster_node_timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
	c)、PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认知。
	d)、FAIL消息,当一个主节点判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,素有收到这条消息的节点都会立即将节点B标记为已下线。
	e)、PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。
一条消息由消息头(header)和消息正文(data)组成。
节点发送的所有消息都由一个消息头包裹,消息头除了包含消息正文之外,还记录了消息发送者自身的的一些信息,因为这些消息也会被消息接收者用到,所以严格来讲,我们可以认为消息头本身也是消息的一部分。
每个消息头都有一个cluster.h/clusterMsg结构表示:
typedef struct {
    //消息的长度(包括这个消息头的长度和消息正文的长度)
    uint32_t totlen;
    //消息的类型
    uint16_t type;
    //消息正文包含的节点信息数量
    //只在发送MEET、PING、PONG这三种Gossip协议消息时使用
    uint16_t count;
    //发送者所处的配置纪元
    uint64_t currentEpoch;
    //如果发送者是一个主节点,那么这里记录的是发送者的配置纪元
    //如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元
    uint64_t configEpoch;
    //发送者的名字(ID)
    char sender[REDIS_CLUSTER_NAMELEN];
    //发送者目前的槽指派信息
    unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
    //如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字
    //如果发送者是一个主节点,那么这里记录的是REDIS_NODE_NULL_NAME
    //(一个40字节长,值全为0的字节数组)
    char slaveof[REDIS_CLUSTER_NAMELEN];
    //发送者的端口号
    uint16_t port;
    //发送者的标识值
    uint16_t flags;
    //发送者所处集群的状态
    unsigned char state;
    //消息的正文(或者说,内容)
    union clusterMsgData data;
} clusterMsg;
clusterMsg.data属性指向联合cluster.h/clusterMsgData,这个联合就是消息的正文:
union clusterMsgData {
    // MEET、PING、PONG消息的正文
    struct {
        //每条MEET、PING、PONG消息都包含两个
        // clusterMsgDataGossip结构
        clusterMsgDataGossip gossip[1];
    } ping;
    // FAIL消息的正文
    struct {
        clusterMsgDataFail about;
    } fail;
    // PUBLISH消息的正文
    struct {
        clusterMsgDataPublish msg;
    } publish;
    //其他消息的正文...
};
clusterMsg结构的currentEpoch、sender、myslots等属性记录了发送者自身的节点信息,接收者会根据这些信息,在自己的clusterState.nodes字典里找到发送者对应的clusterNode结构,并对结构进行更新。
redis集群中的各个节点通过Gossip协议来交换各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都有两个cluster.h/clusterMsgDataGossip结构组成:
union clusterMsgData {
    // ...
    // MEET、PING和PONG消息的正文
    struct {
        //每条MEET、PING、PONG消息都包含两个
        // clusterMsgDataGossip结构
        clusterMsgDataGossip gossip[1];
    } ping;
    //其他消息的正文...
};
因为MEET、PING、PONG三种消息都使用相同的消息正文,所以节点通过消息头的type属性来判断一条消息是MEET消息、PING消息还是PONG消息。
每次发送MEET、PING、PONG消息时,发送者都从自己的已知节点列表随机选出两个节点,并将这两个被选中节点的消息分别保存到两个clusterMsgDataGossip结构里面。
clusterMsgDataGossip结构记录了被选中节点的名字,发送者与被选中节点最后一次发送和接收PING消息和PONG消息的时间戳,被选中节点的IP地址和端口号,以及被选中节点的标识值:
typedef struct {
    //节点的名字
    char nodename[REDIS_CLUSTER_NAMELEN];
    //最后一次向该节点发送PING消息的时间戳
    uint32_t ping_sent;
    //最后一次从该节点接收到PONG消息的时间戳
    uint32_t pong_received;
    //节点的IP地址
    char ip[16];
    //节点的端口号
    uint16_t port;
    //节点的标识值
    uint16_t flags;
} clusterMsgDataGossip;
当接收者收到MEET、PING、PONG消息时,接收者会访问消息正文中的两个clusterMsgDataGossip结构,并根据自己是否认识clusterMsgDataGossip结构中记录的被选中节点来选择进行那种操作:
	不存在,接收者将根据结构中记录的IP地址和端口等信息,与被选中节点进行握手。否则更新被选中节点对应的clusterNode结构进行更新。
	在集群的节点数量比较大的情况下,单纯使用Gossip协议来传播节点的已下线信息会给节点的信息更新带来一定延迟,因为Gossip协议消息通常需要一段时间才能传播至整个集群,而发送FAIL消息可以让集群里的所有节点立即知道某个主节点已下线,从而尽快判断是否需要将集群标记为下线,又或者对下线主节点进行故障转移。
	FAIL消息的正文由cluster.h/clusterMsgDataFail结构表示,这个节点只包含一个nodename属性,该属性记录了已下线节点的名字:
typedef struct {
    char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
当客户端向集群中的某个节点发送命令:
PUBLISH <channe> <message>
的时候,接收到PUBLISH命令的节点不仅会向channel频道发送消息message,它还会向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会向channel频道发送message消息。
PUBLIST消息的正文由cluster.h.clusterMsgDataPublish结构表示
typedef struct {
    uint32_t channel_len;
    uint32_t message_len;
    //定义为8字节只是为了对齐其他消息结构
    //实际的长度由保存的内容决定
    unsigned char bulk_data[8];
} clusterMsgDataPublish;
bulk_data属性是一个字节数组,这个字节数组保存了客户端通过PUBLISH命令发送给节点的channel参数和message参数,而结构的channel_lLen和message_len则分别保存了channel参数的长度和message参数的长度:
	a)、bulk_data的0字节至channel_len -1 字节保存的是channel参数。
	b)、而bulk_data得channel_len字节至channel_len + message_len -1 字节保存的则是message参数。

redis 集群设置

redis 集群一般建议设置几个节点?
奇数,因为 redis 为了保证集群完整性,当集群16384个槽任何一个没有指派到节点时,执行任何命令都会返回错误信息,这是对集群完整性的一种保护措施。可设置参数 cluster-require-full-coverage 为no,则会在集群主节点宕机一半是整个集群无法使用。

重点回顾

节点通过握手来将其他节点添加到自己所处的集群当中。
集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录那些槽指派给了自己,而那些槽又被指派给了其他节点。
节点在接到了一个命令请求时,会先检查这个命令是否由自己负责,如果不是的话,节点将向客户端返回一个MOVED错误,MOVED错误携带的信息可以指引客户端转向至正在负责相关槽的节点。
对redis集群的重新分片工作是由redis-trib负责执行的,重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
如果节点A正在迁移槽 i 至节点B,那么当节点A没能在自己的数据库中找到命令指定的数据库键时,节点A会向客户端返回一个ASK错误,指引客户端到节点B继续查找指定的数据库键。
MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施。
集群里的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令请求。
集群中的节点通过发送和接收消息来进行通信创建的消息包括MEET、PING、PONG、PUBLISH、FAIL五种。

redis 快速集群分片手段,具体操作查看 https://www.bilibili.com/video/BV1gr4y1U7CY?p=46&vd_source=5e38b727e16795e1acf29c53739b2193  redis集群配置

在这里插入图片描述
上图命令解析 redis-cli --cluster ip1 ip2… --cluster-repplicas 1
以集群方式启动 redis,并且对每个主节点设置一个从节点,图中填入了6个主机地址,而没个主节点都设置从节点,所以集群分片由3个主机分担,每个集群节点都有一个从节点,形成 3主3从集群配置。具体操作可查看 B站 尚硅谷周阳 docker 教程 P46。

发布和订阅

频道的订阅和退订

redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCIBE等命令组成。
通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息。
通过 SUBSCRIBE 订阅频道,PUBLISH向模式发送消息。
当一个客户端执行SUBSCRIBE命令订阅某个或某些频道的时候,这个客户端与被订阅频道之间就建立起了一种订阅关系。
redis将所有频道的订阅关系都保存在服务器状态的publish_channels字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端:
struct redisServer {
    // ...
    //保存所有频道的订阅关系
    dict *pubsub_channels;
    // ...
};

一个pubsub_channels字典示例
每当客户端执行SUBSCRIBE命令订阅某个或某些频道时,服务器都会将客户端与被订阅的频道在pubsub_channels字典中进行关联,如果频道已有其他订阅者,则将客户端添加到订阅者链表的末尾,否则,先在pubsub_channels字典中为频道创建一个键,并将这个键的值设置为空链表,然后再将客户端添加到链表,成为链表第一个元素。
UNSUBSCRIBE命令的行为和SUBSCRIBE命令的行为正好相反,当一个客户端退订某个或频道的时候,服务器将从pubsub_channels中解除客户端与被退订频道之间的关联:先从订阅者链表删除退订客户端的信息,如果删除之后,订阅者链表变成了空链表,则从pubsub_channels字典中删除频道对应的键。

模式的订阅和退订

服务器将所有模式的订阅关系保存在服务器状态的pubsub_patterns属性里面:
struct redisServer {
    // ...
    //保存所有模式订阅关系
    list *pubsub_patterns;
    // ...
};
pubsub_patterns属性是一个链表,链表中的每个节点都包含着一个pubsub Pattern结构,这个结构的pattern属性记录了被订阅的模式,而client属性则记录了订阅模式的客户端:
typedef struct pubsubPattern {
    //订阅模式的客户端
    redisClient *client;
    //被订阅的模式
    robj *pattern;
} pubsubPattern;

在这里插入图片描述
客户端执行 PSUBSCRIBE命令订阅某个或某些模式的时候,服务器会对每个被订阅的模式执行以下两个操作:
a)、新建一个pubsubPttern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端。
b)、将pubsubPattern结构添加到pubsub_patterns链表的表尾。
PUNSUBSCRIBE 退订模式。就是从pubsub_patterns链表中删除pattern属性为被退订模式,并且client属性为执行退订命令的客户端的pubsubPattern结构。

发送消息

publish <channel> <message>
服务器对此命令需执行两个操作:
	a)、将消息message发送给channel频道的所有订阅者。
	b)、将消息发送给pattern模式的订阅者。

查看订阅消息

PUBSUB命令。客户端可以通过这个命令来查看频道或者模式的相关信息,比如某个频道有多少个订阅者,某个模式有多少个订阅者。
PUBSUB CHANNELS[pattern] 子命令用于返回服务器当前被订阅的频道,pattern不给值,返回所有频道,否则返回pattern相匹配的频道。此子命令通过遍历服务器pubsub_channels字典的所有键,然后记录并返回所有符合条件的频道来实现。例:
pubsub channels  查看所有频道
pubsub channels "news.[is]*"  查看以news.i或者news.s开头的频道
PUBSUB NUMSUB [channel-1 channel-2... channel-n]子命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量。例:
pubsub numsub "news.it"  查看订阅newes.it的的数量
通过在pubsub_channels字典中找到频道对应的订阅者链表,然后返回订阅者链表的长度来实现。

PUBSUB NUMPAT子命令用于返回服务器当前被订阅模式的数量。
通过返回pubsub_patterns链表的长度来实现,因为这个链表的长度就是服务器被订阅模式的数量。例:
pubsub numpat  
需服务器有被模式订阅。psubscribe

重点回顾

服务器状态在pubsub-channels字典保存了所有频道的订阅关系:SUBSCRIBE命令负责将客户端和被订阅的频道关联到这个字典里面,而UNSUBSCRIBE命令则负责解除客户端和被退订频道之间的关联。
负责状态在pubsub_patterns链表保存了所有模式的订阅关系:PSUBSCRIBE命令负责将客户端和被订阅的模式记录到这个链表中,而PUNSUBSCRIBE命令则负责移除客户端和被退订模式在链表中的记录。
PUBLISH命令通过访问pubsub_channels字典来向频道的所有订阅者发送消息,通过访问pubsub-patterns链表来向所有匹配频道的模式的订阅者发送消息。
PUBSUB命令的三个子命令都属读取pubsub_channels字典和pubsub_patterns链表中的信息来实现的。

事务

redis通过MULTI、EXEC、WATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令地机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端地命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端地命令请求。

事务的实现

一个事务从开始到结束通常会经历以下三个阶段:事务开始、命令入队、事务执行。
MULTI命令的执行标志着事务的开始。MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端的flags属性中打开REDIS_MULTI表示来完成。
客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作:
	a)、如果命令为EXEC、DISCARD、WATCH、MULTI四个其中一个,那么服务器立即执行。
	b)、如果是其他命令,服务器不立即执行,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。
每个redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate属性里面:
typedef struct redisClient {
    // ...
    //事务状态
    multiState mstate;    /* MULTI/EXEC state */
    // ...
} redisClient;
事务状态包含一个事务队列,以及一个已入队命令的计数器(也可以说是事务队列的长度):
typedef struct multiState {
    //事务队列,FIFO顺序
    multiCmd *commands;
    //已入队命令计数
    int count;
} multiState;
事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量:
typedef struct multiCmd {
    //参数
    robj **argv;
    //参数数量
    int argc;
    //命令指针
    struct redisCommand *cmd;
} multiCmd;
事务队列以先进先出(FIFO)的方式保存入队的命令,较先入队的命令会被先放到数组的前面,而较后入队的命令则会被放到数组的后面。
当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,最后将执行命令所得的结构全部返回给客户端。

WATCH命令的实现

WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
每个redis数据库都保存着一个watched_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端:
typedef struct redisDb {
    // ...
    //正在被WATCH命令监视的键
    dict *watched_keys;
    // ...
} redisDb;
所有对数据库进行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视各个被命令修改过的数据库键,如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。
当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务:如果被打开,服务器会拒绝客户端提交的事务。否则执行。

事务的ACID

在redis中,事务总是具有原子性(Atimicity)、一致性(Consistency)和隔离性(lsolation),并且当redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)。
redis的事务和传统的关系型数据库事务的最大区别在于,redis不支持事务回滚机制(rallback),即使事务队列中的某个命令在执行期间出了错,整个事务也会继续执行下去,直到事务队列中的所有命令执行完毕为止。
redis使用单线程执行事务,并且服务器保证,在执行事务期间不会对事务进行中断,因此,redis的事务总是以串行的方式运行,并且事务也总是具有隔离性。
服务器在无持久化模式下运作时,事务不具有耐久性。
在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性。
当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步(sync)函数,将莫数据真正地保存在硬盘里面,因此这种配置下的事务是具有耐久性地。
appendfsync是否具有耐久性
always是,执行命令之后就调用同步函数
ecerysec否,每秒同步一次
no否,程序交由操作系统决定何时同步
不论redis在上面模式下运作,在一个事务最后加上SAVE命令总可以保证事务地耐久性:
redis> MULTI
OK
redis> SET msg "hello"
QUEUED
redis> SAVE
QUEUED
redis> EXEC
1) OK
2) OK
不过这种方式做法效率太低,不具备实用性。

重点回顾

事务提供了一种将多个命令打包,然后一次性、有序地执行地机制。
多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。
事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。
带有WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关联,当键被修改时,程序会将所有监视被修改的客户端的REDIS_DIRTY_CAS标志打开。
只有在客户端的REDIS_DIRTY_CAS标志未被打开时,服务器才会执行客户端提交的事务,否则的话,服务器将拒绝执行客户端提交的事务。
redis的事务总是具有ACID中的原子性、一致性和隔离性,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务具有耐久性。

Lua脚本

redis从2.6版本开始引入Lua脚本的支持,通过在客户端中嵌入Lua环境,redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个redis命令。
使用EVAL命令可以直接对输入的脚本进行求值:
EVAL "return 'hello world'" 0 "hello wrold"
# script:执行的脚本
# numkeys:指定键名参数个数
# key:键名,可以多个(key1、key2),通过 KEYS[1] KEYS[2] 的形式访问
# atg:键值,可以多个(val1、val2),通过 ARGS[1] ARGS[2] 的形式访问
EVAL script numkeys key [key ...] arg [arg ...]
使用EVALSHA命令可以根据脚本的SHA1校验和来对脚本进行求值,但这个命令必须要求校验和对应的脚本必须至少被EVAL命令执行过一次.:
127.0.0.1:6381> script load "return 'hello'"
"1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
127.0.0.1:6381> evalsha "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b" 0

创建并修改Lua环境

为了在redis服务器中执行Lua脚本,redis在服务器内嵌了一个Lua环境(environ-ment),并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足redis服务器的需要。
redis服务器创建并修改Lua环境的整个过程由以下步骤组成:
	a)、创建一个基础的Lua环境1,之后的所有修改都是针对这个环境进行的。
	b)、载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库来进行数据操作。
	c)、创建全局表格redis,这个表格包含了对redis进行操作的函数,比如用于在Lua脚本中执行redis命令的redis.call函数。
	d)、使用redis自治的随机函数来替代Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用。
	e)、创建排序辅助函数,Lua环境使用这个辅佐函数来对上一部分redis命令的结果进行排序,从而消除这些命令的不确定性。
	f)、创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息。
		call遇到错误会抛出,pcall遇到错误会返回一个带错误的err。
	g)、对Lua环境中的全局环境进行保护,防止用户在执行Lua脚本的过程中,将额外的全局变量添加到Lua环境中。
	h)、将完成修改的Lua环境保存到服务器状态的lua属性中,等待执行服务器传来的Lua脚本。
if redis.call("EXISTS",KEYS[1]) == 1 then
     return redis.call("INCRBY",KEYS[1],ARGV[1])
   else
     return nil
   end
	如果存在传入key,则将key值+argv

lua 脚本传入多个参数

if redis.call("EXISTS",KEYS[1]) == 1 then
        return redis.call("get",KEYS[1])
else
        return redis.call("set",KEYS[1],KEYS[2])
end
local i = 10
local seq = {}
while (i > 0) do
	seq[i] = math.random(i)
	i = i - 1
end
return seq
生成随机数
eval "return redis.call('SMEMBERS',KEYS[1])" 1 fruit
eval "return redis.call('set',KEYS[1],ARGV[1]),redis.call('set',KEYS[2],ARGV[2])" 2 name age min 18
往数据库添加两个值
./redis-4.0.9/bin/redis-cli --eval test-redis.lua a
执行 test-redis.lua 脚本,a 是传参
redis服务器会将所有被EVAL命令执行过的Lua脚本,以及所有被SCRIPT LOAD命令载入过的Lua脚本都保存到lua_scripts字典里面。
EVAL命令的实现:
	a)、根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。
	b)、将客户端给定的脚本保存到lua_scripts字典,等待将来进一步使用。
	c)、执行刚刚Lua环境中定义的函数,以此来执行客户端给定的Lua脚本。
在执行EVAL命令时,服务器会先在Lua环境中定义函数 f_SHA1校验和的函数,然后通过EVALSHA SHA1校验和直接调用该函数。

脚本管理命令

a)、SCRIPT FULSH:清除服务器中的所有和Lua脚本有关的信息,会重建lua_scripts字典,关闭现有的Lua环境并重新创建一个新的Lua环境。
b)、SCRIPT EXISTS:根据输入的SHA1校验和,检查校验和对应的脚本是否存在于服务器(lua_scripts字典)中。允许一次传入多个SHA1校验和。
c)、SCRIPT LOAD:在Lua环境中为脚本创建相对应的函数,然后将脚本保存到lua_scripts字典里面。
d)、SCRIPT KILL:如果服务器设置了lua_time_limit配置选项,那么在每次执行Lua脚本之前,服务器都会在Lua环境里面设置一个超时处理钩子(hook)。超时处理钩子在脚本允许期间,会定期检查脚本以及运行了多长时间,一旦钩子发现脚本的运行时间以及超过了lua_time_limit选项设置的时长,钩子将定期在脚本运行的间隙中,查看是否由SCRIPT KILL命令或者SHUTDOWN命令到达服务器。如果超时运行的脚本未执行过任何写入操作,那么客户端可以通过SCRIPT KILL命令来指示服务器停止运行这个脚本,并向执行该脚本的客户端发送一个错误回复,处理完SCRIPT KILL命令之后,服务器可以继续运行。如果命令已知执行过写入操作,那么客户端只能用SHUTDOWN nasave命令来停止服务器,从而防止不合法的数据被写入数据库中。

脚本复制

当服务器运行在复制模式之下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包括EVAL、EVALSHA、SCRIPT FLUSH、SCRIPT LOAD命令。
redis复制EVAL、EVALSHA、SCRIPT LOAD三个命令的方法和复制其他普通redis命令的方法一样,当主服务器执行完以上三个命令的其中一个时,主服务器会直接将执行的命令传播给所有从服务器。
redis要求主服务器在传播EVALSHA命令的时候,必须确保EVALSHA命令要执行的脚本已经被所有从服务器载入过,如果不能确保这一点的话,主服务器会将EVALSHA命令转换为一个等价的EVAL命令,然后通过传播EVAL命令来代替EVALSHA命令。
主服务器使用服务器状态的repl_scriptcache_dict字典记录自己已经将哪些脚本传播给了所有从服务器:
struct redisServer {
    // ...
    dict *repl_scriptcache_dict;
    // ...
};
每当主服务器添加一个新的从服务器时,主服务器都会清空自己的repl_scriptcache_dict字典。因为随着新的从服务器出现,repl_scriptcache_dict记录的脚本已经不被所有从服务器载入过。
主服务器在执行完一个EVALSHA命令之后,通过EVALSHA命令指定的SHA1校验和是否存在于repl_scriptcache_dict来决定是否向从服务器传播EVALSHA命令还是EVAL命令。

重点回顾

redis服务器在启动时,会对内嵌的Lua环境执行一系列修改操作,从而确保内嵌的Lua环境可以满足redis在功能性、安全性等方面的需要。
redis服务器专门使用一个伪客户端来执行Lua脚本中包含的redis命令。
redis使用脚本字典来保存所有被EVAL命令执行过,或者被SCRIPT LOAD命令载入过的Lua脚本,这些脚本可以用于实现SCRIPT EXISTS命令,以及实现脚本复制功能。
EVAL命令为客户端输入的脚本在LUA环境中定义一个函数,并通过调用这个函数来执行脚本。
EVALSHA命令通过直接调用Lua环境中已定义的函数来执行脚本。
SCRIPT FLUSH命令会清空服务器lua_scripts字典中保存的脚本,并重置Lua环境。
SCRIPT EXISTS命令接受一个或多个SHA1校验和为参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
SCRIPT LOAD命令接受一个Lua脚本作为参数,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
服务器在执行脚本之前,会为Lua环境设置一个超时处理钩子,当脚本出现超时运行清空时,客户端可以通过向服务器发送SCRIPT KILL命令来让钩子通知正在执行的脚本,或者发送SHUTDOWN nosave命令来让钩子关闭整个服务器。
主服务器复制EVAL SCIPT、FLUSH SCRIPT、LOAD三个命令和复制普通redis命令一样,只有将相同的命令传播给从服务器就可以了。
主服务器在复制EVALSHA命令时,必须确保从服务器都已经载入了EVALSHA命令指定的SHA1校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将EVALSHA命令转换为等效的EVAL命令,并通过传播EVAL命令来获得相同的脚本执行效果。

排序

redis的sort命令可以对列表键、集合键或者有序集合键的值进行排序(只针对值为数字的键)。例:sort list、sort set、sort zset。如需对字母值的键排序需加上 alpha选项。例:sort list aplha。
服务器执行SORT key命令的详细步骤如下
	a)、创建一个和key列表长度相同的数组,该数组的每个项都是一个redis.h/redisSortObject结构

命令为排序key列表而创建的数组
b)、遍历数组,将各个数组项的obj指针分别指向key列表的各个项,构成obj指针和列表项之间的一对一关系。
在这里插入图片描述

	c)、遍历数组,将obj指针所指向的列表项转换成一个double类型的浮点数,并将这个浮点数保存在相应数组项的u.score属性里面。

在这里插入图片描述

	d)、根据数组项u,score属性的值,对数组进行数字值排序,排序后的数组项按u.score属性的值从小到大排序。

在这里插入图片描述

	e)、遍历数组,将各个数组项的obj指针所指向的列表项作为排序结果返回给客户端,程序首先访问索引0,返回u.socre值为1.0的列表项1,而后分为2,再访问3。
redisSortObject结构:
typedef struct _redisSortObject {
    //被排序键的值
    robj *obj;
    //权重
    union {
        //排序数字值时使用
        double score;
        //排序带有BY选项的字符串值时使用
        robj *cmpobj;
    } u;
} redisSortObject;
sort命令为每个被排序的键都创建一个与键长度相同的数组,数组的每个项都是一个redisSortObject结构,根据SORT命令使用的选项不同,程序使用redisSortObject的结构的方式也不同。
sort key asc  正序排列,默认正序
sort list desc  倒序排列

by选项

sadd fruits apple banana orange
mset apple 8 banana 6 orange 7
sort fruits by *-price 将fruits里的元素按字符串名字排序

带有aplha选项的by选项

mset apple-id fruit25 banana-id fruit79 orange-id fruit13
sort fruits by *-id alpha

limit选项

sort nums limit 0 4
sort fruits alpha limit 1 3

get选项

sadd names jark peter tom
mset jackname "jack snow" tomname "tom smith" petername "peter white"
sort names alpha get *name
可以带多个get选项

srote选项

sort只返回排序的结果,store可以将排序结构保存到指定键里面。
sort list alpha store list1
sort list1 alpha

多个选项的顺序执行

按照选项来划分,一个SORT命令的执行过程可以分为:
	a)、排序,使用ALPHA、ASC、DESC、BY这几个选项,对输入键进行排序。
	b)、限制排序结果集长度,使用LIMIT选项
	c)、获取外部键:使用GET选项。
	d)、保存排序结果集:使用STORE选项
	e)、向客户端返回
调用SORT命令。除了GET选项之外,改变选项的摆放顺序并不会影响SORT命令执行这些选项的顺序。

重点回顾

SORT命令通过将被排序键包含的元素载入到数组里面,然后对数组进行排序来完成对键排序的工作。
在默认情况下,SORT命令假设被排序键包含的都是数字值,并且以数字值的方式来进行排序
如果SORT命令使用了ALPHA选项,那么SORT命令假设被配许键包含的都是字符串键,并且以字符串的方式来进行排序。
SORT命令的排序操作由快速排序算法实现。
SORT命令会根据用户是否使用了DESC选项来决定时使用升序还是降序对比来比较被排序的元素。
当SORT命令使用了BY选项时,命令使用其他键的值作为权值来进行排序操作。
当SORT命令使用了LIMIT选项时,命令只保留排序结果集中LIMIT选项指定的元素。
当SORT命令使用了GET选项时,命令会根据排序结果集中的元素,以及GET选项给定的模式,查找并返回其他键,而不是返回被排序的元素。
当SORT命令使用了STORE选项时,命令会将排序结果集保存在指定的键里面。
当SORT命令同时使用多个选项时,命令先执行排序操作,然后LIMIT,之后GET,再之后执行STORE,最后才将排序结果集返回给客户端。
除了GET选项,调整选项的摆放位置不会影响SORT命令的排序结果。

二进制位数组

redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四个命令用于处理二进制数组(bit array,又称"位数组")。
SETBIT用于为位数组指定偏移量上的二进制位设置值,位数组的偏移量从0开始计数,而二进制的值则可以是0或1。setbit bit 0 1
GETBIT命令用于获取位数组指定偏移量上的二进制值。getbit bit 0
BITCOUNT命令用于统计位数组里面,值为1的二进制位的数量。bitcount bit
BITOP命令既可以对多个位数组进行按位与(and)、按位或(or)、按位异或(xor)运算。
bitop and and-result x y 与
bitop or or-result x y 或
bitop xor xor-result x y 异或
bitop not not-result x  取反

位数组的表示

redis使用字符串对象来表示位数组,因为字符串对象使用的SDS数据结构是二进制安全的,所以程序可以直接使用SDS机构来保存位数组,并使用SDS结果的操作函数来处理位数组。
图中展示了SDS表示的亿字节长的位数组:

在这里插入图片描述
redisObject.type的值位REDIS_SYRING表示这是一个字符串对象。
sdshdr.len的值为1,表示这个SDS保存了一个一字节长的位数组。
buf数组的buf[0]字节保存了一字节长的位数组。
buf数组中的buf[1]保存了SDS程序自动追加到值得末尾得空字符 ‘\0’。
在这里插入图片描述
buf得数组的每个字节都有一行来表示,每个的第一个格子buf[i]表示这是buf数组的哪个字节,而buf[i]之后的八个格子则分别代表这一字节中的八个位。
buf数组保存位数组的顺序和我们平时书写位数组的顺序是完全相反的。使用逆序来保存位数组九可以简化SETBIT命令的实现。

GEIBIT命令的实现

GETBIT命令用于返回位数组bitarray在offset偏移量上的二进制的值
GETBIT命令的执行过程如下:
	a)、计算byte=offset / 8,byte值记录了offset便宜量指定的二进制位保存在位数组的哪个字节。
	b)、计算bit=(offset % 8)+1,bit值记录了offset偏移量指定的二进制位是byte字节的第几个二进制。
	c)、根据byte值和bit值,在位数组bitarray中定位offset偏移量指定的二进制位,并返回这个位的值。

SETBIT命令的实现

SETBIT用于将位数组bitarray在offset偏移量上的二进制位的值设置位value,并向客户端返回二进制位被设置之前的旧值。
SETBIT执行过程:
	a)、计算len=offset / 8 +1,len值记录了保存offset偏移量指定的二进制位至少需要多少字节。
	b)、检查bitarray键保存的位数组(也即是SDS)的长度是否小于len,如果是的话,将SDS的长度扩展位len字节,并将所有新扩展的空间的二进制的值设置为0.
	c)	、计算byte=offset / 8 ,byte值记录了offset偏移量指定的二进制位保存在位数组的哪个字节。
	d)、计算bit=(offset % 8) + 1,bit值记录了offset偏移量指定的二进制位是byte的第几个二进制位。
	e)、根据byte值和bit值,在bitarray键保存的位数组中定位offset偏移量指定的二进制位,首先将指定二进制位现在值保存在oldvalue变量,然后将新值value设置为这个二进制的值。
	f)、向客户端返回oldvalue的值。

BITCOUNT命令的实现

BITCOUNT命令要解决的问题:统计一个位数组中非0二进制位的数量,在数学上被称为 计算汉明重量。
BITCOUNT命令用于统计给定位数组中,值为1的二进制位的数量。
二进制位统计算法:
	a)、遍历算法:遍历位数中的每个二进制位。
	b)、查表算法:创建一个哈希表,键为某种排列的位数组,值为相应位数组中,值为1的二进制的数量。
	c)	、variable-precision SWAR算法:通过一系列位移和位运算操作,可以在常数时间内计算多个字节的汉明重量。
		以下为处理32位长度位数组的variable-precision SWAR算法的实现:
uint32_t swar(uint32_t i) {
    //步骤1
    i = (i & 0x55555555) + ((i >> 1) & 0x55555555);
    //步骤2
    i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
    //步骤3
    i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);
    //步骤4
    i = (i*(0x01010101) >> 24);
    return i;
}
步骤1计算出值 i 的二进制表示可以按每两个二进制位位一组进行分组,各组的十进制表示就是该组的汉明重量。
步骤2计算出的值 i 的二进制表示可以按每四个二进制位一组进行分组,各组的十进制表示就是该组的汉明重量。
步骤3计算出的值 i 在二进制表示可以按每八个二进制位为一组进行分组,各组的十进制表示就是该组的汉明重量。
步骤4的0x01010101语句计算出bitarray的汉明重量并记录在二进制位的最高八位,而 >>24语句则通过右移运算,将bitarray的汉明重量移动到最低八位,得出的结构就是bitarray的汉明重量。
BITCOUNT命令的实现用到了查表和variable-precision SWAR两种算法:查表算法使用键长为8位的表,表中记录了从0000 0000 到 1111 1111在内的所有二进制的汉明重量。至于variable-precision SWAR算法,BITCOUNT命令在每次循环中载入128个二进制位,然后调用4此32位variable-precision SWAR算法来计算着128个二进制位的汉明重量。
在执行BITCOUNT命令时,程序会根据未处理的二进制位的数量来决定使用那种算法:如果未处理的二进制位的数量大于等于128位,使用variable-precision SWAR,小于则使用查表算法。

BITOP命令的实现

基于C语言对字节执行逻辑与、逻辑或、逻辑异或	和逻辑非操作。

重点回顾

redis使用SDS来保存位数组
SDS使用逆序来保存位数组,这种保存顺序简化了SETBIT命令的实现,使得SETBIT命令可以在不移动现有二进制位的情况下,对位数组进行空间扩展。
BITCOUNT命令使用查表算法和variable-precision SWAR算法来优化命令的执行效率。
BITOP命令的所有操作都是使用C语言内置的位操作来实现。

慢查询日志

reis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。
服务器配置有两个慢查询日志的选项:	
	a)、slowlog-log-slower-than 指定执行时间超过多少秒的命令请求会被记录到日志上。
	b)、slowlog-max-len 指定服务器最多保存多少条慢查询日志
服务器使用先进先出的方式保存多条慢查询日志,当服务器存储的慢查询日志等于slowlog-max-len选项的值时,服务器在添加一条新的慢查询日志之前,会先将最久的一条慢查询日志删除。
可通过命令或者配置文件指定这两个值
CONFIG SET slowlog-log-slower-than 0
CONFIG SET slowlog-max-len 5
可通过命令SLOWLOG GET查看慢查询日志。
SLOWLOG LEN  查看慢查询日志的数量。

慢查询日志记录的保存

redis服务器状态中包含了几个和慢查询日志功能有关的属性:
struct redisServer {
    // ...
    //下一条慢查询日志的ID
    long long slowlog_entry_id;
    //保存了所有慢查询日志的链表
    list *slowlog;
    //服务器配置slowlog-log-slower-than选项的值
    long long slowlog_log_slower_than;
    //服务器配置slowlog-max-len选项的值
    unsigned long slowlog_max_len;
    // ...
};
slowlog_entry_id属性的初始值为0,每当创建一条新的日志,这个属性值就会用作新日志的id值,之后程序会对这个属性值增一。
slowlog链表保存了服务器中的所有慢查询日志,链表中的每个节点都保存了一个slowlogEntry结构,每个slowlogEntry结构代表一条日志:
typedef struct slowlogEntry {
    //唯一标识符
    long long id;
    //命令执行时的时间,格式为UNIX时间戳
    time_t time;
    //执行命令消耗的时间,以微秒为单位
    long long duration;
    //命令与命令参数
    robj **argv;
    //命令与命令参数的数量
    int argc;
} slowlogEntry;
SLOWLOG RESET 可用于清除所有慢查询日志。

重点回顾

Redis的慢查询日志功能用于记录执行时间超过指定时长的命令。
Redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,每个slowlogEntry结构代表一条慢查询日志。
打印和删除慢查询日志可以通过遍历slowlog链表来完成。
slowlog链表的长度就是服务器所保存慢查询日志的数量。
新的慢查询日志会被添加到slowlog链表的表头,如果日志的数量超过slowlog-max-len选项的值,那么多出来的日志会被删除。

监视器

通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息。
每当客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将关于这条命令请求的信息发送给所有监视者。
客户端向服务器发送MONITOR命令,那么这个客户端的REDIS_MONITOR标志会被打开,并且这个客户端本身会被添加到monitors链表的表尾。

重点回顾

客户端可以通过执行MONITOR命令,将客户端转换成监视器,接收并打印服务器处理的每个命令请求的相关信息。
当一个客户端从普通客户端变为监视器时,该客户端的REDIS_MONITOR标识会被打开。
服务器将所有监视器都记录在monitors链表中。
每次处理命令请求时,服务器都会遍历monitors链表,将相关信息发送给监视器。





















。8
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值