Redis底层对象
字符串-->简单动态字符串(SDS)
Redis中用到最多的就是字符串的使用,作为一种常见的数据类型,Redis没有直接使用C语言传统的字符串表示,而是自己创建了一种动态字符串(SDS)
struct sdshdr { uint8_t free; /* used */ uint8_t len; /* excluding the header and null terminator */ char buf[]; };
优势(主要针对与C语言字符串的比较):
-
常数复杂度获取字符串长度
-
针对C字符串必须便利整个字符串才能获取长度
-
-
杜绝缓冲区溢出
-
针对要修改字符串内容时,当需要修改的长度大于已分配长度,并且没有重新分配空间,导致溢出
-
SDS API在对SDS进行修改时,会自动将空间扩展至所需大小,不需要手动操作
-
-
减少修改字符串时带来的内存重新分配次数
-
空间预分配:不仅为SDS分配修改所需要的空间,还会额外分配未使用的空间(基于当前修改的最大值 预期下次分配的结果,已使用和未使用free各占一半)
-
惰性释放:SDS字符串缩短操作时不立即重新分配,而是使用free属性记录下来,等待将来使用
-
有序集合-->跳跃表,压缩列表
有序集合类型 (Sorted Set或ZSet) 相比于集合类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
有序集合的底层编码实现主要是跳跃表和压缩列表。使用压缩列表的判断条件(同时满足):
-
有序集合的数量小于128个
-
所有元素成员的长度都小于64字节
压缩列表
当一个列表键之包含少量的列表项,并且列表项要么就是小数值,要么就是长度比较短的字符串,那么Redis就会将压缩列表来作为列表键的底层实现。(压缩列表是列表键和哈希键的底层实现之一)
压缩列表存在的意义: 节约内存
定义: 由一系列特殊编码的连续内存块组成的顺序型数据结构,每个节点都可以保存一个字节数组和一个整数值。
之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。我们知道,数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小(假设是20个字节)。存储小于 20 个字节长度的字符串的时候,便会浪费部分存储空间。
数组的优势占用一片连续的空间可以很好的利用CPU缓存访问数据。如果我们想要保留这种优势,又想节省存储空间我们可以对数组进行压缩。
但是这样有一个问题,我们在遍历它的时候由于不知道每个元素的大小是多少,因此也就无法计算出下一个节点的具体位置。这个时候我们可以给每个节点增加一个lenght的属性。
压缩列表的组成
包含三个节点的压缩列表:
包含五个节点的压缩列表:
-
zlbytes: 压缩列表的总长
-
zltail : 压缩列表总的偏移量(末尾到起始之间地址有多少字节数)
-
zllen : 压缩列表的节点数
-
zlend : 标记压缩列表的末端
假设P为只想一个压缩列表的起始指针,那么entry5的地址为 P+zltail(179)
压缩节点的组成
pervious_entry_length:前一个节点的长度
encoding: 记录节点的content属性所保存数据的类型及长度
content:压缩节点的值(一个字节数组或者整数),值的类型和长度由节点encoding属性决定
压缩列表的遍历
跳跃表
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的,作为Redis中有序集合键的底层实现之一。
为什么元素数量比较多或者成员是比较长的字符串的时候Redis要使用跳跃表来实现?
跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。
Note:
跳跃表类似于二分法的链表化
-
表格的高度(n为总节点数):
\log_2n -
对于单链表的时间复杂度:
O(log_2n) -
对于跳跃表的时间复杂度:
O(m*\log_2n)
对于m而言,最上层的索引节点最多为两个,当从最上层往下查询时,相当于是进行了一次从最小粒度往大粒度的二分查找,即最上层的1个节点相当于进行了一次二分查找。
假如我们要查找的数据是x,在第k级索引中,我们遍历到y结点之后,发现x大于y,小于y后面的结点z。所以我们通过y的down指针,从第k级索引下降到第k-1级索引。在第k-1级索引中,y和z之间只有3个结点(包含y和z)。所以,我们在k-1级索引中最多需要遍历3个结点,以此类推,每一级索引都最多只需要遍历3个结点。
因此,m=3,所以跳表查找任意数据的时间复杂度为O(logn)
跳跃表的插入
当我们往跳表中插入数据的时候,我们可以通过一个随机函数,来决定这个结点插入到哪几级索引层中,比如随机函数生成了值K,那我们就将这个结点添加到第一级到第K级这个K级索引中。如下图中要插入数据为6,K=2的例子:
随机函数的选择是非常有讲究的,从概率上讲,能够保证跳表的索引大小和数据大小平衡性,不至于性能的过度退化。
列表对象-->压缩列表,双端链表
列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储232-1个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
特点:
-
列表中的元素是有序的,可以通过索引下标获取某个元素或者某个范围内的元素列表。
-
列表中的元素可以是重复的.
当元素的个数(512)以及元素的值(64字节)时,使用压缩列表,否则则使用双端链表
而在Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.(混合了ziplist和linkedlist)
哈希对象-->压缩列表,哈希表
哈希类型的内部编码有两种:压缩列表和哈希表。只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:
-
当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)
-
所有值都小于hash-max-ziplist-value配置(默认64字节)
ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
压缩列表存储哈希对象时:
保存同一键值对的两个节点总是挨在一起,保存键的节点在前,保存值得节点在后
哈希表存储哈希对象时:
使用字典作为底层实现,哈希对象中得每个键值对都使用一个字典键值对来保存
集合对象-->intset,hashtable
集合类型的内部编码有两种:
-
intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
-
hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
Redis持久化
-
RDB内存快照
-
AOF内存日志
内存快照RDB
所谓内存快照,顾名思义就是给内存拍个照,在某个时刻把内存中的数据记录下来,以文件的形式保存到硬盘上,这样即使宕机,数据依然存在。在服务器重启后只需要把“照片”中的数据恢复即可。
RDB持久化就是把当前进程的数据在某个时刻生成快照(一个压缩的二进制文件)保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
持久化命令
save: 会阻塞当前Redis服务器,直到RDB过程完成
bgsave: 派生出一个子进程(而不是线程),由子进程进行RDB文件创建,而父进程继续处理命令。
在Redis启动的时候,只要检测到RDB文件的存在,就会自动加载RDB文件。需要注意的是
-
因为AOF文件的更新频率通常比RDB文件的更新频率高,所以口如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
-
只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
关于RDB快照的问题
-
Redis RDB持久化是对
某一时刻
的内存中的全量数据
进行存储。在进行快照的过程数据可以修改吗?按照正常的逻辑,在存储的过程中修改数据是会破坏数据的完整性的,但是Redis使用一种操作系统提供的写时复制技术(Copy-On-Write, COW),可以让在执行快照的同时,正常处理写操作。简单来说,bgsave fork子进程的时候,并不会完全复制主进程的内存数据,而是只复制必要的虚拟数据结构,并不为其分配真实的物理空间,它与父进程共享同一个物理内存空间。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,此时会给子进程分配一块物理内存空间,把要修改的数据复制一份,生成该数据的副本到子进程的物理内存空间。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
2.可以频繁的进行快照操作吗?
-
由于快照是全量复制,所以频繁复制会给磁盘带来压力
-
bgsave所fork出来的子进程执行操作虽然并不会阻塞父进程的操作,但是
fork
出子进程的操作却是由主进程完成的,会阻塞主进程,fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间也就越久。
AOF日志
AOF
持久化是通过保存Redis
服务器所执行的写命令来记录数据库状态的。即每执行一个命令,就会把该命令写到日志文件里。
特点:
-
AOF
虽然避免了对当前命令的阻塞,但却可能会给下一个操作带来阻塞风险。因为,AOF
日志是在主进程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了 -
如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时
Redis
是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果Redis
是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
三种回写策略
Redis AOF
机制提供了三种回写磁盘的策略。
-
Always(同步写回)
: 命令写入AOF
缓冲区后调用系统fsync
操作同步到AOF
文件,fsync
完成后线程返回 -
Everysec(每秒写回)
: 命令写人AOF
缓冲区后调用系统write
操作,write
完成后线程返回。fsync
同步文件操作由专门线程每秒调用一次 -
No(操作系统自动写回)
: 命令写入AOF
缓冲区后调用系统write
操作,不对AOF
文件做fsync
同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒
AOF重写
Redis
根据数据库的现状创建一个新的 AOF
文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入(记录最终状态)。
AOF重写缓冲区主要是保持在AOF执行重写功能的同时保持数据的一致性
在子进程执行AOF
重写期间。服务器进程需要执行以下3个动作:
-
执行客户端命令
-
执行后追加到
AOF
缓冲区 -
执行后追加到
AOF
重写缓冲区
-
T3的重写是针对T1,T2两个操作
-
T4~T6实在重写过程中又加入了新的值
-
T7完成AOF重写之后直接将T4~T6重写之后的结果进行追加
Redis内存消耗与三大缓冲区
Redis主要有三个缓冲区,客户端缓冲区、AOF缓冲区、复制积压缓冲区。
-
客户端缓冲区是为了解决客户端和服务端请求和处理速度不匹配问题的,它又分为输入和输出缓冲区。
输入缓冲区会先把客户端发送过来的命令暂存起来,Redis 主线程再从输入缓冲区中读取命令,进行处理。当在处理完数据后,会把结果写入到输出缓冲区,再通过输出缓冲区返回给客户端。
-
-AOF缓冲区是在进行AOF持久化时所用到的缓冲区,AOF缓冲区消耗的内存取决于AOF重写时间和写入命令量, 这部分空间占用通常很小关于AOF缓冲区的介绍我们可以复习一下
-
-复制积压缓冲区则是在集群环境中为了保证主从节点数据同步的所设置的。
主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。