redis设计与实现

参考系列文章:https://www.cnblogs.com/ysocean/category/1221478.html

1、redis基础概念
1、redis类型

redis是以<key,value>的形式存储的,其中的value可以有多种的数据类型,于Memcache不同。redis主要有5大基本类型 :String、list、hash、set、zset

2、 redis的五大基本类型 :String ,hash ,list,set,zset

(1)String的使用:最常用,其中的value支持二进制安全,最大可达到512M,

set k1 v1

(2)hash的使用:类似于java中的Map<object,Map<object,value>>

hset k1 k2 v1

(3)list的使用:底层是一个链表的形式,功能类似于栈,只能push,pop

lpush list1 1 2 3 4 
	lpop list1

(4)set的使用:具备去重的功能,当表中存在的时候,自动忽略,不存在的时候插入。

sADD set1 1 2 3 4 5 6 7

(5)zset的使用:具备排序的set(其中为每个值设置一个权重)

zadd sorta 1 a 2 b 3 d 4 c
2、 redis底层数据结构(用这5大基本类型来表示key和value)
 <1>SDS
 <2>、链表
 <3>、字典
 <4>、跳跃表
 <5>、整数集合
 <6>、压缩列表
 <7>long型整数
 <8>embstr
(1)首先介绍一下redis的字符串结构

我们知道redis是使用C语言编写的,但是对于Redis的字符串,却不是 C 语言中的字符串
(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。

SDS的定义:

struct sdshdr{
		 //记录buf数组中已使用字节的数量
		 //等于 SDS 保存字符串的长度
		 int len;
		 //记录 buf 数组中未使用字节的数量
		 int free;
		 //字节数组,用于保存字符串
		 char buf[];
	}

在这里插入图片描述

我们看上面对于 SDS 数据类型的定义:

1、len 保存了SDS保存字符串的长度
  2、buf[] 数组用来保存字符串的每个元素
  3、free 记录了 buf 数组中未使用的字节数量

上面的定义相对于 C 语言对于字符串的定义,多出了 len 属性以及 free 属性。为什么不使用C语言字符串实现,而是使用 SDS呢?这样实现有什么好处?

①、常数复杂度获取字符串长度

由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,
时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。

②、杜绝缓冲区溢出

我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,
会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

③、减少修改字符串的内存重新分配次数

C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。

而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:

1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

2、惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,
当我们有需要时,也可以手动释放这些未使用的空间。)

④、二进制安全

因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,
并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。

⑤、兼容部分 C 字符串函数

虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。

(2)数据结构

<1> 链表

链表是一种常用的数据结构,C 语言内部是没有内置这种数据结构的实现,所以Redis自己构建了链表的实现。(而且采用的是一个双向的链表)

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

通过多个 listNode 结构就可以组成链表,这是一个双端链表,Redis还提供了操作链表的数据结构:
在这里插入图片描述

typedef struct list{
	 //表头节点
	 listNode *head;
	 //表尾节点
	 listNode *tail;
	 //链表所包含的节点数量
	 unsigned long len;
	 //节点值复制函数
	 void (*free) (void *ptr);
	 //节点值释放函数
	 void (*free) (void *ptr);
	 //节点值对比函数
	 int (*match) (void *ptr,void *key);
}list;

Redis链表特性:

①、双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。

②、无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。

③、带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。

④、多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。

<2> 字典

字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。
C 语言中没有内置这种数据结构的实现,所以字典依然是 Redis自己构建的。Redis 的字典使用哈希表作为底层实现,

哈希表结构定义:

typedef struct dictht{
	 //哈希表数组
	 dictEntry **table;
	 //哈希表大小
	 unsigned long size;
	 //哈希表大小掩码,用于计算索引值
	 //总是等于 size-1
	 unsigned long sizemask;
	 //该哈希表已有节点的数量
	 unsigned long used;
 
}dictht

在这里插入图片描述
key 用来保存键,val 属性用来保存值,值可以是一个指针,也可以是uint64_t整数,也可以是int64_t整数。

注意这里还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。这里采用的便是链地址法,
通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突。

<3> 跳跃表(skiplist)

跳跃表:http://blog.jobbole.com/111731/

要注意最重要一个是跳跃表跟二叉查找树的区别:跳跃表是维持结构平衡的成本比较低,完全依靠随机。而二叉查找树在多次插入或者删除之后可能就要Rebalance了。
Redis当中的Sorted-set不谋而合。而Sorted-set这种有序集合,正是对于跳跃表的改进和应用。
在这里插入图片描述
跳跃表(skiplist)是一种有序数据结构—主要用在有序链表中,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。具有如下性质:

1、由很多层结构组成;

2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;

3、最底层的链表包含了所有的元素;

4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);

5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;

<4>整数集合

整数集合(intset)是Redis用于保存整数值的集合抽象数据类型,它可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中不会出现重复元素。

typedef struct intset{
	 //编码方式
	 uint32_t encoding;
	 //集合包含的元素数量
	 uint32_t length;
	 //保存元素的数组
	 int8_t contents[];
 
}intset;

整数集合的每个元素都是 contents 数组的一个数据项,它们按照从小到大的顺序排列,并且不包含任何重复项。

length 属性记录了 contents 数组的大小。

需要注意的是虽然 contents 数组声明为 int8_t 类型,但是实际上contents 数组并不保存任何 int8_t 类型的值,其真正类型有 encoding 来决定。

①、升级

当我们新增的元素类型比原集合元素类型的长度要大时,需要对整数集合进行升级,才能将新元素放入整数集合中。具体步骤:

1、根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。

2、将底层数组现有的所有元素都转成与新元素相同类型的元素,并将转换后的元素放到正确的位置,放置过程中,维持整个元素顺序都是有序的。

3、将新元素添加到整数集合中(保证有序)。

升级能极大地节省内存。

②、降级

整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

<5> 压缩列表

压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
在这里插入图片描述
压缩列表每个节点的结构由这三部分组成:
①、previous_entry_ength:记录压缩列表前一个字节的长度。previous_entry_ength的长度可能是1个字节或者是5个字节,如果上一个节点的长度小于254,
则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于254,则previous length的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。
利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。

②、encoding:节点的encoding保存的是节点的content的内容类型以及长度,encoding类型一共有两种,一种字节数组一种是整数,encoding区域长度为1字节、2字节或者5字节长。

③、content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定。

3、redis对象
(1)redisObject 结构

Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redisObject 结构来表示:

typedef struct redisObject{
		 //类型
		 unsigned type:4;
		 //编码
		 unsigned encoding:4;
		 //指向底层数据结构的指针
		 void *ptr;
		 //引用计数
		 int refcount;
		 //记录最后一次被程序访问的时间
		 unsigned lru:22;
	 
	}robj

① type类型(5大数据类型)
在这里插入图片描述

注意:在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合,哈希,有序集合对象,所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,我们说一个键为集合键时,表示的是这个键对应的值为集合对象。

*②、encoding 属性和 prt 指针
对象的 prt 指针指向对象底层的数据结构,而数据结构由 encoding 属性来决定。

在这里插入图片描述

而每种类型的对象都至少使用了两种不同的编码:
在这里插入图片描述

(2)五大数据类型
<1>字符串对象(String)

字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。

①、编码

字符串对象的编码可以是int,raw或者embstr。

1、int 编码:保存的是可以用 long 类型表示的整数值。

2、raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

3、embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

int 编码是用来保存整数值,raw编码是用来保存长字符串,而embstr是用来保存短字符串。其实 embstr 编码是专门用来保存短字符串的一种优化编码,raw 和 embstr 的区别:
在这里插入图片描述在这里插入图片描述

embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。
因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,
整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。

<2> 列表对象(list)

list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。

①、编码

列表对象的编码可以是 ziplist(压缩列表) 和 linkedlist(双端链表)。

比如我们执行以下命令,创建一个 key = ‘numbers’,value = ‘1 three 5’ 的三个值的列表。

rpush numbers 1 "three" 5

在这里插入图片描述在这里插入图片描述
当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

1、列表保存元素个数小于512个
2、每个元素长度小于64字节

不能满足这两个条件的时候使用 linkedlist 编码。

上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置。

<3>哈希对象(hash)

哈希对象的键是一个字符串类型,值是一个键值对集合。也就是类比于Map<object,Map<object,Value>>
①、编码
  哈希对象的编码可以是 ziplist 或者 hashtable(底层是字典)。
  当使用ziplist,也就是压缩列表作为底层实现时,新增的键值对是保存到压缩列表的表尾。比如执行以下命令:

hset profile name "Tom"
hset profile age 25
hset profile career "Programmer"

如果使用ziplist,profile 存储如下:

在这里插入图片描述
 hashtable 编码的哈希表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。
在这里插入图片描述
  在前面介绍压缩列表时,我们介绍过压缩列表是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,相对于字典数据结构,压缩列表用于元素个数少、元素长度小的场景。其优势在于集中存储,节省空间。

②、编码转换

和上面列表对象使用 ziplist 编码一样,当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

1、列表保存元素个数小于512个

2、每个元素长度小于64字节

不能满足这两个条件的时候使用 hashtable 编码。第一个条件可以通过配置文件中的 set-max-intset-entries 进行修改。

<4> 集合对象(set)

集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。

①、编码

集合对象的编码可以是 intset 或者 hashtable。

intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。

hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。
这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。

SADD numbers 1 3 5
SADD Dfruits "apple" "banana" "cherry"

在这里插入图片描述
在这里插入图片描述
②、编码转换

当集合同时满足以下两个条件时,使用 intset 编码:

1、集合对象中所有元素都是整数

2、集合对象所有元素数量不超过512

不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置

<5> 有序集合(zset)

和上面的集合对象相比,有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。

在这里插入图片描述
①、编码

有序集合的编码可以是 ziplist 或者 skiplist。

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。
并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。
ZADD price 8.5 apple 5.0 banana 6.0 cherry

skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:

typedef struct zset{
     //跳跃表
     zskiplist *zsl;
     //字典
     dict *dice;
} zset;

字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。

这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。

说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。

②、编码转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

1、保存的元素数量小于128;

2、保存的所有元素长度都小于64字节。

不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。

4、5种类型应用场景

对于string 数据类型,因为string 类型是二进制安全的(因为采用的是length来记录长度,而不是采用空格,这样就可以避免图片中也有空格而出现不安全),可以用来存放图片,视频等内容,另外由于Redis的高性能读写功能,
而string类型的value也可以是数字,可以用作计数器(INCR,DECR),比如分布式环境中统计系统的在线人数,秒杀等。(用的最多的)

对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息。

对于 list 数据类型,可以实现简单的消息队列,另外可以利用lrange命令,做基于redis的分页功能

对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。

5、内存回收和内存共享
(1)内存回收

前面讲 Redis 的每个对象都是由 redisObject 结构表示:

typedef struct redisObject{
	 //类型
	 unsigned type:4;
	 //编码
	 unsigned encoding:4;
	 //指向底层数据结构的指针
	 void *ptr;
	 //引用计数
	 int refcount;
	 //记录最后一次被程序访问的时间
	 unsigned lru:22;
 
}robj	

其中关键的 type属性,encoding 属性和 ptr 指针都介绍过了,那么 refcount 属性是干什么的呢?

因为 C 语言不具备自动回收内存功能,那么该如何回收内存呢?于是 Redis自己构建了一个内存回收机制,通过在 redisObject 结构中的 refcount 属性实现。这个属性会随着对象的使用状态而不断变化:

1、创建一个新对象,属性 refcount 初始化为1
2、对象被一个新程序使用,属性 refcount 加 1
3、对象不再被一个程序使用,属性 refcount 减 1
4、当对象的引用计数值变为 0 时,对象所占用的内存就会被释放。

在 Redis 中通过如下 API 来实现:
  在这里插入图片描述

学过Java的应该知道,引用计数的内存回收机制其实是不被Java采用的,因为不能克服循环引用的例子(比如 A 具有 B 的引用,B 具有 C 的引用,C 具有 A 的引用,除此之外,这三个对象没有任何用处了),
这时候 A B C 三个对象会一直驻留在内存中,造成内存泄露。那么 Redis 既然采用引用计数的垃圾回收机制,如何解决这个问题呢?

在 redis.conf 配置文件中,在 MEMORY MANAGEMENT 下有个 maxmemory-policy 配置:

maxmemory-policy :当内存使用达到最大值时,redis使用的清除策略。有以下几种可以选择:

1)volatile-lru 利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )

2)allkeys-lru 利用LRU算法移除任何key

3)volatile-random 移除设置过过期时间的随机key

4)allkeys-random 移除随机key

5)volatile-ttl 移除即将过期的key(minor TTL)

6)noeviction noeviction 不移除任何key,只是返回一个写错误 ,默认选项

通过这种配置,也可以对内存进行回收。

(2)内存共享

refcount 属性除了能实现内存回收以外,还能用于内存共享。

比如通过如下命令 set k1 100,创建一个键为 k1,值为100的字符串对象,接着通过如下命令 set k2 100 ,创建一个键为 k2,值为100 的字符串对象,那么 Redis 是如何做的呢?

1、将数据库键的值指针指向一个现有值的对象

2、将被共享的值对象引用refcount 加 1

在这里插入图片描述

注意:Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。
对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。

虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。

(3)对象的空转时长

在 redisObject 结构中,前面介绍了 type、encoding、ptr 和 refcount 属性,最后一个 lru 属性,该属性记录了对象最后一次被命令程序访问的时间。

使用 OBJECT IDLETIME 命令可以打印给定键的空转时长,通过将当前时间减去值对象的 lru 时间计算得到。
lru 属性除了计算空转时长以外,还可以配合前面内存回收配置使用。如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用超过maxmemory指定的值时,
Redis会优先选择空转时间最长的对象进行释放。

6、持久化(RDB和AOF)

我们知道redis之所以比mysql等其他关系型数据库快,是因为数据保存在了内存中,但是这样也带来了一个问题,那就是当电脑断电了怎么办,因为内存的数据当你电脑断电的时候就清除掉。这时候就需要把数据持久化到我们的硬盘中
这样当断电或者出现异常的时候可以用来恢复数据或者现场。redis采用了两种方式来实现,一种是RDB,另一种是AOF。

(1)RDB
<1>什么是RDB:
RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照(某一时刻)写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。
<2>RDB的触发方式:
RDB 有两种触发方式,分别是自动触发和手动触发。

自动触发:(配置文件中设置)

RDB配置文件:

在这里插入图片描述
①、save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave(这个命令下面会介绍,手动触发RDB持久化的命令)

默认如下配置:

	save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
	save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
	save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存

当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。可以直接一个空字符串来实现停用:save “”
②、stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。
如果Redis重启了,那么又可以重新开始接收数据了

③、rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。

④、rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

⑤、dbfilename :设置快照的文件名,默认是 dump.rdb

⑥、dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。默认是和当前配置文件保存在同一目录。

也就是说通过在配置文件中配置的 save 方式,当实际操作满足该配置形式时就会进行 RDB 持久化,将当前的内存快照保存在 dir 配置的目录中,文件名由配置的 dbfilename 决定。

手动触发:

手动触发Redis进行RDB持久化的命令有两种:

1、save

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。

显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷,为了解决此问题,Redis提供了第二种方式。

2、bgsave

执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。

基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。

ps:执行 flushall 命令,也会产生dump.rdb文件,但里面是空的,无意义

<3> RDB的优势和劣势

①、优势

1.RDB是一个非常紧凑(compact)的文件,它保存了redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
2.生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

②、劣势

1、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作(内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑),频繁执行成本过高(影响性能)
2、RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题(版本不兼容)
3、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改(数据有丢失)

<4>RDB的自动保存原理

Redis有个服务器状态结构:

struct redisService{
     //1、记录保存save条件的数组
     struct saveparam *saveparams;
     //2、修改计数器
     long long dirty;
     //3、上一次执行保存的时间
     time_t lastsave;
 
}

①、首先看记录保存save条件的数组 saveparam,里面每个元素都是一个 saveparams 结构:

struct saveparam{
	 //秒数
	 time_t seconds;
	 //修改数
	 int changes;
};

前面我们在 redis.conf 配置文件中进行了关于save 的配置:

	save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
	save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
	save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存

那么服务器状态中的saveparam 数组将会是如下的样子:
在这里插入图片描述

②、dirty 计数器和lastsave 属性

dirty 计数器记录距离上一次成功执行 save 命令或者 bgsave 命令之后,Redis服务器进行了多少次修改(包括写入、删除、更新等操作)。

lastsave 属性是一个时间戳,记录上一次成功执行 save 命令或者 bgsave 命令的时间。

通过这两个命令,当服务器成功执行一次修改操作,那么dirty 计数器就会加 1,而lastsave 属性记录上一次执行save或bgsave的时间,Redis 服务器还有一个周期性操作函数 severCron ,默认每隔 100 毫秒就会执行一次,该函数会遍历并检查 saveparams 数组中的所有保存条件,只要有一个条件被满足,那么就会执行 bgsave 命令。

执行完成之后,dirty 计数器更新为 0 ,lastsave 也更新为执行命令的完成时间。

(2)AOF

RDB 持久化存在一个缺点是一定时间内做一次备份,如果redis意外down掉的话,就会丢失最后一次快照后的所有修改(数据有丢失)。对于数据完整性要求很严格的需求,怎么解决呢?这时候就要用AOF,这个文件将命令也保存进文件。

<1>什么是AOF

Redis的持久化方式之一RDB是通过保存数据库中的键值对来记录数据库的状态。而另一种持久化方式 AOF 则是通过保存Redis服务器所执行的写命令来记录数据库状态。

<2>AOF配置文件

在这里插入图片描述
①、appendonly:默认值为no,也就是说redis 默认使用的是rdb方式持久化,如果想要开启 AOF 持久化方式,需要将 appendonly 修改为 yes。

②、appendfilename :aof文件名,默认是"appendonly.aof"

③、appendfsync:aof持久化策略的配置;

no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;

always表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低;

everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。通常选择 everysec ,兼顾安全性和效率。

④、no-appendfsync-on-rewrite:在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。
如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,
建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。默认值为no。

⑤、auto-aof-rewrite-percentage:默认值为100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。
当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。

⑥、auto-aof-rewrite-min-size:64mb。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。

⑦、aof-load-truncated:aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象
redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,
用户必须手动redis-check-aof修复AOF文件才可以。默认值为 yes。

<3> AOF重写

由于AOF持久化是Redis不断将写命令记录到 AOF 文件中,随着Redis不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。为了解决这个问题,Redis新增了重写机制,
当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令 bgrewriteaof 来重新。

如果不进行 AOF 文件重写,那么 AOF 文件将保存四条 SADD 命令,如果使用AOF 重写,那么AOF 文件中将只会保留下面一条命令:

sadd animals "dog" "tiger" "panda" "lion" "cat"

也就是说 AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。

AOF 文件重写触发机制:通过 redis.conf 配置文件中的 auto-aof-rewrite-percentage:默认值为100,以及auto-aof-rewrite-min-size:64mb 配置,也就是说默认Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。

这里再提一下,我们知道 Redis 是单线程工作,如果 重写 AOF 需要比较长的时间,那么在重写 AOF 期间,Redis将长时间无法处理其他的命令,这显然是不能忍受的。Redis为了克服这个问题,解决办法是将 AOF 重写程序放到子程序中进行,这样有两个好处:
①、子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理其他命令。
②、子进程带有父进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。

使用子进程解决了上面的问题,但是新问题也产生了:因为子进程在进行 AOF 重写期间,服务器进程依然在处理其它命令,这新的命令有可能也对数据库进行了修改操作,使得当前数据库状态和重写后的 AOF 文件状态不一致。

为了解决这个数据状态不一致的问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区是在创建子进程后开始使用,当Redis服务器执行一个写命令之后,就会将这个写命令也发送到 AOF 重写缓冲区。当子进程完成 AOF 重写之后,就会给父进程发送一个信号,父进程接收此信号后,就会调用函数将 AOF 重写缓冲区的内容都写到新的 AOF 文件中。

这样将 AOF 重写对服务器造成的影响降到了最低。

<4>AOF优缺点

优点:

①、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。
 ②、AOF 文件使用 Redis 命令追加的形式来构造,因此,即使 Redis 只能向 AOF 文件写入命令的片断,使用 redis-check-aof 工具也很容易修正 AOF 文件。
③、AOF 文件的格式可读性较强,这也为使用者提供了更灵活的处理方式。例如,如果我们不小心错用了 FLUSHALL 命令,在重写还没进行时,我们可以手工将最后的 FLUSHALL 命令去掉,然后再使用 AOF 来恢复数据。

缺点:

①、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大。
②、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。但在 Redis 的负载较高时,RDB 比 AOF 具好更好的性能保证。
③、RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 方式更健壮。官方文档也指出,AOF 的确也存在一些 BUG,这些 BUG 在 RDB 没有存在。

那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?

如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,而且使用 RDB 还可以避免 AOF 一些隐藏的 bug;
否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。Redis后期官方可能都有将两种持久化方式整合为一种持久化模型。

7、Redis三种类型架构

redis主要有三种类型的架构:standalone类型架构(单机模式)、sentinel类型架构(哨兵模式)、cluster类型架构(集群模式)

(1)主从复制(主要是对任务进行分工):

前面介绍Redis,我们都在一台服务器上进行操作的,也就是说读和写以及备份操作都是在一台Redis服务器上进行的,那么随着项目访问量的增加,对Redis服务器的操作也越加频繁,虽然Redis读写速度都很快,
但是一定程度上也会造成一定的延时,那么为了解决访问量大的问题,通常会采取的一种方式是主从架构Master/Slave,Master 以写为主,Slave 以读为主,Master 主节点更新后根据配置,自动同步到从机Slave 节点。

接下来我们就来介绍如何进行主从架构的搭建。

ps:这里我是在一台机器上模拟多个Redis服务器,与实际生产环境中相比,基本配置都是一样,仅仅是IP地址和端口号变化。

架构图如下:
在这里插入图片描述

注:这是一种主模式负责的工作和从模式负责的工作不一致

(2)主从模式(实现高可用的架构):

主从模式是用来实现高可用的,如下图所示,主从模式是主模式和从模式负责同样的工作。当主模式崩了,Sentinel切换到从模式继续任务的执行。
在这里插入图片描述

(3)三种模式的结构和应用场景

(1) standalone类型架构
在这里插入图片描述
用于可穿透业务场景,如后端有DB存储,脱机影响不大的应用。

(2) sentinel类型架构
在这里插入图片描述
用于高可用需求场景,可用于高可用Cache,存储等场景。 内存/QPS受限于单机。

(3) cluster类型架构
在这里插入图片描述
用于高可用需求场景,可用于大数据量高可用Cache/存储等场景。 内存/QPS不受限于单机,可受益于分布式集群高扩展性。

8、redis服务器的单线程单进程模型
8.1 服务器-客户端模型

redis服务器是典型的一对多服务器程序,一个服务器可以与多个客户端建立网络连接。每个客户端都可以像服务端发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。其中采用多路复用技术实现的文件事件处理器,redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

8.2 文件事件和时间事件

redis的服务器是一个时间驱动程序。它主要处理两类事件:文件事件和时间事件。

8.2.1 文件事件:

在这里插入图片描述

前面提到redis采用多路复用技术来处理多个客户端。那么其里面是怎么实现对多个客户端进行管理的呢?原来在redis里面采用一个链表结构保存了所有与服务器连接的客户端的状态结构。然后对某个客户端的操作可以通过遍历整个链表来查找。
在这里插入图片描述

在这里插入图片描述

我们再回到文件事件处理器来,它主要由套接字、IO多路复用程序、文件时间分派器以及事件处理器组成。

在这里插入图片描述

红框框中的这种方式是单线程单进程的多路复用技术的使用方式。

接下来我们再来看看文件事件是怎么样的一个多路复用程序。

在这里插入图片描述

8.2.2 时间事件:

在这里插入图片描述

时间事件的实现方法

在这里插入图片描述

8.2.3 事件的调度和执行:

因为在服务器中同时存在着文件事件和时间事件,所以服务器必须对这两种事件进行调度。在redis的调度过程大概由下面这张图所示。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值