Redis设计与实现 读书笔记

redis 数据结构

Redis数据库里面的每个键值对(key-value pair)都是由对象(object)组成的,其中:
键总是一个字符串对象(string object),

值则可以是字符串对象(string object)、列表对象(list object)、哈希对象(hash object)、集合对象(set object)、有序集合对象(sorted set object)这五种对象中的其中一种.

字符串

自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,可以被修改的字符串值时,Redis就会使用SDS来表示字符串值.

SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区.

字符数组最后一个字节则保存了空字符’\0’

redis 字符串 与 c 比较
  1. DS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1).而获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N).

  2. C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow).DS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题.

  3. SDS使用len属性的值而不是空字符来判断字符串是否结束.处理二进制的方式来处理SDS存放在buf数组里的数据.
    SDS实现了空间预分配和惰性空间释放两种优化策略.

当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间.

当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用.

c字符串SDS字符串
获取字符串长度的复杂度为O(N)获取字符串长度的复杂度为O(1),因为记录了字符串的长度
API是不安全的,可能会造成缓存区溢出API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据可以保存文本或二进制数据
可以使用所有<string.h>库中的函数可以使用一部分<string.h>库中的函数

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度.

Redis的链表实现的特性可以总结如下:
  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1).

  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点.

  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1).

  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1).

  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值.

字典

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构.
在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值),这些关联的键和值就称为键值对.

当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突(collision).

Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题.

在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的.

跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.

跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点.

在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树.

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现.

跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快.

每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”.

保存过期时间

redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典:

  • 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键).

  • 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间,即一个毫秒精度的 UNIX 时间戳.

过期键删除策略
  • 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作.

  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键.

  • 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键.至于要删除多少过期键,以及要检查多少个数据库,则由算法决定.

在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略.

Redis过期键删除策略

Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡.

  • Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则由redisServer.dbnum属性保存.

  • 客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同的数据库.

  • 数据库主要由 dict 和 expires 两个字典构成,其中 dict 字典负责保存键值对,而 expires 字典则负责保存键的过期时间.

  • 因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的.

RDB持久化(Redis DataBase)

将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失.

RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中.

RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态.

两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE.

SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求.

BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求.

因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令.

RDB重点

RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据.

  • SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器.

  • BGSAVE令由子进程执行保存操作,所以该命令不会阻塞服务器.

  • 服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令.

  • RDB文件是一个经过压缩的二进制文件,由多个部分组成.

  • 对于不同类型的键值对,RDB文件会使用不同的方式来保存它们.

RDB载入

服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止.

因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:

  • 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态.
  • 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态.

AOF持久化

除了RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能.

与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的.

服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态.

AOF持久化过程

AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤.

  • 命令追加
    当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾.

  • AOF文件的 写入 与 同步
    Redis的服务器进程就是一个事件循环(loop),

这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,

时间事件则负责执行像 serverCron 函数这样需要定时运行的函数.

因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,

所以在服务器每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,

考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到AOF文件里面.

AOF持久化的效率和安全性

服务器配置appendfsync选项的值直接决定AOF持久化功能的效率和安全性.

  • 当 appendfsync 的值为 always 时
    服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件,
    所以always的效率是appendfsync选项三个值当中最慢的一个,
    但从安全性来说,always也是最安全的,因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中所产生的命令数据.

  • 当 appendfsync 的值为 everysec 时
    服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且每隔一秒就要在子线程中对AOF文件进行一次同步.
    从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据.

  • 当 appendfsync 的值为 no 时
    服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,至于何时对AOF文件进行同步,则由操作系统控制.

因为处于no模式下的flushAppendOnlyFile调用无须执行同步操作,所以该模式下的AOF文件写入速度总是最快的,

不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的.

从平摊操作的角度来看,no模式和everysec模式的效率类似,当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有写命令数据.

AOF文件的载入与数据还原

因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态.

Redis读取AOF文件并还原数据库状态的详细步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client):
    因为Redis的命令只能在客户端上下文中执行,
    而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,
    所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,
    伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样.

  2. 从AOF文件中分析并读取出一条写命令.

  3. 使用伪客户端执行被读出的写命令.

  4. 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止.

AOF重写

Redis提供了AOF文件重写(rewrite)功能.

通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,

新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多.

首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理.

为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令.

Redis使用子进程来执行AOF重写,目的:

  • 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求.

  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性.

使用子进程也有一个问题需要解决,
因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,
从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致.

Redis服务器设置了一个AOF重写缓冲区来解决这种数据不一致问题.

AOF重写步骤

在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

  1. 执行客户端发来的命令.

  2. 将执行后的写命令追加到AOF缓冲区.

  3. 将执行后的写命令追加到AOF重写缓冲区(用于保证数据一致性).

这样一来可以保证:

  • AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行.

  • 从创建子进程(用于AOF重写)开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面.

当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:

  1. 将AOF重写缓冲区中的所有内容写入到新AOF文件中(重写的AOF文件中),这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致.

  2. 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换.

这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了.

在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低.

AOF重点

AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态.

  • AOF文件中的所有命令都以Redis命令请求协议的格式保存.

  • 命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件.

  • appendfsync选项的不同值(配置什么时候将AOF缓冲区里的内容刷到AOF文件中)对AOF持久化功能的安全性以及Redis服务器的性能有很大的影响.

  • 服务器只要载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态.

  • AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小.

  • AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作.

  • 在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令.当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致.最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作.

事件

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:

  • 文件事件(file event):
    Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象.服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作.

  • 时间事件(time event):
    Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象.

文件事件

Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler):

  • 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器.

  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件.

虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性.

文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件.因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现.

I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字.

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面(I/O多路复用程序 通过 队列 向 文件事件分派器 传送 套接字),然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字.当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字.

Redis的I/O多路复用程序的所有功能都是通过包装常见的 select、epoll、evport和 kqueue 这些I/O多路复用函数库来实现的,因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的.

I/O多路复用程序允许服务器同时监听套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE事件,等到 AE_READABLE 事件处理完之后,才处理 AE_WRITABLE 事件.
这也就是说,如果一个套接字又可读又可写的话,那么服务器将先读套接字,后写套接字.

文件事件的处理器

Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求,比如说:

  • 为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器.

  • 为了接收客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器.

  • 为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器.

  • 当主服务器和从服务器进行复制操作时,主从服务器都需要关联特别为复制功能编写的复制处理器.

在这些事件处理器里面,服务器最常用的要数与客户端进行通信的连接应答处理器、命令请求处理器和命令回复处理器

时间事件

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器.

事件总结

  • Redis服务器是一个事件驱动程序,服务器处理的事件分为时间事件文件事件两类.

  • 文件事件处理器是基于Reactor模式实现的网络通信程序.

  • 文件事件是对套接字操作的抽象:每次套接字变为可应答(acceptable)、可写(writable)或者可读(readable)时,相应的文件事件就会产生.

  • 文件事件分为AE_READABLE事件(读事件)和AE_WRITABLE事件(写事件)两类.

  • 时间事件分为定时事件周期性事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次.

  • 服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件.

  • 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占.

  • 时间事件的实际处理时间通常会比设定的到达时间晚一些.

客户端

一个服务器可以与多个客户端建立网络连接(1对多),每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复.

客户端类型

  • 伪客户端(fake client)
    的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符.目前Redis服务器会在两个地方用到伪客户端,一个用于载入AOF文件并还原数据库状态,而另一个则用于执行Lua脚本中包含的Redis命令.

  • 普通客户端
    的fd属性的值为大于-1的整数:普通客户端使用套接字来与服务器进行通信,所以服务器会用fd属性来记录客户端套接字的描述符.因为合法的套接字描述符不能是-1,所以普通客户端的套接字描述符的值必然是大于-1的整数.

关闭客户端

一个普通客户端可以因为多种原因而被关闭:

  • 如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭.

  • 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭.

  • 如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭.

  • 如果用户为服务器设置了timeout配置选项,那么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭.不过timeout选项有一些例外情况:如果客户端是主服务器(打开了REDIS_MASTER标志),从服务器(打开了REDIS_SLAVE标志),正在被BLPOP等命令阻塞(打开了REDIS_BLOCKED标志),或者正在执行SUBSCRIBE、PSUBSCRIBE等订阅命令,那么即使客户端的空转时间超过了timeout选项的值,客户端也不会被服务器关闭.

  • 如果客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为1 GB),那么这个客户端会被服务器关闭.

  • 如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭.

前面介绍输出缓冲区的时候提到过,可变大小缓冲区由一个链表和任意多个字符串对象组成,理论上来说,这个缓冲区可以保存任意长的命令回复.

但是,为了避免客户端的回复过大,占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作.

服务器使用两种模式来限制客户端输出缓冲区的大小:

  • 硬性限制(hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端.

  • 软性限制(soft limit):如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态结构的obuf_soft_limit_reached_time属性记录下客户端到达软性限制的起始时间;之后服务器会继续监视客户端,如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端;相反地,如果输出缓冲区的大小在指定时间之内,不再超出软性限制,那么客户端就不会被关闭,并且obuf_soft_limit_reached_time属性的值也会被清零.

服务端

服务端处理命令步骤

从客户端发送SET KEY VALUE命令到获得回复OK期间,客户端和服务器共需要执行以下操作:

  1. 客户端向服务器发送命令请求SET KEY VALUE.
    客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器.

  2. 服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK.
    当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

    1)读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面.

    2)对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性里面.

    3)调用命令执行器,执行客户端指定的命令.
    [1]在命令表(command table)中查找参数所指定的命令.
    [2]

  3. 服务器将命令回复OK发送给客户端.

  4. 客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看.

一个命令请求从发送到完成主要包括以下步骤:

  1. 客户端将命令请求发送给服务器;

  2. 服务器读取命令请求,并分析出命令参数;

  3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;

  4. 服务器将命令回复返回给客户端.

serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等.

服务器从启动到能够处理客户端的命令请求需要执行以下步骤:

  1. 初始化服务器状态

  2. 载入服务器配置

  3. 初始化服务器数据结构

  4. 还原数据库状态
    如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态.
    相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态

  5. 执行事件循环

复制

在Redis中,用户可以通过执行 SLAVEOF 命令或者设置 slaveof 选项,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave).

Redis从2.8版本开始,使用 PSYNC 命令代替 SYNC 命令来执行复制时的同步操作
PSYNC 命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:

完整重同步
用于处理初次复制情况:完整重同步的执行步骤和 SYNC 命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步.

部分重同步
用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态.

哨兵 Sentinel

监视从,从升主.

哨兵是Redis的高可用性(high availability)解决方案:
由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器.

并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求.

主server挂掉,故障转移.
当Sentinel系统监视到某个主server挂掉了,sentinel系统会对该server进行故障转移操作.
首先,Sentinel系统会挑选server1属下的其中一个从服务器,将这个被选中的从服务器升级为新的主服务器.
之后,Sentinel系统会向server1属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕.

哨兵的工作

Sentinel只是一个运行在特殊模式下的Redis服务器,它使用了和普通模式不同的命令表,所以Sentinel模式能够使用的命令和普通Redis服务器能够使用的命令不同.

Sentinel会读入用户指定的配置文件,为每个要被监视的主服务器创建相应的实例结构,并创建连向主服务器的命令连接和订阅连接,其中命令连接用于向主服务器发送命令请求,而订阅连接则用于接收指定频道的消息.

Sentinel通过向主服务器发送INFO命令来获得主服务器属下所有从服务器的地址信息,并为这些从服务器创建相应的实例结构,以及连向这些从服务器的命令连接和订阅连接.

在一般情况下,Sentinel以每十秒一次的频率向被监视的主服务器和从服务器发送INFO命令,当主服务器处于下线状态,或者Sentinel正在对主服务器进行故障转移操作时,Sentinel向从服务器发送INFO命令的频率会改为每秒一次.

对于监视同一个主服务器和从服务器的多个Sentinel来说,它们会以每两秒一次的频率,通过向被监视服务器的__sentinel__:hello频道发送消息来向其他Sentinel宣告自己的存在.

每个Sentinel也会从__sentinel__:hello频道中接收其他Sentinel发来的信息,并根据这些信息为其他Sentinel创建相应的实例结构,以及命令连接.

Sentinel只会与主服务器和从服务器创建命令连接和订阅连接,Sentinel与Sentinel之间则只创建命令连接.

Sentinel以每秒一次的频率向实例(包括主服务器、从服务器、其他Sentinel)发送PING命令,并根据实例对PING命令的回复来判断实例是否在线,当一个实例在指定的时长中连续向Sentinel发送无效回复时,Sentinel会将这个实例判断为主观下线.

当Sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他Sentinel进行询问,看它们是否同意这个主服务器已经进入主观下线状态.

当Sentinel收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作.

发布与订阅

Redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成.

通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息.

Lua脚本

在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令.
与Lua环境进行协作的两个组件,它们分别是负责执行Lua脚本中包含的Redis命令的伪客户端,以及负责保存传入服务器的Lua脚本的脚本字典.

慢查询日志

Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度.

服务器配置有两个和慢查询日志相关的选项:

·slowlog-log-slower-than选项指定执行时间超过多少微秒(1秒等于1 000 000微秒)的命令请求会被记录到日志上.
举个例子,如果这个选项的值为100,那么执行时间超过100微秒的命令就会被记录到慢查询日志;如果这个选项的值为500,那么执行时间超过500微秒的命令就会被记录到慢查询日志.

·slowlog-max-len选项指定服务器最多保存多少条慢查询日志.
服务器使用先进先出的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于slowlog-max-len选项的值时,服务器在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FlyingZCC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值