redis要点总结

redis是一个非常优秀的内存数据库,有着非常广泛的应用。本文接下来对redis的要点和原理做一个总结。
下面内容总结自《Redis设计与实现》。

一、数据结构

1、字符串

redis是使用C语言编写的,但是它没有使用C语言的字符串表示方式,而是内部创建了一个名为“SDS”(简单动态字符串)的数据结构来表示字符串。在redis中,键总是使用字符串表示。
字符串使用下面的数据结构表示:

struct sdshdr{
	int len;//字符串长度,也是buf数组中已使用字节数
	int free;//buf数组中未使用字节数
	char buf[];//字节数组,存储字符串,字符串以'\0'结尾,但是不计入字符串长度
}

SDS相对于C语言的字符串优势有:

  1. SDS记录了字符串长度,相对于C语言的字符串来说,不用遍历可以直接获取字符串长度;
  2. 当字符串过长,buf数组不能保存时,SDS会自动扩充数组大小;
  3. 字符串长度增加,buf数组的空间不足以保存字符串时,SDS会扩充buf数组长度,扩充时SDS不仅分配保存字符串所需要的空间,而且还会多分配额外的空闲空间以备以后使用,这样无需频繁的申请空间来保存字符串。当len的值小于1MB时,每次扩充都会分配和len同样大小的空闲空间,这意味着扩充后,len和free的值是相同的;当len的长度大于等于1MB时,SDS每次会固定分配1MB的空闲空间。
  4. 字符串长度缩短时,SDS不会重新分配空间来释放多出来的未使用空间,而是增加free的值,多出来的空间在将来字符串长度增加时使用。
  5. SDS是二进制安全的,redis认为buf数组保存的不是字符而是二进制数据,redis按照处理二进制数据的方式处理buf数组,这样带来的好处是,buf数组不仅可以保存文本数据,而且还可以保存图片、视频等数据,使得SDS可以适用于各种不同的场景。

2、链表

链表可以用于实现列表键。当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,redis便会使用链表这种数据结构实现列表键。
链表的节点使用listNode表示:

typedef struct listNode{
	struct listNode *prev;//前置节点
	struct listNode *next;//后置节点
	void *value;//节点数据
}listNode;

然后使用list来持有链表:

typedef struct list{
	listNode *head;//表头
	listNode *tail;//表尾
	unsigned long len;//链表长度
	//下面三个指针指向的是函数,用于多态实现
	void *(*dup)(void *ptr);//节点值复制函数
	void (*free)(void *ptr);//节点值释放函数
	int (*match)(void *ptr,void *key);  //节点值比较函数
}list;

redis的链表是一个双端无环链表。

3、字典

字典这种数据结构在redis中有非常广泛的使用,比如redis数据库就是字典作为底层数据结构实现的。哈希键也是字典实现的,当哈希键包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,redis就会使用字典实现哈希键。
字典使用哈希表作为底层实现。redis的哈希表与java7中的HashMap实现类似,其结构如下:
在这里插入图片描述
上图中table后面的哈希节点链表是用于解决键冲突使用的。每个哈希节点都有一个next指针,用于指向链表中后一个节点。
当字典中要存储一个键值对时,首先使用MurmurHash2算法计算出键的哈希值,然后将哈希值%table数组长度得到的便是该键值对在table数组的下标,如果发生了冲突,则将键值对放到链表的表头位置。
再哈希
当哈希表的负载因子小于0.1时,哈希表执行收缩操作;当哈希表的负载因子大于等于1(redis目前没有执行BGSAVE或者BGREWRITEAOF命令)或者5(redis正在执行BGSAVE或者BGREWRITEAOF命令)时,哈希表执行扩展操作。
执行再哈希时,redis首先创建一个新的哈希表,如果是扩展操作,新哈希表的大小为第一个大于等于原哈希表节点数量2倍的2^n,如果是收缩操作,新哈希表的大小为第一个大于等于原哈希表节点数量的2^n。之后将原哈希表中的节点再哈希到新哈希表上,待所有节点迁移完之后,使用新哈希表替换原哈希表,释放原哈希表空间。再哈希结束。
redis再哈希不是一次性完成的,而是分多次、渐进式完成的。每次对哈希表执行增删改查时,redis将其中一个桶的链再哈希到新哈希表中。这样随着增删改查不断进行,最终所有的元素都被移动到新哈希表中。在两个哈希表共存的这段时间,每次删改查都会在两个哈希表上进行。另外,新添加的键值对一律保存到新哈希表中。

4、跳跃表

跳跃表是一种有序的数据结构,使用跳跃表查找元素,平均时间复杂度是O(logN),最坏O(N)。
跳跃表可以用于实现有序集合键。如果一个有序集合包含的元素比较多,或者元素是比较长的字符串时,redis使用跳跃表实现有序集合键。
跳跃表根据score(double类型)将数据从小到大排序,数据保存在obj属性中,在跳跃表中,保存的数据只能是SDS。跳跃表不允许有相同的obj值存在,但是允许有相同score,当数据的score相同时,按照obj的顺序排序。

5、整数集合

整数集合用于实现集合键,当集合中只包含整数值元素,且元素数量不多时,redis使用整数集合实现集合键。
整数集合用于保存int16,int32,int64三种类型的整数。集合定义如下:

typedef struct intset{
	uint32_t encoding;//指示contents数组中的整数是三种类型中的哪一种
	uint32_t length;//元素个数
	int8_t contents[];//保存元素的数组
}

整数集合中的数据在contents数组中按照从小到大的顺序排序,且不包含重复数据。
contents可以保存三种类型的数据,具体数据是哪一种类型是redis根据被保存数据的大小自动确定的。当要保存的数据大小都在-32768和32767之间时,也就是int16类型数据,那么contents数组的元素类型都是int16,每个数据占2个字节,如果数据在-2147483648和2147483647之间时,那么contents数组的元素类型都是int32,每个数据占4个字节。如果一开始数组中的数据都是int16类型,现在要插入一个int32类型数据,那么redis首先将contents数组扩长,并将原来里面的元素全部转换为int32类型,最后插入新元素。这个过程叫做整数集合的升级。不过,整数集合升级了之后,无论里面的元素如何变化,整数集合都不会降级。

6、压缩列表

当一个列表键只包含少量的元素或者每个元素要么是小整数值要么是比较短的字符串,那么该列表键底层使用压缩列表实现。同样的哈希键如果也满足上述要求,那么哈希键底层也是压缩列表实现。
压缩列表使用一段连续的空间存储元素,每个元素可以是字符串或者是整数,其结构如下:
在这里插入图片描述
各个字段说明如下:

  • zlbytes表示整个压缩列表占用的字节数;
  • zltail表示压缩列表的表尾节点距离压缩列表起始地址有多少个字节;
  • zllen记录压缩列表元素个数,当元素个数小于65535时,该属性记录的是真实的元素个数,如果该属性等于65535,那么只能通过遍历列表才能知道元素个数;
  • entryX表示各个列表的节点;
  • zlend表示列表的结尾,占一个字节,值为0xFF。

每个entryX由以下结构组成:
在这里插入图片描述
其中previous_entry_length表示前一个节点占用的字节数;encoding记录节点数据的类型和长度,因为每个节点可以保存整数或者字符串,因此使用encoding字段的前两位作类型区分,后面的内容表示数据长度;content用于记录节点数据。

7、对象

每新建一个redis键值对,redis都会创建对应的对象来保存这些数据。redis一共有5种类型的对象:集合对象、字符串对象、哈希对象、有序集合对象、列表对象。
redis中,对象的结构如下:

typedef struct redisObject{
	unsigned type:4;//类型
	unsigned encoding:4;//编码
	void *ptr;//指向底层实现的数据结构,也就是指向保存数据的位置
	....
}robj;
  • type属性描述了对象是五种对象的哪一种,redis提供了TYPE命令可以查看键值对中的值是哪一种对象。
  • encoding属性描述了对象底层使用的是哪种数据结构,redis提供了8种数据结构:long类型整数、embstr编码的SDS、SDS、字典、双向链表、压缩列表、整数集合、跳跃表和字典。每种对象至少使用了两种以上的数据结构。OBJECT ENCODING命令可以查看值使用的是哪种数据结构。

下面具体介绍每种对象可以使用哪些数据结构,以及这些数据结构的使用场景。

(1)字符串对象

字符串对象的数据结构可以是long类型整数、embstr编码的SDS、SDS三种。
如果字符串对象保存的是整数值,且可以用long类型表示,那么字符串对象便将整数值保存在ptr属性中,数据结构使用long类型整数;
如果字符串对象保存的是一个字符串,且长度小于等于32字节,那么字符串对象使用embstr编码的SDS保存该字符串;
除了上述两种之外的场景,字符串对象都使用SDS保存字符串。
embstr编码的SDS与SDS的区别是内存分配方式上的不同,embstr编码要求一次性分配一段连续的空间用于保存redisObject和sdshdr,而SDS需要两次内存分配,一次分配空间用于保存redisObject,另一次用于保存sdshdr。除此之外,两种SDS没有区别。
浮点数在redis中也是使用字符串存储的,使用SDS或者embstr编码存储,redis会在需要的时候将字符串转换为浮点数值,执行完操作后,再将其转换为字符串。比如对浮点数执行加减时,redis首先将其转换为数值,计算后将结果再转换为字符串存储。
字符串的三种数据结构之间可以转化:
1、当整数对象不再是纯数字或者超过了long类型表示的范围,整数对象转化为SDS存储;
2、embstr编码的字符串是只读的,也就是说这种类型的字符串不支持修改,如果要修改,需要先将其转化为SDS,然后再修改,所以embstr编码的字符串创建后,只要对其执行修改,它就变为SDS。

(2)列表对象

列表对象可以使用压缩列表或者双向列表存储数据。当列表对象保存的所有字符串对象长度都小于64字节,且元素个数小于512,那么列表对象底层使用压缩列表,否则使用双向列表。当条件不满足时,压缩列表也会转换为双向列表表示。

(3)哈希对象

哈希对象可以使用压缩列表和字典存储数据。如果使用压缩列表表示,每次添加新键值对时,都会将其放在压缩列表的表尾。
当哈希对象保存的所有键值对的键和值的字符串对象长度都小于64字节,且键值对个数小于512,哈希对象使用压缩列表表示,否则使用字典存储数据。当条件不满足时,压缩列表也会转换为字典表示。

(4)集合对象

集合对象可以使用整数集合和字典存储数据。如果使用字典存储集合对象,字典的键用于保存集合元素,值全部设置为null。
当集合中元素都是整数值,且元素数量不超过512,集合对象底层使用整数集合表示,否则使用字典。当条件不满足时,整数集合会转换为字典表示。

(5)有序集合对象

有序集合对象可以使用压缩列表和跳跃表存储数据。
当使用压缩列表存储时,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的值,第二个节点保存元素的score,压缩列表中的数据按照score从小到大排序。
当使用跳跃表存储数据时,还要搭配一个字典一起使用,增加字典是为了快速查找元素的score值,在字典里面,元素值作为键,score作为value。
当有序集合中的元素个数小于128,且每个元素的长度都小于64字节时,有序集合对象底层使用压缩列表,否则使用跳跃表。当条件不满足时,压缩列表也会转换为跳跃表表示。

redis将数据封装成对象之后,就可以基于对象来管理内存。redisObject结构中有一个属性refCount记录了对象的引用计数信息,redis通过该值是否为0判断是否释放对象并回收内存。redis中的对象也可以共享,不过目前只能共享整数值类型的字符串对象,除此之外的对象都不能共享。redis启动时,默认创建一万个整数值类型的字符串对象,这些对象包含了从0到9999的所有整数值,当redis需要用到0到9999之间的字符串对象时,就可以直接使用共享对象,而不用新建对象。每共享一次对象,refCount就会加1。
redisObject结构中还提供另一个属性:lru。该属性记录了对象最后一次被命令程序访问的时间。命令OBJECT IDLETIME可以打印对象已经有多长时间未被访问了,这个时间就是使用当前时间减去lru得到的,使用命令OBJECT IDLETIME时不会更改lru值。lru有一个非常重要的作用是,当redis占用内存超过maxmemory设定值时,redis可以根据lru值释放对象占用的内存。

二、数据库

redis启动时默认创建16个数据库,数据库的下标从0开始。我们可以通过参数database来修改默认创建的数据库个数。
数据库之间是完全隔离的,命令只在指定的数据库中执行。默认情况下,redis客户端使用0号数据库,可以用过SELECT命令切换到其他数据库。比如:

SELECT 2   --可以切换到2号数据库

redis的数据库底层都是使用字典实现的,我们在数据库上执行的任何操作最终都是在字典上执行。
redis可以对键设置过期时间,当到了指定的时间(EXPIREAT)或者过了指定的时间(EXPIRE),redis会将该键值对从数据库中删除。 为了更好的管理键的过期时间,redis在数据库中创建了一个过期字典,过期字典底层结构为字典,用于保存键和过期时间之间的对应关系,其中过期时间为键的过期时间点。 对于已经过期的键,redis采用两种方式删除:惰性删除和定期删除。 惰性删除指的是当访问该键时,检查该键是否过期,如果过期则将其删除。redis中任何读取键的命令执行前都会先检查键是否已过期。 惰性删除只有等到访问键的时候,才去检查键是否过期,如果键已经过期但是始终未被访问,那么键会始终占据内存不释放,这便造成内存泄漏,因此redis提供了定期删除策略。 定期删除策略是每过一段时间,redis随机检查过期字典中一部分键的过期时间,并将过期键删除。定期删除每次不会检查所有的键,而是从每个数据库的过期字典中随机选择一部分键检查,这样可以防止定期删除长时间占用CPU。

三、RDB

redis提供了RDB持久化功能,可以将数据库中的数据保存到RDB文件做持久化保存。RDB文件是一个经过压缩的二进制文件。redis重启时会自动读取RDB文件以还原内存数据。RDB保存数据时,将redis中所有数据库都进行保存而不是只保存某一个数据库。
执行RDB持久化,可以通过SAVE和BGSAVE命令。SAVE命令会阻塞redis进程,直到RDB文件创建完毕,在阻塞期间,redis拒绝所有的命令请求。BGSAVE命令则不同,它在后台启动一个子进程,然后由子进程负责RDB持久化,父进程继续处理命令请求。载入RDB文件,只能发生在redis重启,redis重启时检查是否有RDB文件,如果有则自动载入。在载入期间,redis不接受任何请求。
尽管BGSAVE命令执行期间可以处理其他命令,但是在BGSAVE命令执行期间,如果客户端发送SAVE、BGSAVE和BGREWRITEAOF三个命令,redis的处理方法会有所不同。对于前两个命令,redis直接拒绝执行。BGREWRITEAOF命令会被延迟到BGSAVE执行结束后执行。不过如果在BGREWRITEAOF命令执行期间,客户端发送过来BGSAVE命令,redis会直接拒绝。
除了通过命令执行之外,redis还可以通过save参数设置redis自动执行BGSAVE命令。redis服务器默认每100ms启动一个定时任务,定时任务首先检查当前是否满足save参数的要求,满足save参数中的任何一项都会触发BGSAVE命令的执行。
RDB文件内容按照键值对的格式保存数据,键的过期时间也会保存。RDB文件相当于是对内存的直接拷贝。
对于过期键,RDB文件保存数据时,如果键已过期则不保存,从文件加载数据到内存时,如果键已经过期,则不加载到内存。

四、AOF

AOF也是一种持久化功能,与RDB不同的是,AOF是将写命令保存到文件中。启动时,redis如果发现有AOF文件,那么redis会忽略RDB文件直接使用AOF文件还原内存数据。与RDB一样,AOF文件也是保存所有的数据库数据。
redis收到写命令后,首先执行写命令,之后将命令保存在缓冲区,每过一段时间redis将缓冲区的数据保存到AOF文件。redis还提供了参数appendfsync来决定何时做文件同步。
因为AOF文件里面保存的是完整的写命令,redis重启读取AOF文件时,直接执行文件里面的命令就可以还原内存数据。
因为AOF文件保存写命令,随着redis运行时间越来越长,那么AOF文件会非常大,为了防止AOF文件过大,redis提供了AOF重写功能。AOF重写的原理是读取现在数据库中的键值对,然后用一条命令记录该键值对,代替之前记录的这个键值对的多条命令。通过AOF重写可以大大减小文件的大小。AOF重写可以通过命令BGREWRITEAOF触发也可以由后台自动触发。无论使用哪一种,它们的处理方式都是一样的。下面先介绍一下redis执行AOF重写的过程。
当开始AOF重写时,redis创建一个子进程,AOF重写的工作完全交给该子进程完成,父进程继续接收客户端请求,创建子进程的同时redis还会创建一个AOF重写缓冲区,用于保存在重写期间父进程执行过的写命令。在重写期间,原AOF文件正常保存服务器执行的写命令。当子进程完成AOF重写后,向父进程发送一个信号,父进程收到该信号后,调用一个处理函数,然后完成以下两个工作:

  1. 将AOF重写缓冲区的所有内容写入到新的AOF文件中;
  2. 对新的AOF文件改名,原子的覆盖现有的AOF文件,完成新旧AOF文件的替换。

在这个处理函数执行期间,redis会拒绝所有的客户端请求,等到执行结束后,AOF重写的工作就全部完成了,父进程可以正常处理客户端请求了。
下面介绍一下AOF自动执行重写的条件。

服务器在AOF功能开启的情况下,会维持以下三个变量:
1、记录当前AOF文件大小的变量aof_current_size。
2、记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size。
3、增长百分比变量aof_rewrite_perc。
每次当serverCron(服务器周期性操作函数,默认100ms执行一次)函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
1、没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
2、没有BGREWRITEAOF在进行;
3、当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
4、当前AOF文件大小和最后一次重写后的大小之间的比率等于或者大于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)
如果前面三个条件都满足,并且当前AOF文件大小比最后一次AOF重写时的大小要大于指定的百分比,那么触发自动AOF重写。

对于过期键,当以AOF模式保存数据时,如果数据还没有被惰性或者定期删除,那么数据会被正常保存到AOF文件中,当键被惰性或者定期删除时,redis在AOF文件中新增一条DEL命令用以删除该过期键。AOF重写时,会检查键是否过期,如果已经过期不会将其写入文件。

五、事件

redis要处理的事件分为两种:文件事件和时间事件。文件事件指的是服务端与客户端交互产生的事件,文件事件可以理解为服务器要执行的命令;时间事件是redis定时要执行的操作。

1、文件事件

文件事件由文件事件处理器处理,文件事件处理器分为四个部分:套接字、IO多路复用程序、文件事件分派器、事件处理器。文件事件处理器是单线程运行。
redis使用了IO多路复用技术同时监听多个套接字,当套接字准备好连接、写入、读取、关闭等操作时,IO多路复用程序将套接字转发给文件事件分派器,因为套接字可能有多个同时准备好,redis在IO多路复用程序和事件分派器之间创建了一个队列,IO多路复用程序将准备好的套接字放到一个队列中,文件事件分派器每处理完一个套接字(该套接字为事件锁关联的事件处理器执行完毕),便会从队列中取出下一个套接字继续处理。文件事件分派器接收到套接字之后,根据套接字的事件类型调用相应的事件处理器。事件处理器定义了某个事件发生后,服务器应该执行的动作,常用的事件处理器有连接应答处理器、命令请求处理器、命令回复处理器。

2、时间事件

redis的时间事件分为两种:定时事件和周期性事件。目前redis中只有周期性事件,还没有使用定时事件。
redis中最重要的时间事件是serverCron。默认serverCron每100毫秒运行一次,可以通过hz参数调整serverCron每秒执行次数。serverCron主要完成的工作有:

  • 更新服务器的各类统计信息,比如内存占用、数据库占用情况等;
  • 清理数据库中的过期键值对;
  • 关闭和清理失效的连接;
  • 尝试进行AOF和RDB持久化;
  • 与从服务器定期同步;
  • 如果处于集群模式,对集群进行定期同步和连接测试。

文件事件和时间事件是在一个线程中串行处理的,redis先处理文件事件,之后处理已经到达的时间事件。文件事件和时间事件之间不会抢占CPU。

六、复制

我们可以设置slaveof参数或者执行SLAVEOF命令,让一台服务器复制另一台服务器的数据,其中被复制的服务器称为主服务器,执行复制的服务器称为从服务器。主服务器可读可写,从服务器只可以读,不可以写。
复制的过程分为两部分:同步和命令传播。同步是从服务器更新数据至主服务器当前的状态,同步完成后进入命令传播阶段。命令传播是主服务器执行写命令后数据发生变化,主服务器发送消息通知从服务器也进行改变的过程。主从服务器数据同步完成后,就进入了命令传播阶段,命令传播阶段相对比较简单,主服务器将执行过的写命令发送给从服务器,从服务器接收到命令在本地数据库中执行,这样主从服务器又一次保持了一致。下面重点介绍一下同步过程。
发生同步的情况分为两种,一种是初次复制,这种是从服务器以前没有复制过该主服务器,是第一次复制该主服务器的数据;另一种是断线后重复制,这种是在命令传播阶段阶段,从服务器与主服务器断开后再次重新连上。
初次复制

  1. 从服务器向主服务器发送PSYNC命令;
  2. 主服务器收到PSYNC命令后,执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行过的所有写命令;
  3. 当主服务器执行完BGSAVE命令后,主服务器将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收到RDB文件后,读取文件内容,更新数据库数据;
  4. 主服务器将缓冲区里面的所有命令发送给从服务器,从服务器执行这些写命令,将自己数据更新到主服务器的状态。

初次复制的代价比较高,因为需要生成RDB文件,并且通过网络传输,从服务器再读取文件,这个过程需要耗费大量的CPU、内存和网络资源。而且从服务器在读取RDB文件的过程中,也无法处理客户端命令请求。
因此断线后重复制采用了与初次复制完全不同的流程。
断线后重复制
断线后从服务器重新连上主服务器,不会重新完全同步主服务器,而是从上次断开的位置开始,将这之后主服务器执行过的写命令在从服务器上执行一遍。
断线后重复制由以下三部分组成:

  1. 复制偏移量,主从服务器分别记录各自的复制偏移量;
  2. 主服务的复制积压缓冲区;
  3. 服务器的ID。

主从服务器会记录各自的一个复制偏移量,主服务器每次向从服务器发送N个字节的数据,就将自己的复制偏移量的值加上N,同样的,从服务器接收到主服务器的N个字节数据,也将自己的复制偏移量的值加上N。这样主从服务器之间可以根据复制偏移量是否相等判断当前数据是否一致:如果两个机器上的偏移量相等,则说明数据是一致的,无需同步,否则需要同步。
复制积压缓冲区是一个固定长度的FIFO队列,当处于命令传播阶段时,主服务器除了将写命令发送给所有的从服务器之外,还会发送给复制积压缓冲区。复制积压缓冲区默认大小是1M,当写命令超过1M后,最先入队的命令会被弹出,新命令被放入队列。积压缓冲区除了存放最近主服务器执行过的所有写命令之外,还会记录每个写命令对应的复制偏移量。这样当从服务器断线重连后,主服务器检查积压缓冲区,如果从服务器的复制偏移量在积压缓冲区中存在,那么主服务器直接将积压缓冲区中的命令发送给从服务器,如果不存在,那么主从服务器只能执行初次复制的流程了。
每个redis服务器启动的时候都会自动生成一个ID,这个ID由40个随机十六进制的字符组成,每个服务器是唯一的。主服务器可以根据ID判断从服务器是第一次复制数据还是断线后重连。
下面介绍一下断线后重复制的完整过程。

  1. 从服务器连接上主服务器后,发送PSYNC命令,PSYNC命令还带有两个参数:上次连接过的主服务器ID和从服务器当前的复制偏移量;
  2. 主服务器会返回下面两种情况中的一种:一种是返回“+FULLRESYNC ID 偏移量”,ID指的是主服务器ID,偏移量指的是当前主服务器的偏移量,该返回值表示主从之间执行初次复制流程,从服务器接收到该返回值后,记录下上述两个参数值,并开始初次复制;另一种是返回“+CONTINUE”,这个表示主从之间开始断线后重复制流程,从服务接下来便等着主服务器将自己缺少那部分数据发送过来。

在命令传播阶段,从服务器默认每秒一次的频率向主服务器发送心跳。心跳有三个作用:

  1. 检测主从服务器之间的网络连接状态,如果主服务器超过1s没有收到心跳,那么意味着主从之间的网络连接出现问题;
  2. 辅助实现min-slaves-to-write和min-slaves-max-lag参数,min-slaves-to-write表示最少需要多少台从服务器,min-slaves-max-lag表示主从之间最大允许延迟多少秒,当不满足上述两个参数的要求时,主服务器将拒绝执行写命令;
  3. 检测命令丢失,发送心跳的时候,心跳信息里面携带了从服务器的复制偏移量,主服务器收到复制偏移量之后,如果从服务器的复制偏移量与主服务器的不同,说明命令有丢失,那么主服务器从复制积压缓冲区里面找到丢失的命令并发送给从服务器。

对于过期键,主服务器检测到键过期并将其删除后,会向所有的从服务器发送一条DEL命令,告知从服务器删除该过期键。如果从服务器检测到键已经过期,从服务器不会删除该键,而是把它当做未过期键处理,比如GET命令,即使键已经过期,从服务器把它当做未过期键正常返回键值。从服务器删除过期键只能通过主服务器通知才可以删除。不过这一点从Redis3.2版本已经修复了,从Redis3.2版本开始,从服务器读取键值对时,要检查键是否过期,如果已经过期不会返回客户端。

七、sentinel

sentinel是redis的高可用解决方案。sentinel可以监控主服务器和从服务器,当主服务器下线后,sentinel可以自动选择某一个从服务器升级为主服务器,然后对外提供服务。下线的主服务器如果再次上线,sentinel会将它设置为从服务器。sentinel也可以集群部署。
sentinel与redis不同,sentinel只能通过客户端执行7个命令,其他的命令sentinel都无法执行。
sentinel内部使用一个masters字典记录所有被监控的主服务器信息,其中字典的键时主服务器名字,字典值是主服务器的信息,包括了主服务器IP端口、ID、等信息,masters字典主要是根据sentinel的配置文件做初始化。sentinel启动时候,会根据主服务器配置信息创建与主服务器之间的网络连接。
sentinel建立与主服务器的连接后,默认每十秒向主服务器发送INFO命令,通过INFO命令返回值获取主服务的当前信息,其中很重要的一项是获取主服务器下的从服务器信息,这样sentinel可以自动发现从服务器。当sentinel发现新的从服务器后,便创建与从服务器之间的连接,默认情况下,sentinel每十秒也会向从服务器发送INFO命令,然后根据INFO命令返回值更新sentinel记录的从服务器信息。
sentinel除了可以自动发现从服务器之外,还可以自动发现监控同一个主服务器的其他sentinel实例,这是通过订阅频道__sentinel__:hello实现的。所有的sentinel实例都会订阅频道__sentinel__:hello,默认sentinel每两秒向所有服务器的__sentinel__:hello频道发送一个PUBLICSH命令(每个sentinel与主从服务器之间有两个连接,一个是命令连接,一个是订阅连接),该命令里面携带了sentinel本身的信息,包括IP、端口、ID等,其他的sentinel之后从该频道接收到信息,这样便可以获得其他sentinel的信息了。sentinel中有一个sentinels字典,字典中保存了所有其他sentinel的信息,每次从频道里面得到其他sentinel的信息后,便会更新sentinels字典的数据。sentinel发现有新sentinel之后,还会创建一个新sentinel之间的连接,这样监控同一个主服务器的sentinel之间便建立了互相连接,与sentinel与redis之间的连接不同,sentinel之间只建立了一个命令连接。
主观下线
默认情况下,sentinel每一秒向所有建立命令连接的实例发送PING命令,然后根据收到的回复判断实例是否在线。配置文件中有一个参数down-after-milliseconds,该参数指定了多长时间未收到PING命令的回复或者有效回复,便认为实例已经进入主观下线。
客观下线
当sentinel认为主服务器已经主观下线,它然后会向所有的监视同一个主服务器的其他sentinel进行询问,看他们是否也认为主服务器已经主观下线,当有足够多数量(配置文件quorum参数指定了具体sentinel的数量)的sentinel认为主服务器已经主观下线之后,sentinel便将主服务器的状态改变为客观下线,下面便开始执行故障转移。
leader选举
当sentinel认为主服务已经客观下线之后,便开始故障转移。故障转移前,要先选举出sentinel的leader。
sentinel认为主服务器已经客观下线了之后,向所有其他的sentinel发送选举命令,命令里面携带了自己的ID,如果其他sentinel是第一次接收到该命令,那么便认为该sentinel为局部leader,然后这些机器会拒绝后面收到的选举命令。当超过半数的sentinel认为某个sentinel是局部leader,那么该sentinel便成为全局leader。leader选举结束。
leader选举结束之后,还要修改配置纪元。每选举一次,配置纪元增加1,配置纪元只是一个计数器,用于标示某次选举。
故障转移
选举出的leader在从服务器里面挑选出一台机器作为主服务器,然后向这个从服务器发送SALAVEOF no one命令。从服务器收到命令后将自己切换为主服务器。
选举主服务器的规则:

  • 去除已经下线或者断线的从服务器;
  • 去除最近5s内没有回复leader INFO命令的从服务器;
  • 去除与主服务器断开时间过长的从服务器;
  • 根据剩下机器的slave-priority值排序,选出优先级最高的作为主服务器;
  • 如果有多个机器具有相同的最高优先级,那么将这些优先级最高的机器按照复制偏移量排序,复制偏移量最大的作为主服务器;
  • 如果这时还有多个机器最高优先级相同、复制偏移量相同,那么接下里根据ID排序,ID最小的作为主服务器。

主服务器选举完成之后,接下来sentinel向其他的从服务器发送SLAVEOF命令,将其他的从服务器变为新主服务器的从服务器。
当旧的主服务器重新上线之后,sentinel向该机器发送SLAVEOF命令,将它变为新主服务器的从服务器。

八、配置文件

windows机器上启动redis,需要手工指定启动配置文件,一般使用redis.windows.conf文件。
下面只对redis.windows.conf配置文件中一部分配置做介绍。

  • 默认AOF是关闭的,可以通过appendonly yes开启AOF;
  • 默认RDB是开启的,可以通过save ""关闭RDB;
  • save 900 1:save命令用于设置自动触发RDB的条件,前面这个命令表示距离上次保存时间超过900s,且发生过至少一次数据修改,就启动RDB保存;
  • appendfsync :设置AOF模式下,AOF文件同步的策略;
  • appendfilename:设置AOF文件名;
  • dbfilename :设置RDB文件名;
  • dir:工作目录,指定RDB/AOF文件的目录位置,只能为目录不能为文件名;
  • slaveof:当前机器为从服务器,设置从主服务器同步数据;
  • maxmemory:设置最大允许使用的内存,如果超过了,redis使用一些策略清理内存数据;
  • hz:serverCron每秒执行次数;
  • slave-priority:标示slave的权重值,默认100。当master失效后,Sentinel将会从slave列表中找到权重值最低(>0)的slave,并提升为master。如果权重值为0,表示此slave为"观察者",不参与master选举;
  • slave-read-only:设置slave机器是否只读,默认是yes,也就是只读,可以设置no,这时slave允许写入,但是一旦与主服务器发生同步,这些数据就会从salve中删除。

参考文章

https://blog.csdn.net/hezhiqiang1314/article/details/69396887
redis命令:http://redisdoc.com/topic/sentinel.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值