文章目录

一、前言

本文介绍单机版redis,尽量从底层原理出发,以图解方式介绍,分别包括:

介绍redis服务器中的数据库,分为三个小节:redis服务器中数据库、redis增删查改底层实现,redis失效;

介绍redis RDB持久化,分为四个小节:RDB文件生成与载入、RDB文件自动间隔保存、RDB文件结构介绍与RDB文件的分析;

介绍redis AOF持久化,分为两个小节:从RDB持久化到AOF持久化、AOF持久化的实现;

介绍redis两种持久化对比,分为三个小节:定义、优缺点和应用;

介绍redis客户端,分为三个小节:redis客户端-服务器架构、redis客户端属性和redis客户端创建与关闭;

介绍redis服务端,分为两个小节:redis服务端初始化、redis服务端处理命令请求执行过程。

二、redis服务器中数据库

redis有五种基本类型,每一个类型都可以做增删查改操作。

2.1 初识redis服务器中数据库

让我们来见识一下redis中的数据库结构(这是整篇博客的基础,后面的都是围绕redisServer这个结构来讲解的)

struct redisServer{
       // ...
       //一个数组,保存着服务器中的所有数据库
              redisDb *db;
       //服务器的数据库数量
              int dbnum;
       // ...
};

Redis,性能加速的催化剂(二)_服务器

对于上述代码和示意图的解释是:Redis服务器将所有数据库都保存在服务器状态redis.h/ redisServer结构的db数组中,db数组的每个项都是一个 redis.h/redisDb结构,每个 redisDb结构(即代码中的redisDb *db)代表一个数据库,同时,程序会根据服务器状态的donum属性(即代码中的int dbnum)来决定应该创建多少个数据库。num属性的值由服务器配置的 database选项决定,默认情况下,该选项的值为16,所以 Redis服务器默认会创建16个数据库:

Redis,性能加速的催化剂(二)_原力计划_02
Redis,性能加速的催化剂(二)_服务器_03

切换数据库:对于redis默认的16个数据库(db0-db15),在操作的时候可以选择将数据(五种类型均可)存放在哪个数据库中,只要将某个数据库设置成当前数据库就好(select 数目),宏观命令如下:

Redis,性能加速的催化剂(二)_redis_04
Redis,性能加速的催化剂(二)_redis_05
底层变化:
Redis,性能加速的催化剂(二)_redis_06

2.2 从底层原理图讲解redis增删改查操作

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

键空间和用户所见的数据库是直接对应的:

1)键空间的键也就是数据库的键,每个键都是一个字符串对象;

2)键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象集合对象和有序集合对象中的任意一种Redis对象。

如图:

Redis,性能加速的催化剂(二)_redis_07

对于上图解释:redisDb即表示redis服务器中的数据库,里面有一个dict字典,里面存放数据实体(即key-value键值对),这里有三个key-value键值对,分别是

(key,value)=(“alpha”,“a b c”)为ListObjet类型,

(key,value)=(“book”,"<name,Redis in Action><author,Josiah L.Carlson><publisher,Manning>")为HashObject类型,

(key,value)=(“alpha”,“hello world”)为StringObject类型

2.2.1 添加新键

Redis,性能加速的催化剂(二)_原力计划_08

对于上图解释:添加新键,StringObject类型 <key,value>=<“date”,“2020/02/09”>

2.2.2 删除键

Redis,性能加速的催化剂(二)_redis_09

对于上图解释:删除新键,删除key为“book”的键值对

2.2.3 更新键

Redis,性能加速的催化剂(二)_数据库_10

对于上图解释:更新键,更新key为“message”的键值对

2.2.4 对键取值

Redis,性能加速的催化剂(二)_客户端_11

对于上图解释:对键取值,获取key为“message”的value

2.3 redis失效(主动删除+被动删除)

2.3.1 生存时间的设置与读取

介绍四个命令(和TTL time to live 生存时间有关的),用表格清晰些,如下:

命令 含义
EXPIRE 设置剩余生存时间,以秒为单位,将键key的生存时间设置为ttl秒
PEXPIRE 设置剩余生存时间,以毫秒为单位,将键key的生存时间设置为ttl毫秒
EXPIREAT 设置剩余生存时间,以秒为单位,将键key的生存时间设置为timestamp所指定的秒数时间戳
PEXPIREAT 设置剩余生存时间,以毫秒为单位,将键key的生存时间设置为timestamp所指定的秒数时间戳
TTL 返回指定key的剩余生存时间,以秒为单位
PTTL 返回指定key的剩余生存时间,以毫秒为单位

这个表格给出了指定key的过期时间的存储,这里需要注意一个点,设置指定key生存时间一共有四个命令EXPIRE PEXPIRE EXPIREAT PEXPIREAT,这里展示四个命令底层关系,如图:

Redis,性能加速的催化剂(二)_原力计划_12

我们可以看到,四个命令底层关系:四个设置生存时间的命令,底层最终都是使用PEXPIREAT命令去实现的。

2.3.2 生存时间的底层保存(过期字典)

redis是基于key-value存储的一个非关系型数据库,对于每一个记录的key,都有一个生存时间TTL,上面介绍了指定key的生存时间的读写,那么,redis中每一个key的生存时间是底层是如何存储的呢?答案是使用“过期字典”存储。

过期字典引入:redisDB结构的expires字典保存了数据库中所有键的过期时间,这个expires字典就是过期字典。

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

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

Redis,性能加速的催化剂(二)_客户端_13

展示了一个带有过期字典的数据库例子,在这个例子中,键空间保存了数据库中的所有键值对,而过期时间则保存了数据库中所有按键值对的过期时间。

生存时间(过期时间)的添加和删除略过。

2.3.3 过期字典如何处理过期key(即过期key的删除):定时删除 惰性删除 定期删除

一表总结(用表格比对清晰):

删除方式(过期key删除) 解释名称 含义 删除类型 优点 缺点 备注
定时删除 内部含有定时器,故称为定时删除 在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。 主动删除 对内存友好,过期key尽快被删除,被释放过期key的内存 对CPU时间不友好,较快删除key,占用CPU时间 该方式redis服务器中需要创建大量定时器,不现实,舍去。
惰性删除 一定要等到使用该键的时候才删除过期key,比较懒惰,故称为惰性删除 放任键不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。 被动删除 对CPU时间友好,过期key尽可能慢地删除,取出时才检查是否过期,尽可能少的占用CPU时间 对内存不友好,过期地key删除不及时,一定要等到再次取出时才检查删除,这段时间内占用内存 由于存在着很多过期的key没有及时被删除,容易造成内存泄露
定期删除 均衡定时删除和惰性删除,得到一个合适的时间段删除,故称为定期删除 每隔一段时间,程序就会对数据库进行一个检查,删除里面的过期键。至于要删除多少个过期键,以及要检查多少个数据库,则由算法决定。 主动删除 定时删除和惰性删除的综合,合理使用CPU和内存 难以确定删除操作执行的时长和频率 若删除频繁,则接近定时删除,消耗太多CPU时间;若删除太少,则接近惰性删除,消耗内存。
三、RDB持久化(核心:磁盘上的RDB文件)

持久化就是持久存储,计算机中能够作为存储的介质一般有三种:半导体、光存储介质、磁性介质,

半导体存储介质 光存储介质 磁性介质
应用 缓存ROM、内存RAM 光盘 磁盘、磁带
易失性 断电易失性 断电不易失 断电不易失
持久化 不可实现持久化 可实现持久化 可实现持久化

对于持久化最最通俗易懂的理解,就是把数据存放到磁盘上去(把数据从内存中备份到磁盘上去)。

Redis是一个内存数据库,它将自己的数据库状态存储到内存中,Redis提供两个持久化方式:RDB持久化、AOF持久化,如果选择呢?如图:

Redis,性能加速的催化剂(二)_原力计划_14

对于上图的解释是:因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:

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

3.1 RDB文件的生成与载入(内存上的redis数据库 --> 磁盘上的RDB文件、磁盘上的RDB文件 -->内存上的redis数据库)

本节称为RDB文件的生成与载入,实际包括两个部分的内容:RDB文件的生成+RDB文件的载入,如下图所示:

Redis,性能加速的催化剂(二)_redis_15

3.1.1 RDB文件的生成(SAVE命令+BGSAVE命令)

RDB文件生成涉及两个命令,分别是SAVE命令和BGSAVE命令,两个如下:

命令 含义 异同点
SAVE命令 SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止(ps:在服务器进程阻塞期间,服务器不能处理任何命令请求) 相同点:都是在磁盘上创建/生成RDB文件; 不同点:SAVE命令会阻塞,BGSAVE命令不会阻塞 注意:SAVE译为保存,所以会阻塞,BGSAVE为Background SAVE,译为后台保存,所以不会阻塞。
BGSAVE命令 BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求

1)当SAVE命令执行时, Redis服务器会被阻塞,所以当SAVE命令正在执行时,客户端发送的所有命令请求都会被拒绝。只有在服务器执行完SAVE命令、重新开始接受命令请求之后,客户端发送的命令才会被处理。

2)BGSAVE命令的保存工作是由子进程执行的,所以在子进程创建RDB文件的过程中,Redis服务器仍然可以继续处理客户端的命令请求,但是,在 BGSAVE命令执行期间,服务器处理SAVE、 BGSAVE、 BGREWRITEAOF三个命令的方式会和平时有所不同

首先,在 BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和 BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbsave调用,防止产生竞争条件

其次,在BGSAVE命令执行期间,客户端发送的 BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件

最后, BGREWRITEAOF和 BGSAVE两个命令不能同时执行(如果 BGSAVE命令正在执行,那么客户端发送的 BGREWRITEAOF命令会被延迟到RGSAVE命令执行完毕之后执行;如果 BGREWRITEAOF命令正在执行,那么客户端发送的 RGSAVE命令会被服务器拒绝)。实际上,BGREWRITEAOF和 BGSAVE两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,但是,这两个子进程都同时执行大量的磁盘写入操作,不能同时执行它们只是一个性能方面的考虑一并发出两个子进程。

3.1.2 RDB文件的载入

服务器在载入RDB文件期间,(redis服务器)会一直处于阻塞状态,直到载入工作完成为止,没什么好介绍的,故略去。

3.2 Redis自动间隔保存

对于redis数据库保存的两个命令(SAVE命令和 BGSAVE命令):SAVE命令由服务器进程执行保存工作, BGSAVE命令则由子进程执行保存工作,所以SAVE命令会阻塞服务器,而 BGSVE命令则不会。因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行 BGSAVE命令,即我们的Redis自动间隔保存就是通过设置条件,在满足阈值的时候调用BGSAVE命令来实现的。

举个例子,如果我们向服务器提供以下配置

save  900  1
save  300  10
save  60   10000

只要满足三个条件中的任意一个,BGSAVE命令就会执行,实现RDB文件保存至磁盘。

服务器在900秒之内,对数据库进行了至少1次修改。
服务器在300秒之内,对数据库进行了至少10000次修改。
服务器在60秒之内,对数据库进行了至少10000次修改。

Redis自动间隔保存的底层实现(saveparam数组(seconds秒数+changes修改数)+dirty计数器+lastsave属性)

Redis,性能加速的催化剂(二)_客户端_16

3.3 RDB文件结构

让我们来看一下磁盘上这个神秘的RDB文件:

Redis,性能加速的催化剂(二)_原力计划_17

针对上图给出表格(各个部分含义用表格看清晰些)

RDB文件结构各个部分 含义 长度 备注
第一个部分REDIS 该部分保存着“REDIS”五个字符,程序载入文件时,通过这五个字符,快速检查所载入的文件是否是RDB文件 5字节 这里是二进制数据而不是字符串,即“REDIS”表示’R’‘E’‘D’‘I’‘S’五个字符,而不是’R’‘E’‘D’‘I’‘S’’\0’五个字符
第二个部分db_version 该部分记录RDB文件版本号,如“0006”代表RDB文件为第六版本。 4字节
第三个部分databases 该部分根据实际情况记录着0个或多个数据库 0~n字节 根据数据库锁保存键值对的数量、类型和内容,该部分长度不同
第四个部分EOF 该部分一个EOF常量,表示RDB文件正文部分结束 1字节 当程序读到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了
第五个部分check_sum 该部分为一个无符号数,保存着一个检验和,载入RDB文件时,用来检查是否损坏 8字节 这个检验和是程序通过对REDIS、db_version databases EOF四个部分的内容计算得出的

Redis,性能加速的催化剂(二)_客户端_18

对于上图的解释:上图同时给出了“RDB文件结构、RDB文件中的数据库结构、RDB文件中的数据库中的键值对的结构” 三层结构,我们要对三层结构同时解析。

关于RDB文件结构:包括REDIS常量、db_version数据库版本、databases实际数据库、EOF常量标志、check_sum检验和,其中,数据库为空则没有第三部分databases,其他不难,结合上图一看就懂,略。

关于RDB文件中的数据库结构:包括SELECTDB常量、db_number数据库序号、key_value_pairs实际键值对,结合上图一看就懂,略。

关于RDB文件中的数据库中的键值对的结构:包括TYPE类型、key键、value值。

RDB文件中的数据库中的键值对的结构(TYPE+key+value)

TYPE记录了value的类型,长度为1个字节(每一个TYPE常量都代表一个对象类型或底层编码)

TYPE常量 对应的对象类型或编码类型
REDIS_RDB_TYPE_STRING string类型 int底层编码/raw底层编码/embstr编码
REDIS_RDB_TYPE_LIST list类型 linkedlist底层编码
REDIS_RDB_TYPE_SET set类型 hashtable编码
REDIS_RDB_TYPE_ZSET sorted set类型 skiplist编码
REDIS_RDB_TYPE_HASH hash类型 hashtable编码
REDIS_RDB_TYPE_LIST_ZIPLIST list类型 ziplist底层编码
REDIS_RDB_TYPE_SET_INTSET set类型 intset底层编码
REDIS_RDB_TYPE_ZSET_ZIPLIST sorted set类型 ziplist底层编码
REDIS_RDB_TYPE_HASH_ZIPLIST hash类型 ziplist底层编码

key表示键,value表示值

1)字符串对象(REDIS ENCODING_INT和REDIS_ENCODING_RAW(大于20字节压缩,小于等于20字节不压缩))

对于上表,如果TYPE的值为 REDIS_RDB_TYPE_STRING, value保存的就是一个字符串对象,字符串对象的编码可以是REDIS ENCODING_INT或者REDIS_ENCODING_RAW。

如果字符串对象的编码为 REDIS_ENCODING_INT,那么说明对象中保存的是长度不超过32位的整数 ,

如果字符串对象的编码为 REDIS_ENCODING_RAW,那么说明对象所保存的是一个字符串值,根据字符串长度的不同,有压缩和不压缩两种方法来保存这个字符串:如果字符串的长度小于等于20宇节,那么这个字符串会直接被原样保存;如果字符串的长度大于20字节,那么这个字符串会被压缩之后再保存。

一图小结:

Redis,性能加速的催化剂(二)_服务器_19

对上图的理解:左边存放数字,中间因为字符串长度为21>20,所以压缩,右边因为字符串长度为5<20,不压缩。

2)列表对象

如果TYPE的值为 REDIS_RDB_TYPE_LIST,那么 value保存的就是一个 REDIS_ENCONDING_LINKEDLIST编码的对象,一图小结:

Redis,性能加速的催化剂(二)_数据库_20

3)集合对象

如果TYPE的值为REDIS_RDB_TYPE_SET,那么 value保存的就是一个 REDIS_ENCODING_HT编码的集合对象,一图小结:
Redis,性能加速的催化剂(二)_原力计划_21

4)哈希表对象

如果TYPE的值为REDIS_RDB_TYPE_HASH,那么value保存的就是一个 REDIS_ENCODING_HT编码的集合对象,一图小结:

Redis,性能加速的催化剂(二)_redis_22

5)有序集合对象

如果TYPE的值为 REDIS_RDB_TYPE_ZSET,那么value保存的就是一个REDIS_ENCODING_SKIPLIST编码的有序集合对象,一图小结:

Redis,性能加速的催化剂(二)_数据库_23

6)INTSET编码的集合

如果TYPE的值为 REDIS_RDB_TYPE_SET_INTSET,那么 value保存的就是一个整数集合对象,RDB文件保存这种对象的方法是,先将整数集合转换为字符串对象,然后将这个字符串对象保存到RDB里面。如果程序在读入RDB文件过程中,碰到由整数集合对象转换成的字符串对象,那么程序会根据TYPE值的指示,先读入字符串对象,再将这个字符串对象转换成原来的整数集合对象。

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

如果TYPE的值为 REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST或者REDIS_RDB_TYPE_ZSET ZIPLISL,value保存的就是一个压缩列表对象,RDB文件保存这种对象的方法是:

1)将压缩列表转换成一个字符串对象;
2)将转换所得的字符串对象保存到RDB文件。

如果程序在读入RDB文件的过程中,碰到由压缩列表对象转换成的字符串对象,那么程序会根据TYPE值的指示,执行以下操作:

1)读入字符串对象,并将它转换成原来的压缩列表对象。
2)根据TYPE的值,设置压缩列表对象的类型:如果TYPE的值为 REDIS_RDB_TYPE_LIST_ZIPLIST,那么压缩列表对象的类型为列表;如果TYPE的值为REDIS_RDB_TYPE_HASH_ZIPLIST,那么压缩列表对象的类型为哈希表;如果TYPE的值为REDIS_RDB_ TYPE_ZSET_ZIPLIST,那么压缩列表对象的类型为有序集合。

从步骤2可以看出,由于TYPE的存在,即使列表、哈希表和有序集合三种类型都使用压缩列表来保存,RDB读入程序也总可以将读人并转换之后得出的压缩列表设置成原来的类型。

3.4 分析RDB文件

上面对RDB文件的介绍,这里对实际的RDB文件分析。

3.4.1 不包含任何键值对的RDB文件

Redis,性能加速的催化剂(二)_客户端_24

3.4.2 包含任何键值对的RDB文件

Redis,性能加速的催化剂(二)_客户端_25

3.4.3 包含带有过期时间的字符串键的RDB文件

Redis,性能加速的催化剂(二)_数据库_26

3.4.4 包含一个集合键的RDB文件

Redis,性能加速的催化剂(二)_原力计划_27

四、AOF持久化(核心:磁盘上的AOF文件)

4.1 从RDB持久化到AOF持久化

除了RDB持久化功能之外, Redis还提供了AOF( Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如下图:

Redis,性能加速的催化剂(二)_服务器_28

关于RDB持久化与AOF持久化的不同:RDB持久化是将进程数据写入文件,而AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。所以,与RDB相比,AOF持久化拥有更好的实时性。

注意:Redis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置:appendonly yes

4.2 AOF持久化的实现(命令追加append+文件写入write+文件同步sync+文件重写rewrite)

AOF持久化三个步骤:命令追加append、文件写入write、文件同步sync,且看下表:

AOF持久化步骤 含义
命令追加append (当AOF持久化功能处于打开状态时 appendonly yes),服务器在执行完一个写命令write,会以协议格式将其(write命令)追加到服务器状态auto_aof缓冲区的末尾
文件写入write和文件同步sync 根据不同的同步策略将aof_buf中的内容同步到硬盘
文件重写rewrite 定期重写AOF文件,达到压缩的目的

4.2.1 命令追加append

略过,看上面表格就好了,将Redis的写命令追加到缓冲区aof_buf。

4.2.2 文件写入append和文件同步sync

事件循环(基础概念):事件循环就是一个Redis的服务器进程,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。

因为服务器在处理文件事件时可能会执行写命令,使得些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。即AOF文件写入和文件同步是通过flushAppendOnlyFile函数来完成的。

又flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,关于appendfsync不同值的不同持久化操作,一表小结:

appendfsync选项的值 flushAppendOnlyFile属性的行为 效率 安全性
always 将aof_buf缓冲区中的所有内容写入并同步到AOF文件中 最慢(服务器每个事件循环将aof_buf缓冲区的所有内容写入到AOF文件中,并同步AOF文件) 最安全(出现故障停机,数据库丢失一个数据循环中的所有命令数据)
everysec 将aof_buf缓冲区中的所有内容写入到AOF文件中,如果上次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的 适中(服务器每个事件循环将aof_buf缓冲区的所有内容写入到AOF文件中,并每隔一秒钟在子线程中同步AOF文件) 适中(出现故障停机,数据库会丢失一秒钟的命令数据)
no 将aof_buf缓冲区中的所有内容写入到AOF文件中,但并不对AOF文件进行同步,何时同步由操作系统决定 最快(服务器每个事件循环将aof_buf缓冲区的所有内容写入到AOF文件中,同步操作的执行由操作系统控制) 最不安全(出现故障停机,数据库会丢失上次同步AOF文件之后的所有写命令数据)

注意:appendfsync默认值是everysec。

4.2.3 AOF重写rewrite

1)为什么进行AOF重写?

随着时间流逝,Redis服务器执行的写命令越来越多,AOF文件也会越来越大;过大的AOF文件不仅会影响服务器的正常运行,也会导致数据恢复需要的时间过长,这个时候需要在服务器上存放一个精简版的AOF文件,这里就涉及到AOF重写。

2)什么是AOF重写?

文件重写是指定期重写AOF文件,减小AOF文件的体积,即生成新AOF文件替换旧AOF文件的功能。

注意1:AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作,即旧文件是不会有任何读写操作的。

注意2:对于AOF持久化来说,文件重写虽然是强烈推荐的,但并不是必须的;即使没有文件重写,数据也可以被持久化并在Redis启动的时候导入;因此在一些实现中,会关闭自动的文件重写,然后通过定时任务在每天的某一时刻定时执行。

3)文件重写为什么能够压缩AOF文件?文件重写是如何实现压缩AOF文件的?
过期的数据不再写入新的AOF文件,从而使新的AOF文件相对于旧的AOF文件体积得到压缩;
无效的命令不再写入新的AOF文件,从而使新的AOF文件相对于旧的AOF文件体积得到压缩;
多条命令可以合并为一个,从而使新的AOF文件相对于旧的AOF文件体积得到压缩;

注意:为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义,不可更改。

五、两种持久化的对比

无论是RDB持久化还是AOF持久化,都是Redis 高可用 中比较重要的一个环节,因为Redis数据在内存的特性,一断电或者重启丢失了,所以持久化必须得有,只有通过持久化,redis这个基于内存的缓存,才能变成一个NoSQL非关系型数据库。

5.1 RDB持久化与AOF持久化

RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化, 把整个 Redis 的数据保存在单一文件中。RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储。

AOF:AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作,AOF 机制对每条写入命令作为日志,对日志文件的写入操作使用的追加模式,以 append-only 的模式写入一个日志文件中。因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的redolog。

tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

附:RDB的原理是什么?
回答:fork和cow。
fork(分岔)是指redis通过创建子进程来进行RDB操作;
cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

5.2 RDB持久化优缺点与AOF持久化优缺点

5.2.1 RDB持久化优点

第一,RDB持久化生成的每个数据文件中存放的都是完整的Redis中的信息,很适合做冷备

RDB持久化会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,每一个数据文件中存放的都是完整的Redis中的信息,很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。

第二,RDB持久化,在同步数据的时候,对Redis的性能影响非常小,在恢复数据的时候,速度比AOF持久化快

RDB持久化对Redis的性能影响非常小,因为RDB持久化,在同步数据的时候他只是fork(分岔)了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

5.2.2 RDB持久化缺点

第一,RDB持久化数据完整性不如AOF持久化

RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。

第二,RDB持久化,在同步数据的时候,如果文件很大,会使前端客户端停顿几毫秒或几秒

RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,因为这个时候它fork(分岔)了一个子进程去生成一个大快照,如果你公司在做秒杀的时候, 就出大问题。

5.2.3 AOF持久化优点

第一,AOF持久化适合做适合做灾难性数据误删除的紧急恢复,适合做热备

AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了,然后重新导入热备就好了。

第二,AOF持久化数据完整性更好

相比RDB五分钟一次生成快照,AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。

第三,AOF持久化,同步数据的时候,追加写文件,不用寻址,非常快,一定不会产生停顿

AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。

5.2.4 AOF持久化缺点

第一,一样的数据,空间占用大:一样的数据,AOF文件比RDB还要大。

第二,写QPS(Query Per Second每秒查询率)效率低:AOF开启后,AOF写的QPS会比RDB支持写的要低。如果AOF持久化是每秒都要去异步刷新一次日志fsync,这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的。

5.3 RDB持久化和AOF持久化的应用

两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去(比如用户数据,备份一份到我广州的节点,再备份一个到深圳的节点),RDB更适合做冷备,AOF更适合做热备。

单独用RDB你会丢失很多数据,你单独用AOF你数据恢复没RDB来的快。所以,一旦宕机,先用RDB恢复,然后AOF做数据补全,冷备热备一起上,才是互联网时代一个高健壮性系统的王道。

1、原因:因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。
2、总流程:RDB做镜像全量持久化,AOF做增量持久化。
3、具体流程:在redis实例重启或启动时,会使用RDB持久化文件重新构建内存,再redis运行时,使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
4、具体流程:把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志就好了,服务器重启的时候先把表的数据全部搞进去(即做一次RDB持久化),但是他可能不完整,你再回放一下日志(即做一次AOF持久化),数据就完整了。
5、Redis持久化机制:
(1) AOF持久化开启且存在AOF文件时,优先加载AOF文件;
(2) AOF关闭或者AOF文件不存在时,加载RDB文件;
(3) 加载AOF/RDB文件后,Redis启动成功;
(4) AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

问题:如果持久化的时候,突然机器掉电会怎样?
回答:取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

小结:MySQL容灾类比Redis持久化
MySQL容灾 = 每日全量备份数据 + 今天的binlog日志
Redis持久化 = RDB冷备全量数据(类似存量备份数据) + AOF热备增量数据(类似binlog)

5.4 小结

RDB持久化 AOF持久化
优点 1、空间:一样的数据,空间占用小;2、一样的数据,写操作QPS效率高:写操作QPS效率高;3、一样的数据,恢复数据速度快:在恢复数据的时候,速度比AOF持久化快; 1、AOF持久化数据完整性更好;2、AOF持久化,同步数据的时候,追加写文件,不用寻址,非常快,一定不会产生停顿
缺点 1、持久化的数据完整性:RDB持久化数据完整性不如AOF持久化;2、持久化数据时候的停顿:在同步数据的时候,如果文件很大,会使前端客户端停顿几毫秒或几秒 1、一样的数据,空间占用大;2、一样的数据,写操作QPS效率低;3、一样的数据,在恢复数据的时候,速度比RDB持久化慢;

1、RDB持久化生成的每个数据文件中存放的都是完整的Redis中的信息,全量数据 + 空间占用小 + 写QPS效率高 + 恢复数据快,很适合做冷备;
2、AOF持久化每次只是追加的方式写数据,增量数据 + 数据完整性好 + 同步数据不影响客户端,很适合热备;

六、客户端

6.1 Redis客户端-服务器架构

Redis服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。Redis客户端-服务器的结构如下图:

Redis,性能加速的催化剂(二)_服务器_29

通过使用I/O多路复用技术实现的文件事件处理器, Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/ redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时所需要的数据结构,主要包括(“6.2 客户端属性”会具体介绍每一个属性):

信息名称 redisClient属性名称 对应下面的小节 备注
套接字描述符 fd属性 6.2.1 套接字描述符(fd) 记录了客户端正在使用的套接字描述符
名字 name属性 6.2.2 名字(name) 记录了连接到服务器的客户端名字
标志 flag属性 6.2.3 标志(flags) 记录了客户端的角色及目前所处的状态
输入缓冲区 querybuf属性 6.2.4 输入缓冲区(querybuf) 用户输入和输入缓冲区中的内容是不一样的,如用户输入为SET KEY VALUE,输入缓冲区内容为*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n,下面会介绍。
命令与命令参数 argv属性、argc属性 6.2.5 命令与命令参数(argv argc) argv属性表示数组本身,argc属性表示数组元素个数
命令的实现函数 cmd属性(cmd指针) 6.2.6 命令的实现函数(redisCommand) cmd是redisClient中的一个指针属性,redisCommand是命令的具体值
输出缓冲区 buf属性、bufpos属性 6.2.7 输出缓冲区(buf bufpos)(固定缓冲区+可变缓冲区) 输出缓冲区和显示给用户的内容是不一样的,如输出缓冲区的内容为+OK\r\n,显示给用户为OK,下面会介绍。
身份验证 authenticated属性 6.2.8 身份验证(authenticated) 客户端是否通过相互验证,值为0,未通过身份验证,值为1,通过身份验证
时间 ctime属性、lastinteraction属性、obuf_soft_limit_reached_time属性 6.2.9 时间(ctime lastinteraction obuf_soft_limit_reached_time) 客户端、服务器网络交互相关实现

Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构。对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历client链表来完成,如下图:

Redis,性能加速的催化剂(二)_服务器_30

6.2 客户端属性

“6.1 Redis客户端-服务器架构”介绍了客户端的各个属性,本节详细介绍。

客户端状态包含的属性可以分为两类:

一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性,本节详细介绍。

另外一类是和特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_keys属性等等,不介绍,略过。

先上一张图,redis客户端运行时,输入“client list”打印客户端状态:

Redis,性能加速的催化剂(二)_客户端_31

6.2.1套接字描述符(fd)

客户端状态的fd属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd属性的值可以是-1或者是大于-1的整数 :

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

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

6.2.2 名字(name)

在默认情况下,一个连接到服务器的客户端是没有名字的,使用client setname命令可以为客户端设置一个名字,让客户端的身份变得更清晰,如图:

Redis,性能加速的催化剂(二)_数据库_32

6.2.3 标志(flags)

客户端的标志属性flags记录了客户端的角色(role),以及客户端目前所处的状态:

​typedef struct redisClient{
    int flags;
}redisClient;

flags属性中,每个标志使用一个常量表示,一部分标志记录了客户端的角色,如:

(1)在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。 REDIS_MASTER标志表示客户端代表的是一个主服务器, REDIS_SLAVE标志表示客户端代表的是一个从服务器

(2)REDIS_PRE_PSYNC标志表示客户端代表的是一个版本低于Redis2.8的从服务器,主服务器不能使用PSYNC命令与这个从服务器进行同步。这个标志只能在 REDIS_SLAVE标志处于打开状态时使用。

(3)REDIS_LUA_ CLIENT标识表示客户端是专门用于处理Lua脚本里面包含的Redis命令的伪客户端。

而另外一部分标志则记录了客户端目前所处的状态

以上提到的所有标志都定义在redis.h文件里面
REDIS_MONITOR标志 表示客户端正在执行 MONITOR命令
REDIS_MONITOR标志 表示服务器使用UNIX套接字来连接客户端
REDIS_BLOCKED标志 表示客户端正在被 BRPOP、BPOP等命令阻塞
REDIS_UNBLOCKED标志 表示客户端已经从 REDIS_BLOCKED标志所表示的阻塞状态中脱离出来,不再阻塞, REDIS_UNBLOCKED标志只能在 REDIS_BLOCKED标志已经打开的情况下使用。
REDIS_MULTI标志 表示客户端正在执行事务。
REDIS_ DIRTY_CAS标志 表示事务使用 WATCH命令监视的数据库键已经被修改
REDIS_DIRTY_EXEC标志 表示事务在命令入队时出现了错误
REDIS_CLOSE_ASAP标志 表示客户端的输出缓冲区大小超出了服务器,服务器会在下一次执行 servercron函数时关闭这个客户端,以免服务器的稳定性受到这个客户端影响。积存在输出缓冲区中的所有内容会直接被释放,不会返回给客户端。
REDIS_CLOSE_AFTER_REPLY标志 表示有用户对这个客户端执行了CLIENT KILL命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容。服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端。
REDIS_ASKING标志 表示客户端向集群节点(运行在集群模式下的服务器)发送了ASKING命令。
REDIS_FORCE_AOF标志 强制服务器将当前执行的命令写人到AOF文件里面
REDIS_FORCE_REPL标志 强制主服务器将当前执行的命令复制给所有从服务器

6.2.4 输入缓冲区(querybuf)

Redis,性能加速的催化剂(二)_原力计划_33

输入缓冲区querybuf的大小会根据输入内容动态地缩小或者扩大,但是它的最大大小不能超过1GB,否则服务器关闭这个客户端。

6.2.5 命令与命令参数(argv argc)

Redis,性能加速的催化剂(二)_服务器_34

6.2.6 命令的实现函数(redisCommand)

当服务器从协议内容中分析并得到argv属性和argc属性的值之后,redis服务器将根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。
Redis,性能加速的催化剂(二)_redis_35

6.2.7 输出缓冲区(buf bufpos)(固定缓冲区+可变缓冲区)

Redis,性能加速的催化剂(二)_redis_36

6.2.8 身份验证(authenticated)

Redis,性能加速的催化剂(二)_服务器_37

6.2.9 时间(ctime lastinteraction obuf_soft_limit_reached_time)

typedef struct redisClient{
    time_t  ctime;
    time_t  lastinteraction;
    time_t  obuf_soft_limit_reached_time;
}redisClient;
redisClient属性(与时间相关的属性) 含义
ctime 该属性记录创建客户端的时间,这个时间用来计算客户端与服务器已经连接了多少秒了,使用client list命令查看时,age域记录了这个秒数(age域以秒为单位,记录了ctime参数)
lastinteraction 该属性记录了客户端与服务器最后一个进行互动interaction的时间,这里的互动指的是客户端对服务端命令请求和服务端对客户端的命令回复。
obuf_soft_limit_reached_time 该属性用来计算客户端的空转时间idle,即距离客户端与服务端的最后一次交互,已经过去的了多少秒,使用client list命令查看时,idle域记录了这个秒数(idle域以秒为单位,记录了obuf_soft_limit_reached_time参数)

6.3 客户端创建与关闭

6.3.1 客户端创建

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

举个例子,假设当前有c1和c2两个普通客户端正在连接服务器,那么当一个新的普通客户端c3连接到服务器之后,服务器会将c3所对应的客户端状态添加到clients链表的末尾,如图:

Redis,性能加速的催化剂(二)_服务器_38

注意:上图中用虚线包围的就是服务器为c3新创建的客户端状态。

6.3.2 客户端关闭

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

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

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

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

4)如果用户为服务器设置了timeout配置选项,郡么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭。

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

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

七、服务端

Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。

7.1 初始化服务器

7.1.1 初始化服务器状态结构

void initServerConfig(void){
//设置服务器的运行id
getRandomHexChars(server,runid,REDIS_RUN_ID_SIZE);
//为运行id加上结尾字符串
server.runid[REDIS_RUN_ID_SIZE] = '\0';
//设置默认配置文件路径
server.configfile=null;
//设置默认服务器频率
server.hz=REDIS_DEFAULT_HZ;
//设置服务器的运行架构
server.arch_bits=(sizeof(long)==8) ? 64 : 32;
//设置默认服务器端口号
server.port=REDIS_SERVERPORT;
}

关于initServerConfig函数完成的主要工作:

设置服务器的运行ID:getRandomHexChars(server,runid,REDIS_RUN_ID_SIZE);

设置服务器的默认运行频率:server.hz=REDIS_DEFAULT_HZ;

设置服务器的默认配置文件路径:server.configfile=null;

设置服务器的运行架构:server.arch_bits=(sizeof(long)==8) ? 64 : 32;

设置服务器的默认端口号:server.port=REDIS_SERVERPORT;

设置服务器的默认RDB持久化条件和AOF持久化条件,初始化服务器的LRU时钟,创建命令表。

initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表之外, initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lu环境、共享对象这些数据结构在之后的步骤才会被创建出来。

当initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段一载人配置选项。

7.1.2 载入配置选项(载入用户指定的配置选项+server状态更新)

分为两个部分,即“载入用户指定的配置选项+server状态更新”。

(1)载入用户指定的配置选项

在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。举个例子,如果我们在终端中输入:

​$ redis-server --port 10086

那么我们就通过给定配置参数的方式,修改了服务器的运行端口号。另外,如果我们在端中输入:

$ redis-server redis.conf

井且redis. conf文件中包含以下内容:

# 将redis服务器的数据库数量设置为32个(默认16个 db0-db15)
database 32
# 关闭RDB文件的压缩功能
rdbcompression no
那么我们就通过指定配

置修改了服务器的数据库数量,以及RDB持久化模块的压缩功能。

(2)server状态更新

Redis服务器在用initServerConfig函数初始化完 server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。

例如,在初始化 server变量时,程序会为决定服务器端口号的port属性设置默认值,为数据库数量设置默认值:

​void initServerConfig(void){
   server.port = REDIS_SERVERPORT; //默认为6379端口     上面用户设置为10086
   server.dbnum = REDIS_DEFAULT_DBNUM; //默认为16个数据库   上面用户设置为32个
}

这里,因为用户在启动服务器时为配置选项port指定了新值10086,dbnum指定新值为32,所以server port属性的值就会被更新为10086,server donum属性的值就会被更新为32,这就是server状态更新;所以,服务器的端口号从默认的6379变为用户指定的10086,数据库数量从默认的16个变为用户指定的32个,这就是让用户指定的配置选项生效。

所以,用户指定配置选项和server状态更新是同时进行的,所以这里放在同一个小节中。

实际上,其他配置选项相关的服务器状态属性的情况与上面列举的port属性和dbnum属性一样,如果用户为这些属性的相应选项指定了新的值,那么服务器就使用用户指定的值来更新相应的属性;同理,如果用户没有为属性的相应选项设置新的值,那么服务器就沿用之前initserverconfig函数为属性设置的默认值。

服务器在载入用户指定的配置选项,井对 server状态进行更新之后,服务器就可以进入初始化的第三个阶段一初始化服务器数据结构。

7.1.3 初始化服务器数据结构(initServer()函数)

注意,在Redis服务器的初始化分为两步,initServerConfig函数主要负责初始化一般属性,initServer函数主要负责初始化数据结构(此外,initServer函数还完成一些重要的设置动作)。

(1)initServer函数负责初始化数据结构

初始化server.clients链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个 redisClient结构实例;

初始化server.db数组,数组中包含了服务器的所有数据库;

初始化sorver, pubaub channela字典,该字典用于保存频道订阅信息;

初始化server, pubsub patterns链表,该链表用于保存模式订阅信息;

初始化server.lua环境,该环境用于执行Lua脚本;

初始化server.showlog属性,该属性用于保存慢查询日志。

(2)initServer函数负责完成重要的设置动作

为服务器设置进程信号处理器

创建共享对象:这些对象包含 Redis服务器经常用到的一些值,服务器通过重用这些共享对象来避免反复创建相同的对象。

打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接。

为serverCron函数创建时间事件 ,等待服务器正式运行时执行serverCron函数。

如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备

初始化服务器的后台I/O模块(bio)为将来的I/O操作做好准备

小结:当initServer函数执行完毕之后,服务器将用ASCII字符在日志中打印出Redis的图标,以及Redis版本信息,如下:
Redis,性能加速的催化剂(二)_客户端_39

7.1.4 还原数据库状态

在完成了对服务器状态 server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。

根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所口如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态

相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原

当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载人文件并还原数据库状态所耗费的时长

Redis,性能加速的催化剂(二)_数据库_40

7.1.5 执行事件循环

在初始化的最后一步,服务器将打印出以下日志:

Redis,性能加速的催化剂(二)_服务器_41

并开始执行服务器的事件循环(loop)。

7.1.6 一图小结(这个很重要,演示了redis服务端初始化)

Redis,性能加速的催化剂(二)_服务器_42

至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求,且看下面的“7.2 命令请求的执行过程”。

7.2 命令请求的执行过程

一图预览,redis整个命令请求执行过程。

Redis,性能加速的催化剂(二)_服务器_43

一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。举个例子,如果我们使用客户端执行以下命令:

redis > SET KEY VALUE
OK

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

1)客户端向服务器发送命令请求 SET KEY VALUE(上图中的“发送命令请求”);

2)服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK(上图中的“读取命令请求、执行命令请求”);

3)服务器将命令回复OK发送给客户端(上图中的“命令回复发送给客户端”);

4)客户端接收服务器返回的命令回复OK,并将这个回复打印给用户查看(上图中的“命令回复发送给客户端”)。

7.2.1 发送命令请求

当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器,如下图:
Redis,性能加速的催化剂(二)_客户端_44

7.2.2 读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

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

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

3)调用命令执行器,执行客户端指定的命令

如图:
Redis,性能加速的催化剂(二)_客户端_45

步骤一和步骤二在图中演示了,步骤三即“调用命令执行器,执行客户端指定的命令”且看“7.2.3 执行命令请求”。

7.2.3 执行命令请求

(1)查找命令实现函数

命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表(common table)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面。注意,这是两个步骤,

1)在命令表(common table)中查找参数所指定的命令
2)将找到的命令保存到客户端状态的cmd属性里面

如图:

Redis,性能加速的催化剂(二)_原力计划_46

(2)执行预备操作

到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但是在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:

a.检查客户端状态的cmd指针是否指向NULL,如果是的话,那么说明用户输入命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。

b.根据客户端cmd属性指向的 redisCommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。

c.检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令,如果未通过身份验证的客户端试图执行除AUH命令之外的其他命令,那么服务器将向客户端返回一个错误。

d.如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返闻一个错误。

e.如果服务器上一次执行 BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgbrave-error功能,而且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误

f.如果客户端当前正在用 SUBSCRIBE命令订阅频道,或者正在用 PSUBSCRIBE命令订阅模式,那么服务器只会执行客户端发来的 SURSCRIBE、 PSUBSCRIBE、UNSUBSCRIBE、 PUNSUBSCRIBE四个命令,其他命令都会被服务器拒绝

g.如果服务器正在进行数据载人,那么客户端发送的命令必须带有1标识(比如INFO、 SHUTDOWN、 PUBLSH等等 )才会被服务器执行,其他命令都会被服务器

h.如果服务器因为执行Lua脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和 SCRIPT KILL命令,其他命令都会被服务器拒绝。

i.如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、 DISCARD、MULTI、 WATCH四个命令,其他命令都会被放进事务队列中

j.如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。当完成了以上预备操作之后,服务器就可以开始真正执行命令了

注意:以上只列出了服务器在单机模式下执行命令时的检查操作,当服务器在复制或者集群模式下执行命令时,预备揉作还会更多一些。

(3)调用命令实现函数

在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端状态的argv属性和argc属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:

因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里面了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。如图:

Redis,性能加速的催化剂(二)_服务器_47

(4)执行后续工作

在执行完实现函数之后,服务器还需要执行一些后续工作:

a.如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志

b.根据刚刚执行命令所耗费的时长,更新被执行命令的 redisCommand结构的milliseconde属性,并将命令的redisCommand结构的ca11s计数器的值增一

c.如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写人到AOF缓冲区里面

d.如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器

当以上操作都执行完了之后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。

7.2.4 命令回复发送给客户端

被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里面(buf属性和 reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端.

对于前面SET命令的例子来说,函数调用 setCommand(client)将产生一个 ”+OK r\n“回复,这个回复会被保存到客户端状态的buf属性里面,如图:
Redis,性能加速的催化剂(二)_数据库_48

7.2.5 客户端接收并打印命令回复

当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看(假设我们使用的是Redis自带的reds-cli客户端),如图148所示:

Redis,性能加速的催化剂(二)_原力计划_49

7.2.6 小结(执行命令请求整个过程,这个很重要,这三个图搞懂了,整个redis服务器执行命令就八九不离十了)

执行命令请求整个过程,且看图1:

Redis,性能加速的催化剂(二)_redis_50

实际上,对于“步骤3执行命令请求”的四个步骤3.1 3.2 3.3 3.4 可以画在一个图中,得到图2:

Redis,性能加速的催化剂(二)_原力计划_51

将步骤2、步骤3、步骤4放在一起,得到图3:

Redis,性能加速的催化剂(二)_客户端_52

至此,Redis客户端和服务端整个命令执行过程完毕。

八、尾声

本文介绍单机版redis底层原理,分为六个部分,

第二部分:介绍redis服务器中的数据库,分为三个小节:redis服务器中数据库、redis增删查改底层实现、redis失效;

第三部分:介绍redis RDB持久化,分为四个小节:RDB文件生成载入、RDB文件自动间隔保存、RDB文件结构与RDB文件的分析;

第四部分:介绍redis AOF持久化,分为两个小节:从RDB持久化到AOF持久化、AOF持久化的实现;

第五部分:介绍redis两种持久化对比,分为三个小节:定义、优缺点和应用;

第六部分:介绍redis客户端,分为三个小节:redis客户端-服务器架构、redis客户端属性和redis客户端创建与关闭;

第七部分:介绍redis服务端,分为两个小节:redis服务端初始化、redis服务端处理命令请求执行过程。

均侧重redis的底层实现,侧重使用图解实现。

天天打码,天天进步!