Redis的底层数据结构(总结版)

若是没有了解过redis底层数据结构的实现,请先看Redis数据结构详解 - 掘金 (juejin.cn)

Redis的底层数据结构有:SDS 简单动态字符串、IntSet 整数集合、Dict 字典、SkipList 跳表、ZipList 压缩列表、QuickList 快速列表。

SDS 简单动态字符串 Simple Dynamic String

给我讲讲什么是动态字符串?

SDS其实就是对字符数组的封装。字符数组用来存储数据,同时使用len字段记录了字符串的长度,alloc字段记录了字符数组申请的空间大小。

那为什么Redis要有自己的字符串实现,而不是用C自带的字符串呢?

因为C字符串存在一些问题:

  1. C字符串没有记录自身的长度。 获取长度的时候,需要遍历计算,时间复杂度为O(n)。而SDS使用len字段记录了长度,使得获取长度的操作的时间复杂度降低到了O(1)。
  2. 容易出现缓冲区溢出的问题。 C在进行两个字符串拼接的时候,需要保证分配了足够的空间,否则就会出现缓冲区溢出的问题。而SDS在进行拼接操作的时候,会先判断是否有足够的空间,若不够的时候会自动进行扩容。
  3. 二进制不安全。 C字符串的本质其实是一个字符数组,其最后一个字节会以空字符\0结尾,因此C字符串不允许中间出现空字符。因此,C字符串不能存储图片、视频、文件等二进制文件。而SDS通过len字段来记录字符串长度而不是通过空字符来标记,因此不会存在这种限制,可以用来存储二进制数据。
  4. 每次修改都会导致内存分配。 C字符串在修改的时候,都需要重新进行内存分配。而SDS通过空间预分配和惰性空间释放策略可以减少内存重分配的次数。

讲下SDS扩容公式?

  1. 当新字符串小于1M的时候,则申请的空间大小为两倍的新字符串大小 + 1字节。
  2. 当新字符串大于等于1M的时候,则申请的空间大小为新字符串大小 + 1M + 1字节。

IntSet 整数集合

什么是整数集合?

IntSet是只用来存储整数的集合,且元素有序且不重复。

整数集合底层使用一个整数数组来存储数据。虽然这个整数数组的类型为为int8_t(8位的整数),但实际上真正存储的类型由encoding属性来决定。整数集合IntSet支持存储的整数有16位的整数、32位的整数和64位的整数,不同位数的整数能够表示的数值范围也不同。

整数集合IntSet怎么做到快速定位到元素?

整数集合底层其实是使用一个整数数组来存储数据。整数数组是一块连续的内存空间且每个元素占用的内存空间是固定的,因此通过计算能够很快获取具体下标的的元素的起始地址。当想要查找特定元素的时候的时候,由于整数数组的元素存储是有序的,通过二分法也能很快查找到对应的元素。

讲下整数集合IntSet的升级过程?

整数集合能够支持3种编码的整数的存储。

当整数集合添加了一个超出当前编码能够表示范围的整数时,整数集合就会自动升级到合适的编码。

现有集合{0,1,2},然后需要添加整数50000。那么就需要升级:

  1. 先通过升级后的编码和现有集合中元素的个数计算出需要的空间大小,并根据计算出来的空间扩容数组
  2. 倒序将数组中的元素拷贝到合适的大小
  3. 将要添加的整数放入数组的末尾。

其实升级后新元素放入的位置放到数组的开头或末尾根据元素的大小来决定。若新元素为正数,那么放入数组的末尾。若新元素为负数,那么放入数组的开头。

另外还有一点要提的是:整数集合IntSet不支持降级操作。

整数集合IntSet的好处?(整数集合IntSet和整数数组的区别?)

  1. 使用上更加灵活。 C的数组只能存储同一种类型的元素,否则会出现类型错误的问题。也就是说C的数组要么存储16位的整数,要么就是32位的整数。而整数集合IntSet通过升级的机制使得可以同时添加3种类型的整数且不同担心类型错误的问题,使得使用上更加的灵活。
  2. 节省内存。 整数集合IntSet的升级机制只在需要的时候才升级编码,从而使得能够节省内存。

Dict 字典

什么是字典?

字典是用来实现键值之间的映射关系的。它由三部分组成:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)。

当一个元素被添加到字典中的时候,会通过其哈希值与哈希表的大小的掩码sizemask做与操作,计算出这个元素要被放入哈希表的哪个下标。当存在哈希冲突的时候,就通过链地址法来解决。每个元素就是一个哈希节点。

最后还有一个字典Dict是用来持有哈希表的引用的。字典有一个长度为2的数组,用来持有哈希的引用。数组长度为2的原因是用来执行字典的rehash操作的。

字典什么时候执行扩展或收缩操作?

扩展操作: 当字典中的元素过多的时候,哈希冲突增多,链表也会过长,那么字典的查询效率就会降低。

每次添加元素的时候,都会检查负载因子。有两种情况会触发哈希表扩容:

  • 哈希表的负载因子大于等于1,且后台没有执行BGSAVE或BGWRITEAOF命令。
  • 哈希表的负载因子大于等于5。

收缩操作:

每次删除元素的时候,都会检查负载因子。当负载因子小于0.1的时候就会触发哈希表的收缩操作。

为什么字典扩展操作在负载因子大于等于1且执行后台命令的时候不执行?

在执行BGSAVE或BGWRITEAOF命令的时候,Redis需要创建当前服务器进程的子线程了,而大多数操作系统都采用写时复制技术来优化子进程的使用效率。

因此为了避免不必要的写入操作,在负载因子比较小且存在子线程的时候,不进行哈希表扩展操作,从而节省内存。

字典rehash操作在数据量很多的时候容易导致主线程阻塞,redis是怎么解决的?

当哈希表执行扩展或收缩操作的时候,都需要进行rehash操作。字典采用的是渐进式的rehash,将rehash操作分为多次,从而避免主线程阻塞。

字典里面有一个长度为2的数组持有哈希表的引用。在通常情况下,下标为0的元素就是使用中的哈希表,下标为1就是空。在rehash操作的时候,redis就会创建新的哈希表并赋值到数组的下标为1的地方。字典还有一个rehashidx的属性,初始值为0。每次执行增删查改操作都会将旧哈希表的下标为rehashidx的所有哈希节点进行rehash,然后将rehashidx加一。

跳表 SkipList

什么是跳表?

跳表是一种有序的数据结构,通过在节点维护多个指向其他节点的指针,从而加快节点的访问。

跳表由两个结构组成zskiplist和zskiplistnode。zskiplist维护了指向表头、表尾两个节点的指针。zskiplistnode代表的是跳表节点,维护了后退节点和前进节点两个属性。借助这些属性,从而实现跳表的顺序遍历、倒序遍历。

压缩列表 ZipList

什么是压缩列表ZipList?

压缩列表是由一系列特殊编码的内存块组成的数据结构。压缩列表可以包含任意多个节点,每个节点可以存储一个字节数组或者一个整数。

压缩列表的前4个节点代表的是整个压缩列表占用的内存字节数zlbytes。

紧接着的4个字节代表的是表尾节点距离压缩列表的起始地址有多少字节zltail。

接着的2个字节表示压缩列表总共有多少个节点,当值小于65535的时候就是压缩列表真实的节点数量,否则就需要遍历压缩列表才能计算出真实的节点数量。

接下来的空间就是用来存储节点的,每个节点的大小都是不固定。每个节点有3部分组成:previous_entry_length、encoding、content。

previous_entry_length是用来存储前一个节点的大小的,借助这个属性可以实现压缩列表的倒序遍历。当前一个节点的大小大于等于254字节,那么就使用5字节来记录并且第一个字节固定为0xFE,否则使用1字节记录。

encoding是用来表示content存储的内容是字节数组还是整数。当encoding的前两个字节分别是00、01、10的时候,它占用的大小为一字节、两字节或五字节,content存储的内容为字节数组。当前两个字节为11的时候,占用字节为一字节,content存储的内容为整数。

压缩列表的最后一个字节固定为OxFF,表示压缩列表的结束。

压缩列表存在什么问题?

压缩列表存在连锁更新问题。

当压缩列表存在多个长度为250到253的多个连续节点e1-eN。若e1节点长度发生变化,大于等于254节点,那么就会导致后面e2-eN节点的长度都会发生变化。这种问题就叫做连锁更新,会导致发生n次内存分配。

redis没有对这个连锁更新问题作出处理,这是因为:

  • 这个问题的发生需要多个节点的长度在250到253之间,发生的概率很低。
  • 就算发生了连锁更新问题,只要节点数量不多,也不会影响性能。

快速列表 QuickList

什么是快速列表?

压缩列表存在两个问题:

  1. 当压缩列表需要大量内存的时候,申请这些内存的效率很低。
  2. 当压缩列表内的元素过多的时候,查询等操作效率很低。

因此,redis就推出了新的数据结构:快速列表。

快速列表是一个双端链表,里面每个节点存储的都是压缩列表。这样就可以将压缩列表分片,减少每个压缩列表占用的空间。另外,快速列表也支持节点的压缩,进一步压缩节点从而减少内存占用。

相关推荐
专注VB编程开发20年8 分钟前
asp.net mvc如何简化控制器逻辑
后端·asp.net·mvc
用户67570498850238 分钟前
告别数据库瓶颈!用这个技巧让你的程序跑得飞快!
后端
懒羊羊大王呀1 小时前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
千|寻1 小时前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱1 小时前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯1 小时前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响1 小时前
枚举在实际开发中的使用小Tips
后端
wuhunyu1 小时前
基于 langchain4j 的简易 RAG
后端
techzhi1 小时前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端