Redis数据结构&持久化总结

发布于:2022-12-20 ⋅ 阅读:(376) ⋅ 点赞:(0)

一、Redis的数据结构有哪些

redis的数据结构包含: 简单动态字符串 、 链表 、 字典 、 跳跃表 、 整数集合 、 压缩列表 。

简单动态字符串 :redis没有采用C语言的内置字符串而是自定义了一种叫 简单动态字符串 (SDS),它兼容了C语言的字符串结构(保留了C语言字符串的格式以便使用C语言字符串的库)极其优点于一身的简单动态字符串,还融入了它自己的优点。 例如:

  • 快速获取字符串的长度 ,结构体中存储了字符串的长度,即便是再怎么长的字符串都可以以 O(1) 的速度获取字符串的长度。 

  • 杜绝缓存溢出 ,SDS的空间分配策略完全杜绝 了C语言中的溢出隐患,每当字符串API修改时,sdscat都会进行空间预判并自动扩容字符串空间,最后再拼 接字符串,而不需要程序员手动扩容。

  • 减少修改字符串时带来的内存重分配次数 ,SDS 结构体中有一个free属性,用来记录未使用的空间长度,当SDS的API对一个SDS进行修改时,会自动计算SDS所需要的长度进行分配额外的未使用空间,俗称空间预分配。同理如果字符串不需要那么多free未使用的空间,会进行惰性释放空间的行为。

  • 二进制安全 ,SDS可以存储类似图片、音频、压缩文件等这样的二进制数据。还可以存储空格分割的字符串。

  • 兼容部分C字符串函数 。

链表 : 链表在Redis的应用很广泛,例如列表键的底层设计之一就是链表。链表是由多个节点组成的,它具有以下特点。 例如:

  • 双端 ,链表的每个节点都有prev和next指针,可以快速获取某一个前置节点和后置节点的信息,复杂度为O(1)。

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

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

  • 带链表长度计数器 ,list结构的len属性存储了链表节点的计数,获取节点的数量时间复杂度为O(1)。

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

字典 :字典可以理解为关联数组或映射、以及符号表。是用来保存键值对的抽象数据结构。字典中的键都是独一无二的,可以通过键查找对应的值,以及更新删除等操作。

字典是采用哈希表作为底层实现的。一个哈希表里面可以有多个哈希节点,每个哈希节点就保存了一个字典中的键值对。 例如:

  • 当一个新的键值对要添加到字典中,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值将包含键值对的哈希表节点放到哈希数组指定的索引上。

  • 既然采用了 哈希算法 ,就会遇到 键冲突 。当两个以上的键被分配到了同一个索引上面,就称发生了键冲突。Redis的哈希表采用了 链地址法 来解决键冲突 。即多个哈希表节点采用next指针构成一个 单向链表 。使用单向链表链接起来就解决了键冲突的问题。因为是单向链表,并没有指针指向链表的表尾,所以为了速度考虑,每次新的节点都会在链表的表头位置插入。复杂度为O(1)。

  • 哈希表保存的键值对会越来越多,在对哈希表进行 扩展 和 收缩 操作时,程序需要将现有哈希表包含的所有键值对 rehash到新的哈希表 中,并且这个rehash过程并不是一次性完成的,而是 渐进式地完成 的。

跳跃表 :跳跃表是一种有序数据结构,它通过在每个节点中维持 多个指向其他节点的指针 ,从而达到 快速访问节点 的目的。redis使用跳跃表作为有序集合键的底层实现之一。

  • 跳跃表是使用( 跳跃表信息 )和( 跳跃表节点信息 )两个结构体定义的。

  • 每个跳跃表的 层高 都是1到32之间的随机数

  • 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是 唯一 的

  • 跳跃表中的节点 按照分值大小进行排序 ,当分值相同时,节点按照成员对象的大小进行排序

整数集合 :整数集合是集合键的底层设计之一,当我们存储一个集合中的元素都是整数值时,那么这个集合键的底层实现就会是整数集合。整数集合是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16,int32,int64的整数值。并且保证集合中不出现重复元素。

当我们存储的集合元素比较长时,整数集合会自动进行 升级 ,升级首先要做的的是,根据新类型的长度,以及集合元素的数量,对底层数组进行 空间重分配 ,然后才将新元素添加到整数集合中。集合 一旦升级则不再会降级 。

压缩列表 :压缩列表是由一块连续的内存组成的顺序性数据结构,和顺序型不同的是大小可变,类型可变。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作。

二、对象

Redis并没有直接使用以上这些数据结构来实现键值对数据库。而是基于这些数据结构创建了一个对象系统。这个系统包含了: 字符串对象、列表对象、哈希对象、集合对象、有序集合 对象这五种类型的对象。

每种类型的对象 至少都有两种以上的编码方式 ,不同的编码可以在不同的使用场景上优化对象的使用效率。服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。

  • Redis的对象系统还实现了基于 引用计数技术 的 内存回收机制 。当程序不在使用这个对象的时候,这个对象占用的内存就会被自动释放。

  • Redis还通过引用计数实现了 对象共享机制 ,即多个数据库键共享同一个对象来节约内存。

  • Redis对象带有 访问时间记录 ,可以计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长比较大的那些键可能会优先删除。

三、Redis的应用场景

  • 缓存,redis是基于内存的,读写速度非常快,可以作为缓存使用

  • 消息队列,发布,订阅,解耦

  • 分布式锁,redis的setnx来实现分布式锁

  • 统计,使用集合获取交集并集

  • 计数器,使用string中的inc实现加一or减一操作

  • 排序,有序集合可以实现分数的排序

四、Redis数据库操作

Redis服务器将所有数据库都保存在服务器状态redisServer结构体的db数组中。即一个数组保存着服务器中所有的数据库。dbnum是设置服务器数据库的数量。默认情况下可设置16个数据库。

1)切换数据库

Redis客户端的目标数据库为0号数据库,可以通过select命令来切换目标数据库。切换数据库相当于指针指向了下一个数据库,即切换指针。

2)数据库是如何存储键值对的呢

数据库键空间 ,就是用来保存着数据库中的所有键值对。键空间(*dict)。键空间和用户所见的数据库是直接对应的。

  • 键空间的 键 都是一个 字符串对象 。

  • 键空间的 值 可以是 字符串对象,列表对象,哈希表对象,集合对象,有序集合对象 。

3)键空间的其他操作

键空间除了基本的添加键,删除键,更新键,取值操作等。还有以下几个命令。

  • 清空整个数据库的命令:FLUSHDB。就是删除键空间的所有键值对。

  • 返回数据库键数量的命令:DBSIZE。就是返回键空间包含的键值对的数量。

  • 以及EXISTS,RENAME,KEYS等。这些都是通过键空间进行操作来实现的。

4)读写键空间的维护工作

在对Redis进行读写的时候,服务器不仅对键值对进行指定的读写还会执行一些你看不见的额外维护操作,其中包含:

  • 读取一个键之后,会判断键是否存在。

  • 读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,控制闲置时间。

  • 如果读取的是过期键,服务器会删除这个键,然后再执行其他操作。

  • 如果客户端使用WATCH命令监视了某个键,如果这个键被修改了,则将此键标记为脏(dirty)

  • 服务器每次修改一个键之后,都会对脏(dirty)键计数器值增1. 计数器会触发服务器的持久化以及复制操作。

  • 如果服务器开启了数据库通知功能,在对键进行修改之后,服务器将按配置发送响应的数据库通知。

5)设置键的生存时间或过期时间

  • EXPIRE <key> 设置键的生存时间为ttl秒

  • PEXPIRE <key> 设置键的生存时间为ttl毫秒

  • EXPIREAT <key> 设置键的过期时间为timestamp所指定的秒数时间戳

  • PEXPIREAT <key> 设置键的过期时间为timestamp所指定的毫秒数时间戳

四个命令中的哪一个,经过转换之后,最终执行的效果都和执行PEXPIREAT命令一样。

6)过期时间存储在哪里的呢

redisDB结构的expires字典保存了数据库中所有键的过期时间,称这个字典为 过期字典 。过期字典的键是一个指针 *expires。这个指针指向键空间中的某个键对象(即某个数据库键)过期字典的值是一个long 类型的整数,即过期时间,一个毫秒精度UNIX时间戳。

7)过期键的删除策略

如果不通过命令PERSIST  移除一个键的过期时间,数据库服务器会通过以下三种策略删除过期键。 定时删除、惰性删除、定期删除 。

  • 定时删除 :在设置键的过期时间的同时,创建一个定时器,让定时器在键过期时间来临时,立即执行对键的删除操作。此策略是对内存最友好的,通过定时器,定时删除那些过期的键,缺点是,它对CPU不够友好,如果过期键比较多,删除过期键会占用很多CPU时间。

  • 惰性删除 :每次从键空间获取键时,先检查是否过期,过期就删除,没有过期就返回该键。此策略对CPU来说是友好的,缺点是对内存不友好,因为可能会存在大量的过期键却从来未被访问过。

  • 定期删除 :每隔一段时间检查一次过期字典,进行删除过期键。此策略是针对前两种策略的一种整合和折中。定期删除的难点是如何确定删除操作执行的时长和频率。

8)工作中使用redis的一些疑问

  1. redis中所支持的多数据库其实是用不到那么多的,即dbnum的数量多少并没有太大的意义。切换数据库使用额外的IO。开发沟通成本变大。作者自己也说这个设计不好,很后悔。

  2. redis可以存放多少数据,取决于内存的大小,如果有持久化就看磁盘的大小。

  3. 工作中基本上是一个项目一个redis服务器,对应一个ip地址,一个地址对应一个集群。

  4. redis多数据库的内存大小不是隔离的,所以我们使用select 0 就相当于使用了一整个服务器内存

  5. 工作中通常使用阿里云的redis集群,如果访问量不高可以买低配的

  6. redis 各个数据类型最大存储量

  • string value最大可以存储12M 即0.5GB

  • list 的元素最多2得32次方减1个即4294967295个

  • set 元素最多同上

  • hash 元素最多同上

  • sorted set 元素最多同上

五、Redis的持久化

Redis是内存数据库,它将自己的数据库状态存储在内存中,如果不想服务器进程退出导致数据库状态和数据丢失,则需要将数据持久化到磁盘中。

Redis支持两种数据持久化方法:RDB持久化、AOF持久化。

RDB持久化

RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到RDB文件中。RDB持久化功能将生成一个RDB文件,它是一个经过压缩的二进制文件。通过该文件可以还原生成RDB文件时的数据库状态。因为RDB文件是存储在硬盘中的,即便Redis服务器进程退出,文件也不会丢失。

1)RDB文件的创建

Redis可以使用两个命令生成RDB文件:SAVE 和 BGSAVE.

SAVE命令会阻塞Redis服务器进程,阻塞过程中,服务器不能进行任何客户端命令请求。

BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程可以继续执行客户端请求。

注意 :RDB创建期间,服务器禁止同时执行多个命令生成RDB文件,哪怕是阻塞的SAVE命令,还是创建子进程的BGSAVE命令。因为同时执行两个命令会产生竞争条件。

2)RDB文件的载入

RDB文件的载入是在服务器启动时自动执行的。只要服务器在启动时检索到有RDB文件存在,它就会自动载入RDB文件。

RDB文件在载入的过程中,服务器是处于阻塞的状态,直到载入工作完成为止。

注意 :因为AOF文件的更新频率会更高,所以AOF的载入优先级高于RDB文件,除非关闭了AOF持久化功能。

3)自动间隔性保存

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

Redis服务器启动时,用户可以通过指定的配置文件或者传入启动参数的方式设置save选项,如果用户没有设置save选项,服务器会自动设置默认的save条件。例如:

  • save 900 1     900秒内对数据库进行了1次修改

  • save 300 10   300秒内进行了至少10次修改

  • save 60 1000   60秒内进行了至少1000次修改

4)RDB的优缺点

优点:

  • 体积更小,RDB是一个文件紧凑的二进制压缩文件,比较适合冷备份,就是某一时刻的镜像文件。

  • 恢复更快,RDB是快照,基本上就是数据的复制,不用重新读取命令一条条执行再写入到内存。

  • 性能更高,会fork一个子进程恢复数据,不会影响父进程和其他io的操作

缺点:

  • 故障丢失,RDB是某一时刻的镜像,是全量的,备份时间间隔相对不会太频繁,如果遇到故障,则丢失的数据也相对较多。

  • 耐久性差,RDB是全量复制的,即便父进程会fork一个子进程备份数据,当数量很大的情况下对磁盘的消耗就会很大的,当访问量很大的时候,fork的时间也会延长,对cpu也会造成很大的压力。

AOF持久化

AOF持久化和RDB的保存状态不同的是,AOF存储的是所执行的写命令来记录数据库状态。服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。

1)AOF文件的写入与同步

AOF持久化功能可以分为  命令追加缓冲区 , 缓冲区写入AOF文件 , AOF文件 同步到磁盘 三个步骤。

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

缓冲区写入AOF文件 , Redis的服务器进程就是一个事件循环,当有数据被追加到aof_buf文件后,就结束了一个事件循环,在结束事件循环之前,它都会调用flushAppendOnlyFile()函数,用来考虑是否将aof_buf缓冲区内容写入到AOF文件里面。

flushAppendOnlyFile函数行为由服务器配置的 appendFsync 选项的值决定的。

  • always:将aof_buf缓冲区中的所有内容写入并同步到AOF文件。并且将AOF文件同步到磁盘。(最慢,但最安全,会丢失一个事件循环所产生的数据)

  • everysec:将aof_buf 缓冲区的所有内容写入到AOF文件,并且每隔一秒就要在子进程中对AOF文件进行一次同步磁盘。(效率最高,但会丢失一秒钟的数据)

  • no: 将 aof _buf  缓冲区 的所有内容写入到 AOF 文件,但是并不会将AOF文件同步到磁盘,而是由操作系统控制。(文件的写入最快,但同步磁盘速度是最慢的,丢失数据最多的)

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

因为AOF文件的内容,是一条条客户端执行的命令,所以当服务器启动时,会创建一个伪客户端,从AOF文件中分析并读取一条条命令并执行,效果和客户端操作命令是一样的,所以数据还原的效率不是特别快。

3)AOF重写

因为AOF文件中是一条条客户端请求的命令,会存在大量的重复命令以及变更命令,这使得AOF文件格外的庞大,恢复数据也超级慢。

AOF需要开启一个子进程异步重写文件,进行命令合并,并剔除查询和删除的冗余命令。AOF重写功能,是Redis服务器创建了一个新的AOF文件,来替代现有的AOF文件,新旧两个AOF文件保存的数据库状态是同步相同的。

当子进程进行AOF重写时,服务器主进程还需要继续处理命令请求,而新的命令很能会对现有的数据进行修改,从而导致数据库当前状态和AOF重写文件保存的数据状态不一致。

为了解决这个问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区是和创建子进程一起使用的,当Redis服务器完成一个写命令之后,它会同时将这个命令同时发送给AOF缓冲区和AOF重写缓冲区。当子进程完成AOF重写工作之后,子进程会给父进程发送一个信号。此时,父进程会调用一个信号处理函数,执行以下工作:

  • 将AOF重写缓冲区写入到新的AOF文件中,以保证数据库状态是最新的数据库状态

  • 对新的AOF文件进行改名,原子的覆盖现有的AOF文件

4)AOF的优缺点

优点:

  • 数据保证,可以通过配置  appendFsync 为  everysec 最多丢失1秒的数据

  • 自动缩小,会进行AOF重写工作,并且保证数据库状态的一致性

  • 可读性好,文件命令是一条条客户端请求命令,易读

缺点:

  • 体积较大,即便会自动压缩,也没有RDB的二进制文件压缩的小

  • 同步较慢,一条条执行客户端命令,定然没有RDB文件直接载入的快

六、Redis是多线程还是单线程

Redis6.0以前确实的对外宣称是单线程,但是在redis4.0开始就逐渐的优化了部分命令改成了多线程化,主要体现在大数据的异步删除功能上。例如 unlink key、flushdb async、flushall async等。

除此之外,其他命令都仍旧是单线程的。因为Redis是基于内存的,用于大量的读和写操作。而读和写主要涉及到的就是IO操作,其中包括网络IO 和 磁盘IO,计算操作用于CPU。而多线程的目的是通过并发的模式提升IO的利用率和CPU的利用率。

Redis的操作是基于内存的,CPU资源不是Redis的性能瓶颈,所以不需要使用多线程技术提升CPU利用率。而IO的利用率,确实可以提升。但是多线程也有它自身的一些弊端。比如资源共享的并发控制问题、线程切换带来的性能开销问题。所以Redis没有采用多线程,而是选择了IO多路复用技术(后面会更新Io多路复用)。

为什么Redis6.0之后引入多线程?因为即便采用了多路复用的IO模型,本质上仍然是同步阻塞型IO模型,Redis的性能瓶颈仍旧出现在网络IO的处理上。Redis6.0即便是引入了多线程,也只是针对处理网络请求过程采用了多线程,而数据的读写命令,仍旧是单线程处理的。

Redis6.0只有在网络请求的接收和解析,以及请求后的数据通过网络返回使用了多线程,而数据读写操作仍旧使用单线程,所以不会存在共享资源的并发问题。

七、参考来源

《Redis设计与实现》

知乎:Redis是多线程还是单线程:https://zhuanlan.zhihu.com/p/360335981

 

本文含有隐藏内容,请 开通VIP 后查看