Redis常用数据类型的底层实现

String 底层实现

SDS

  • len,记录了字符串长度。这样获取字符串长度的时候,等于只返回一个成员变量就可以了,时间复杂度是O(1)
  • alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以直接通过alloc - len计算剩余的空间长度,以此来判断是否满足修改要求,如果不满足的话,就会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作
  • flags,用来表示不同类型的SDS。一共5种类型,分别是sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64
  • buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据

扩容规则

  • 如果所需的sds长度小于1MB,那么最后扩容是按照翻倍扩容来执行的
  • 如果所需的sds长度大于1MB,那么最后扩容是按照 newlen + 1MB

flags

5种不同类型的区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同

Hash 底层实现

压缩列表或哈希表 实现

  • 如果哈希类型元素个数小于 512个 ,所有值小于 64字节,Redis会使用 压缩列表 实现
  • 如果不满足上述条件,使用 哈希表 实现

哈希表的优势

key-value的数据结构,能以O(1)的复杂度快速查询数据

渐进式rehash

在数据迁移的时候,为了减小性能消耗情况下使用

  • 给 哈希表2 分配空间
  • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作外,还会顺序将「哈希表1」中索引位置上的所有 key-value 迁移到 「哈希表2」
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表1」的所有key-value迁移到「哈希表2」,从而完成rehash 操作。

rehash触发条件:

负载因子:哈希表已保存节点数量 / 哈希表大小

  • 复杂因子大于等于1,没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作

  • 复杂因子大于等于5,此时哈希冲突非常严重了,强制进行rehash

List 底层实现

双向链表或压缩列表实现

  • 列表的元素个数小于512个,列表每个元素的值都小于64字节,Redis会使用压缩列表作为List类型的底层数据结构
  • 如果不满足以上条件,Redis会使用双向链表作为List类型的底层数据结构

双向链表的优势

  • ListNode链表节点的结构里带有 prev 和 next指针,获取某个节点的前置节点或后置节点的时间复杂度O(1),而且这两个指针都可以指向null,所以链表是无环链表
  • List 结构因为提供了表头指针head和表尾节点tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)
  • List 结构因为提供了链表节点数量len,所以获取链表中节点数量的时间复杂度只需O(1)
  • 链表可以保存不同类型的值

双向链表的缺点

  • 链表每个节点之间的内存都是不连续的,意味着 无法很好利用 CPU 缓存
  • 保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大

所以当 List 对象在数据量比较小的情况下,采用 压缩列表 作为底层数据结构的实现

压缩列表的结构设计

是Redis为了节约内存而开发的,它是 由连续内存块组成的顺序型数据结构,类似于数组

image-20240821082613093

  • zlbytes :记录整个压缩列表占用对内存字节数
  • zltail:记录压缩列表 尾部 节点距离起始地址有多少字节,也就是列表尾的偏移量
  • zllen:记录压缩列表包含的节点数量
  • zlend:标记压缩列表的结束点,固定值 0xFF

在这种设计下,如果查找第一个元素和最后一个元素,复杂度是O(1);如果查找其他元素,需要逐个查找,复杂度O(N)

下面看一个每个节点的设计

image-20240821082953689

  • prevlen:记录了 前一个节点 的长度,目的是为了实现 从后向前遍历
  • encoding:记录了当前节点数据的 类型和长度 ,类型主要有两种:字符串和整数
  • data:记录了当前节点的实际数据,类型和长度都由 encoding 决定

每添加一个数据,压缩列表就会根据数据类型是字符串还是整数以及数据的大小,会使用不同空间大小的 prevlen 和 encoding

这两个元素来保存信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是Redis为了节省内存而采用的。

prevlen属性的大小设计

  • 如果前一个节点长度小于 254字节,那么prevlen属性需要 1字节的空间 保存这个长度值
  • 如果前一个节点长度大于等于 254字节,那么prevlen属性需要用 5字节的空间 保存这个长度值

encoding属性的大小设计

  • 如果当前节点数据是整数,则encoding使用 1字节空间进行编码
  • 如果当前节点数据是字符串,则根据字符串的长度大小选择 1字节/2字节/5字节的空间进行编码

压缩链表的缺点

压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的prevlen占用空间都发生变化,从而引起「连锁更新IJ问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

因此压缩链表只会用于保存的节点数量不多的场景

Set 底层实现

哈希表或整数集合实现

  • 集合中都是整数且元素个数小于 512个,使用整数集合作为底层数据结构
  • 不满足上述条件,使用哈希表作为Set类型的底层数据结构

整数集合的设计

本质就是一块连续内存空间

Zset 底层实现

压缩列表或跳表实现

  • 如果集合中元素个数小于128个,并且每个元素的值小于 64 字节时,使用压缩列表实现
  • 不满足上述条件,采用 跳表

跳表的设计

跳表在链表基础上改进过来的,实现了一种 多层 的有序链表

image-20240821090447770

跳表如何设计呢?如图所示

  • L0层级共5个节点,分别是 1、2、3、4、5
  • L1层级共3个节点,分别是2、3、5
  • L2层级共1个节点,分别是3

现在我们要查找4这个节点,如果采用链表需要从头开始。

使用跳表后,查找两次即可,采用L2层级直接定位到3,然后在顺序遍历到节点4

typedef struct zskiplist {
    struct zskiplistNode *header,*tail;
    unsigned long length;
    int level;
} zskiplist;
  • 包含了头尾节点,在O(1)复杂度内就可以访问跳表的头节点和尾节点

  • 包含长度,O(1)复杂度可以获取节点数量

  • 包含最大层数,O(1)复杂度可以获取跳表中层高最大的哪个节点的层数量

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值