Redis,性能加速的催化剂
Posted 毛奇志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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服务器中数据库
redis有五种基本类型,每一个类型都可以做增删查改操作。
2.1 初识redis服务器中数据库
让我们来见识一下redis中的数据库结构(这是整篇博客的基础,后面的都是围绕redisServer这个结构来讲解的)
struct redisServer{
// ...
//一个数组,保存着服务器中的所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
// ...
};
对于上述代码和示意图的解释是:Redis服务器将所有数据库都保存在服务器状态redis.h/ redisServer结构的db数组中,db数组的每个项都是一个 redis.h/redisDb结构,每个 redisDb结构(即代码中的redisDb *db)代表一个数据库,同时,程序会根据服务器状态的donum属性(即代码中的int dbnum)来决定应该创建多少个数据库。num属性的值由服务器配置的 database选项决定,默认情况下,该选项的值为16,所以 Redis服务器默认会创建16个数据库:
切换数据库:对于redis默认的16个数据库(db0-db15),在操作的时候可以选择将数据(五种类型均可)存放在哪个数据库中,只要将某个数据库设置成当前数据库就好(select 数目),宏观命令如下:
底层变化:
2.2 从底层原理图讲解redis增删改查操作
Redis是一个键值对(key- value pair)数据库服务器,服务器中的每个数据库都由一个 redis.h/ redisDb结构表示,其中, redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间( key space):
键空间和用户所见的数据库是直接对应的:
1)键空间的键也就是数据库的键,每个键都是一个字符串对象;
2)键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象集合对象和有序集合对象中的任意一种Redis对象。
如图:
对于上图解释: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 添加新键
对于上图解释:添加新键,StringObject类型 <key,value>=<“date”,“2020/02/09”>
2.2.2 删除键
对于上图解释:删除新键,删除key为“book”的键值对
2.2.3 更新键
对于上图解释:更新键,更新key为“message”的键值对
2.2.4 对键取值
对于上图解释:对键取值,获取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,这里展示四个命令底层关系,如图:
我们可以看到,四个命令底层关系:四个设置生存时间的命令,底层最终都是使用PEXPIREAT命令去实现的。
2.3.2 生存时间的底层保存(过期字典)
redis是基于key-value存储的一个非关系型数据库,对于每一个记录的key,都有一个生存时间TTL,上面介绍了指定key的生存时间的读写,那么,redis中每一个key的生存时间是底层是如何存储的呢?答案是使用“过期字典”存储。
过期字典引入:redisDB结构的expires字典保存了数据库中所有键的过期时间,这个expires字典就是过期字典。
过期字典的键:是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)。
过期字典的值:是一个long long类型的整数 ,这个整数保存了键所指向的数据库键的过期时间,即—个毫秒精度的UNIX时间戳。
展示了一个带有过期字典的数据库例子,在这个例子中,键空间保存了数据库中的所有键值对,而过期时间则保存了数据库中所有按键值对的过期时间。
生存时间(过期时间)的添加和删除略过。
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持久化,如果选择呢?如图:
对于上图的解释是:因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
1)如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态;
2)只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
3.1 RDB文件的生成与载入(内存上的redis数据库 --> 磁盘上的RDB文件、磁盘上的RDB文件 -->内存上的redis数据库)
本节称为RDB文件的生成与载入,实际包括两个部分的内容:RDB文件的生成+RDB文件的载入,如下图所示:
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属性)
3.3 RDB文件结构
让我们来看一下磁盘上这个神秘的RDB文件:
针对上图给出表格(各个部分含义用表格看清晰些)
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四个部分的内容计算得出的 |
对于上图的解释:上图同时给出了“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字节,那么这个字符串会被压缩之后再保存。
一图小结:
对上图的理解:左边存放数字,中间因为字符串长度为21>20,所以压缩,右边因为字符串长度为5<20,不压缩。
2)列表对象
如果TYPE的值为 REDIS_RDB_TYPE_LIST,那么 value保存的就是一个 REDIS_ENCONDING_LINKEDLIST编码的对象,一图小结:
3)集合对象
如果TYPE的值为REDIS_RDB_TYPE_SET,那么 value保存的就是一个 REDIS_ENCODING_HT编码的集合对象,一图小结:
4)哈希表对象
如果TYPE的值为REDIS_RDB_TYPE_HASH,那么value保存的就是一个 REDIS_ENCODING_HT编码的集合对象,一图小结:
5)有序集合对象
如果TYPE的值为 REDIS_RDB_TYPE_ZSET,那么value保存的就是一个REDIS_ENCODING_SKIPLIST编码的有序集合对象,一图小结:
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文件
3.4.2 包含任何键值对的RDB文件
3.4.3 包含带有过期时间的字符串键的RDB文件
3.4.4 包含一个集合键的RDB文件
四、AOF持久化(核心:磁盘上的AOF文件)
4.1 从RDB持久化到AOF持久化
除了RDB持久化功能之外, Redis还提供了AOF( Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如下图:
关于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客户端-服务器的结构如下图:
通过使用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链表来完成,如下图:
6.2 客户端属性
“6.1 Redis客户端-服务器架构”介绍了客户端的各个属性,本节详细介绍。
客户端状态包含的属性可以分为两类:
一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性,本节详细介绍。
另外一类是和特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_keys属性等等,不介绍,略过。
先上一张图,redis客户端运行时,输入“client list”打印客户端状态:
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命令可以为客户端设置一个名字,让客户端的身份变得更清晰,如图:
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)
输入缓冲区querybuf的大小会根据输入内容动态地缩小或者扩大,但是它的最大大小不能超过1GB,否则服务器关闭这个客户端。
6.2.5 命令与命令参数(argv argc)
6.2.6 命令的实现函数(redisCommand)
当服务器从协议内容中分析并得到argv属性和argc属性的值之后,redis服务器将根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。
6.2.7 输出缓冲区(buf bufpos)(固定缓冲区+可变缓冲区)
6.2.8 身份验证(authenticated)
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域以 以上是关于Redis,性能加速的催化剂的主要内容,如果未能解决你的问题,请参考以下文章 |