redis 最基础的数据结构也该知道了

简单动态字符串(SDS)

什么是SDS & 与 C 字符串有什么不同

先上一个SDS的数据结构SDS 数据结构,来自Redis设计与实现。
Redis 为什么不选择已有的 C 字符串 而要使用 SDS 来作为字符串的底层数据结构呢?
ps:C 字符串使用 N+1 的字符数组来存储长度为 N 的字符串,且最后一位总是一个空字符 ‘\0’。
1、不能让获取字符串长度成为 Redis 的性能瓶颈,由于 SDS 的数据结构中有 len 属性,可以以O(1)的复杂度获取字符串长度,而 C 字符串只能从头便利,直到遇见 ‘\0’;
2、缓冲区溢出:
(1)当两个 C 字符串s1、s2在内存中相邻,此时想要在 s1 后面加上一个字符串却忘记申请空间,则会导致新加的字符串数据溢出到 s2 上,导致 s2 的内容被修改。
(2)SDS 则不会出现上述情况,当使用 SDS 的 API sdscat(s1,"xxx"),进行数据拼接时,sdscat 会首先进行扩展 s1 的空间再进行拼接操作;
3、如果像 C字符串一样每次需要扩展都进行内存重分配,每次缩容都进行内存重分配,这样会很大的影响性能,所以 SDS 产生了空间预分配与惰性空间释放的机制:
(1)空间预分配
分配策略:
a)如果 SDS 修改后 len 属性值小于 1M,则 len = free,即给 SDS 预留的空间和 SDS 本身长度一样,buf 数组的实际长度就是 2 * len + 1
b)如果分配后 len 属性值 大于等于 1M ,则会分配 1M 的预留空间。
(2)惰性空间释放
在 SDS 的 API 需要缩短 SDS 保存字符串时,程序并不会执行重分配,而是将其保存到 free 属性中,以便将来使用。
4、由于 SDS 不通过最后一个字符来判断是否为字符串的结束,而是通过 len 属性值来判断,与 C 字符串的最后一个字符是否为 ‘\0’ 不同,这样 SDS 可以使用 buf 数组保持各种各样的二进制数据;
5、由于 SDS 保留了 C 字符串的最后一个字符为 ‘\0’,使得可以兼容部分 C 字符串的 API ,避免了不必要的代码重复。

链表

链表的实现

链表数据结构

typedef struct list {
// 表头节点
listNode * head;
// 表尾节点
listNode * tail;
// 链表包含的节点数量
unsigned long length;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
} list;

链表节点数据结构如下
链表节点数据结构
链表的特性
双端(prev、next)、无环(表头节点的prev指针和表尾节点的next指针都指向 NULL)、带有表头指针和表尾指针、带有链表长度计数器、多态(使用void* 指针保存节点,可以用来保存各种不同类型的值)

字典

字典的底层实现

Redis的字典使用 哈希表 作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
哈希表数据结构如下:
哈希表数据结构
字典的数据结构如下:
字典数据结构

字典是怎么解决键冲突

字典采用链地址法来处理键冲突,并且每一个新的键都添加到链表表头(即复杂度O(1))

字典的 rehash 策略

主要使用 dictht ht[2] 来进行 rehash,平时仅使用其中一个,只有进行 rehash 时才会使用另一个哈希表;
扩展操作:
h[1] 的大小为第一个大于等于 h[0].used * 2 的 2^n(2 的 n 次方);
收缩操作:
h[1] 的大小为第一个大于等于 h[0].used * 2 的 2 n;
哈希键从 h [0] 迁移到 h[1] 后,会释放掉之前的 h[0],将 h[1] 设为 h[0],在之前 h[1] 的位置重新创建一个 h[1],准备 rehash 下次使用。
补:
a)当服务器没有进行BGSAVE 或 BGREWRITEAOF 时,若负载因子大于 1,则执行扩展操作;
b)当服务器正在进行BGSAVE 或 BGREWRITEAOF 时,若负载因子大于 5,则执行扩展操作;
c)负载因子公式如下:

// 负载因子 = 哈希表已保存节点数 / 哈希表大小
load_factor = ht[0].used / h[0].size

为防止影响服务器性能,字典的 rehash 过程是渐进式的。

跳跃表

一种有序的数据结构,效率媲美平衡树,且实现相对简单,是有序集合中的一员。

跳跃表的底层实现

跳表数据结构如下:
跳表数据结构
跳表节点数据结构如下:
跳表节点

整数集合

是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

整数集合的底层实现

整数集合数据结构如下
整数集合

升级 & 降级

升级

当有新的元素添加到整数集合且新的元素长度比现有的整数集合所有元素的长度都要长时,整数集合需要先升级。
升级三步走:
1)根据新元素类型来扩展整数集合底层数组空间大小,并且为新元素分配空间;
2)底层元素类型转换成新类型,并且维持有序性;
3)添加新元素进入到底层数组。
好处:
提升整数集合的灵活性(不需要考虑添加的元素类型不同该怎么办) & 尽可能地节约内存(只有添加了新的类型元素才会进行升级操作,避免提前准备空间存储)。

降级

整数集合只支持升级不支持降级操作!!!

压缩列表

❑压缩列表是一种为节约内存而开发的顺序型数据结构。
❑压缩列表被用作列表键和哈希键的底层实现之一。
❑压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
❑添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。

----对象----

Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。

对象类型

1、字符串对象(SDS)三种编码 int、raw、embstr;
2、列表对象(ziplist、linkedlist)两种编码转化规则如下:
a)所有字符串元素长度是否都小于 64;
b)列表对象保存的元素数量是否小于 512 个。(这两个边界值可修改)
满足a、b则为压缩列表(ziplist),反之链表(linkedlist)
3、哈希对象(ziplist、hashtable)规则同上;
4、集合对象(intset 或者 hashtable)编码规则如下:
a)元素是否都是整数;
b)集合对象保存的元素数量是否超过512.
满足a、b 则为整数集合(intset),反之哈希表(hashtable);
5、有序集合对象(ziplist 或者 skiplist),skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
zset 数据结构
为什么 zset 同时使用了跳跃表和字典两种结构 ?
1、字典保存了以 O(1)复杂度查找成员分值的特性(但对于范围查询效率不高);
2、跳跃表对范围查询的有点被保留的下来(对于特定值查找效率不高)。
3、字典和跳跃表会共享元素的成员和分值,并不会造成任何数据重复,也不会因此而浪费任何内存。
补:
redis 存在对象共享机制(仅针对数字类型的字符串对象0~9999);
对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值