Redis复习(一):Redis数据类型、底层数据结构、过期键删除策略、内存回收策略、RDB和AOF、事务

一、Redis数据类型

类型 特性
string(字符串) 二进制安全的,可以包含任何数据,一个键最大能存储512M
list(列表) 双向链表,按照插入顺序排序,可以从链表两端进行push和pop操作
hash(散列表) 键值对集合,适合存储对象
set(集合) 元素不重复的无序集合
zset(有序集合) 将set中的元素增加一个权重参数score,元素按score有序排列,数据插入集合时,已经进行天然排序

二、Redis底层数据结构

1、SDS

Redis构建了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作Redis的默认字符串表示

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

在这里插入图片描述

特性

1)、获取字符串长度的复杂度为O(1)

2)、API是安全的,不会造成缓冲区溢出

当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至修改所需的大小,然后才执行实际的修改操作

3)、减少修改字符串时带来的内存重分配次数

SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录

1)空间预分配

空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须的空间,还会为SDS分配额外的未使用空间

  • 如果对SDS进行修改之后,SDS的长度(也就是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同
  • 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间
    通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数

在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无须执行内存重分配

2)惰性空间释放

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

4)、二进制安全

通过使用二进制安全的SDS,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据

2、链表

链表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现

每个链表节点使用一个adlist.h/listNode结构来表示:

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

在这里插入图片描述

多个listNode可以通过prev和next指针组成双端链表

使用adlist.h/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;

在这里插入图片描述

特性

1)、双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)

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

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

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

3、字典

Redis的数据库就是使用字典来作为底层实现的,对数据库的CRUD操作也是构建在对字典的操作之上的

一个没有进行rehash的字典如下:

在这里插入图片描述

dict结构内部包含两个hashtable,通常情况下只有一个hashtable是有值的。但是在dict扩容缩容时,需要分配新的hashtable,然后进行渐进式搬迁,这时候两个hashtable存储的分别是旧的hashtable和新的hashtable。待搬迁结束后,旧的hashtable被删除,新的hashtable取而代之

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

渐进式rehash

为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩,扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成

为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]

1)、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

2)、在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始

3)、在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash至ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一

4)、随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成

因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。而新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作

4、跳表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的,支持平均O(logN)、最坏O(N)复杂度的节点查找

Redis使用跳跃表实现有序集合

在这里插入图片描述

上图中一个跳跃表示例,位于图片最左边的是zskiplist结构,该结构包含以下属性:

  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录目前跳跃表内,层数最大的那个节点的层数
  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量

位于zskiplist结构右方的是四个zskiplistNode结构,该结构包含以下属性:

  • 层:节点中用L1、L2、L3等标记节点的各个层,L1代表第一层、L2代表第二层,每个层都带有两个属性:前进指针和跨度。前进指针用于访问表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离
  • 后退指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用
  • 分值:各个节点中的1.0、2.0和3.0是节点所保存的分值。跳跃表中的所有节点都按分值从小到大来排序
  • 成员对象:各个节点的o1、o2、o3是节点所保存的成员对象

三、过期键删除策略

Redis使用的是惰性删除定期删除两种策略

1、惰性删除

过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:

  • 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除
  • 如果输入键未过期,那么expireIfNeeded函数不做动作

在这里插入图片描述

2、定期删除

过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键

activeExpireCycle函数的工作模式:

  • 函数每次运行时,都会从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键
  • 有一个全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理
  • 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检察工作

四、Redis内存回收策略

当Redis内存使用达到maxmemory上限时触发内存溢出控制策略,具体策略受maxmemory-policy参数控制,Redis支持6种策略

  • noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,删除存活时间最短的key。如果没有对应的键,则回退到noeviction策略

五、RDB和AOF

1、RDB

RDB是Redis默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中,即在指定目录下生成一个dump.rdb文件,Redis重启会通过加载dump.rdb文件恢复数据

1)、RDB文件的创建与载入

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

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

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

RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件

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

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

2)、RDB文件载入时的服务器状态

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

2、AOF

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,默认不开启

1)、AOF持久化的实现

1)命令追加

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

2)AOF文件的写入与同步

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

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

2)、AOF文件的载入与数据还原

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

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

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

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

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

在这里插入图片描述

3)、AOF重写

为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小很多

1)AOF文件重写的实现

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

aof_rewrite函数生成的新AOF文件只包含还原当前数据库所必须的命令,所以新AOF文件不会浪费任何硬盘空间

2)AOF后台重写

aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite函数的话,那么在重写AOF文件期间,服务器将无法处理客户端发来的命令请求

Redis将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:

  • 子进程进行AOF重写期间,服务器进程可以继续处理命令请求
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性

Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区

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

  • 执行客户端发来的命令

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

  • 将执行后的写命令追加到AOF重写缓冲区

在这里插入图片描述

3、混合持久化

Redis4.0版本添加了新的混合持久化方式,混合持久化就是同时结合RDB持久化以及AOF持久化混合写入AOF文件。这样做的好处是可以结合RDB和AOF的优点,快速加载同时避免丢失过多的数据,缺点是AOF里面的RDB部分就是压缩格式不再是AOF格式,可读性差

1)、开启混合持久化

4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的

2)、混合持久化过程

混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据

3)、数据恢复

当开启了混合持久化时,启动Redis依然优先加载AOF文件,AOF文件加载可能有两种情况如下:

  • AOF文件开头是RDB的格式, 先加载RDB内容再加载剩余的AOF
  • AOF文件开头不是RDB的格式,直接以AOF格式加载整个文件

4、RDB和AOF优缺点对比

1)、RDB的优点

  • RDB是一个快照文件,数据很紧凑,它保存了Redis在某个时间点上的数据集,体积比较小
  • RDB适合用于灾难恢复,因为它只有一个文件,而且体积小,方便拷贝
  • RDB可以最大化Redis的性能:父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘I/O操作
  • RDB在恢复大数据集时的速度比AOF的恢复速度要快

2)、RDB的缺点

  • 服务器故障时候会丢失数据。虽然可以调整RDB文件的保存频率,但是要保存整个数据集的快照,也不可能太频繁。所以使用RDB如果服务器出现故障可能出现丢失几分钟的数据
  • 每次保存RDB的时候,Redis都要fork()出一个子进程,并由子进程来进行实际的持久化工作。在数据集比较庞大时,fork()可能会非常耗时,造成服务器在某某毫秒内停止处理客户端;如果数据集非常巨大,并且CPU时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒

3)、AOF的优点

  • AOF的默认策略为每秒钟fsync一次,在这种配置下,Redis仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据(fsync会在后台线程执行,所以主线程可以继续努力地处理命令请求),也可以根据实际情况设置fsync的策略
  • AOF文件是一个只进行追加操作的日志文件,因此对AOF文件的写入不需要进行seek,即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等),redis-check-aof工具也可以轻易地修复这种问题
  • Redis可以在AOF文件体积变得过大时,自动地在后台对AOF进行重写:重写后的新AOF文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的,因为Redis在创建新AOF文件的过程中,会继续将命令追加到现有的AOF文件里面,即使重写过程中发生停机,现有的AOF文件也不会丢失。而一旦新AOF文件创建完毕,Redis就会从旧AOF文件切换到新AOF文件,并开始对新AOF文件进行追加操作
  • AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以Redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松。导出AOF 文件也非常简单:举个例子,如果不小心执行了 FLUSHALL命令,但只要AOF文件未被重写,那么只要停止服务器,移除AOF文件末尾的FLUSHALL命令,并重启Redis,就可以将数据集恢复到FLUSHALL执行之前的状态

4)、AOF的缺点

  • 对于相同的数据集来说,AOF文件的体积通常要大于RDB文件的体积
  • 根据所使用的fsync策略,AOF的速度可能会慢于RDB。在一般情况下,每秒fsync的性能依然非常高,而关闭fsync可以让AOF的速度和RDB一样快,即使在高负荷之下也是如此。不过在处理巨大的写入载入时,RDB可以提供更有保证的最大延迟时间

六、Redis事务

Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送EXEC命令前被放入队列缓存
  • 收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务
  • 命令入队
  • 执行事务

Redis事务相关命令:

  • watch key1 key2 ...:监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断(类似乐观锁)
  • multi:标记一个事务块的开始
  • exec:执行所有事务块的命令(一旦执行exec后,之前加的监控锁都会被取消掉)
  • discard:取消事务,放弃事务块中的所有命令
  • unwatch:取消watch对所有key的监控
发布了190 篇原创文章 · 获赞 442 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/104465757