深入Redis原理与应用——Redis 中的基本数据结构

本文详细探讨了Redis中的四种关键数据结构:SDS(简单动态字符串)提供高效长度管理和内存管理,链表支持双端操作与多态,字典(哈希表)实现高效查找与rehash策略,跳跃表在zset和集群中的应用。整数集合与压缩列表则展示内存优化技术。
摘要由CSDN通过智能技术生成

深入Redis原理与应用——Redis 中的基本数据结构

简单动态字符串(SDS)

(1)SDS 的结构体为 sdshdr,包括属性 len 保存字符串的长度、free 保存在 buf 中但还未被使用的字节的数量、char 类型的 buf 数组用于保存字符串。
(2)SDS 遵循 c 字符串以空字符结尾的惯例,最后以一个’\0’结尾,此空字符的操作由SDS 自动完成。
(3)SDS 与 c 字符串的区别:

  • SDS 在 len 属性中保存了字符串的长度而无须遍历字符数组获取长度,获取长度的时间复杂度为 O(1)。
  • 防止缓冲区溢出:SDS 的 API 需要对 SDS 进行修改时,API 会先检查 SDS 的空间是否满足要求,如果不满足会先将空间拓展至所需的大小。
  • 减小修改字符串带来的内存重分配次数,见(4)。
  • SDS 是二进制安全的,所有 SDS 的 API 都会以处理二进制的方式来处理 SDS 存放在buf 数组里的数据。
  • 兼容部分 c 字符串函数。

(4)为了避免频繁申请内存和释放内存所带来的开销,SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,以下是两种优化的策略:

  • 空间预分配:用于 SDS 的字符串的增长操作,当 SDS 的 API 要对字符串进行空间拓展的时候,程序除了分配必须的空间外,还会分配未使用空间:如果修改之后 SDS 的长度(len属性值)小于 1MB,则会分配和 len 属性同样大小的未使用空间,使 len 属性与 free 属性大小相等;如果分配之后 SDS 的长度大于 1MB,则程序会分配 1MB 的未使用空间。(注:仅当buf剩余空间不够需要继续分配内存时才会触发分配free,如果需要增加的长度小于free
    的长度则会之间处理无须分配内存)。
  • 惰性空间释放:用于 SDS 的字符串缩短操作,当需要缩短 SDS 长度时,缩短的空间不会立即释放而是使用 free 将这些字节的数量记录下来等待日后使用。SDS 也有 API 可以让我们真正释放未使用空间,所以不必担心内存浪费。

链表

(1)链表的数据结构:list 表示一个链表结构包括 head 链表头节点、tail 链表尾节点、len 结点数量以及节点的复制函数、释放函数和对比函数(这三个函数为了实现链表的多态性质)。listNode,其中有 prev 属性节点的前一个元素指针、next 属性节点的后一个元素指针、value 属性节点值。
(2)redis 链表的特征如下:

  • 双端:节点都带有 prev 和 next 指针。
  • 无环:表头节点的 prev 和尾节点的 next 指向 null。
  • list 中带有表头表尾指针快速定位和 len 属性记录长度。
  • 多态:void*指针来保存节点值并可以通过 list 结构的 dup、free、match 三个属性为节点设置类型特别的函数。

字典

(1)字典在 redis 中使用极为广泛,比如 redis 数据库就是使用字典作为底层来实现的。
(2)使用到了字典的结构:数据库、字典、集合和有序集合(有序集合中与跳表结合使用)。
(3)字典其中的 hash 表的实现结构:dictht,其中有一个 dictEntry 类型的 table 数组,table 其中的每一个元素都是一个指向 dictEntry 结构的指针,每个 dictEntry 结构保存key、value 和指向下一个 dictEntry 的指针(此指针的作用是避免 hash 冲突,将 hash 值相同的连接成以一个链表);dictht 还有 size 表示哈希表的大小(table 数组的元素个数),used 表示其中的键值对个数,sizemask 哈希表大小掩码用于计算索引值。
(4)注意 dictht 并非是字典结构而是字典使用的 hash 表的结构,字典结构是 dict,其中有一个长度为 2 的 dictht 类型的哈希表数组(多一个是为在 rehash 时候使用的)。type 属性和privdata属性都是为了实现多态。type是一个指向dictType结构的指针,每个dictype结构保存了一簇用于操作特定类型键值对的函数。rehashidx 记录了 rehash 目前的进度,如果没在 rehash 则其值为-1。
(5)哈希算法:在添加键值对之前需要先根据键值计算出哈希值和索引值
hash = dict->type->hashFunction(key);
index = hash & dict[x].sizemark; x 为 0 或 1
(6)解决键冲突:redis 的 hash 表使用链地址法解决键冲突,hash 冲突的键值对使用 next连成一个单向链表。
(7)rehash 操作:随着键值对数量的增加,为了让哈希表的负载因子维持在一个合理的范围,当哈希表中键值对过多或过少时会执行 rehash 操作。
Rehash 步骤如下:

  • 为字典的 ht[1]哈希表分配内存空间,如果执行扩容操作则其值变为第一个大于等于 ht[0].used*2 的 2 的幂次方;如果是缩容则其值变为第一个大于等于 ht[0]的 2的幂次方。(2 的幂次方一是为了使用&操作代替取模快速计算下标,二是偶数的 len-1一定为奇数,即末位为 1,与之相与可为奇数可为偶数,保证 hash 散开)。
  • 将 ht[0]上的值 rehash 到 ht[1]上,即重新计算 hash 值和索引值并保存到指定位置。
  • 迁移完后将 ht[0]释放并将 ht[1]设置为 ht[0],然后在 ht[1]处创建一个空的
    hash 表以便下次 rehash。

(8)rehash 执行的条件:
服务器没有在执行 BGSAVE 或者 BGREWRITEAOF 操作并且哈希表负载因子大于等于 1。或者服务器正在执行 BGSAVE 或者 BGREWRITEAOF 操作并且哈希表负载因子大于等于 5。这是因为(负载因子即使用的节点数处以槽的数量)。

(9)rehash 的过程不是一次完成的,而是分多次,渐进式的将 ht[0]表中数据 rehash 到ht[1]表中,这是为了避免哈希表过大导致服务器在 rehash 过程中长时间停止服务。过程如下:

  • 为 ht[1]分配空间,让字典同时持有两个哈希表。
  • 在字典中的 rehashidx 变量,将其设为 0 表示 rehash 工作正式开始。
  • 在rehash进行期间每次对字典进行增删改查操作都会顺带将ht[0]表中在rehashidx索引上的所有键值对 rehash 到 ht[1]表中,然后将 rehashidx 加一。
  • 当所有键值对都 rehash 完毕后将 rehashidx 设为-1 表示 rehash 工作结束。这样的好处是分而治之,将 rehash 键值对所需要计算的工作均摊到对字典的每次操作,避免了集中式 rehash 带来的庞大计算量。渐进式 rehash期间字典会同时使用两个 hash表,字典的添加操作会被之间添加到 ht[1]中而查找等操作会现在 ht[0]上查找,找不到则去 ht[1]上查找。

跳跃表

(1)跳跃表在 redis 中的使用场景:zset 和 redis 集群。
(2)跳跃表通过在每个节点中维持多个指向其它节点的指针实现。
(3)zset 使用跳跃表而非红黑树:一是 zset 有一个很核心的操作—范围查找,而跳表的范围查找效率要高于红黑树;二是跳表的实现比红黑树简单得多,更容易实现。
(4)跳跃表由 zskiplistnode 和 zskiplist 两个结构实现。每生成一个 zskiplistnode 节点程序都会根据幂次定律(越大的数出现概率越小)随机生成一个介于 1 到 32 之间的值作为 level 数组的大小,这个大小就是这一层的高度。每个 level 的元素都有一个 forward 属性(前进指针)和一个 span 属性(跨度),根据查找到某一个节点沿途的跨度和可以得到该节点的排位。每个节点都有一个后退指针指向链表的上一个元素(每次只能后退一个元素)。节点的分值是一个 double 类型的浮点数,跳跃表中所有节点都按分值从大到小排列。分值相同的按照成员对象在字典序中的大小来排序,字典序小的在前。

整数集合

(1)整数集合可以保存类型为 int16_t int32_t 和 int64_t 的整数值,集合中不会出现重复元素。由 intset 结构保存,其中的 encoding 属性表示编码方式,length 属性表示元素长度,int8_t 表示保存元素的数组(真正保存的数组元素并非 int8_t 而是由 encoding 属性决定,比如 int_16 类型就占两个 int8_t 的位置)。

(2)升级:当我们要添加一个比当前整数集合的所有元素都长的数时,整数集合要先进行升级:

  • 根据新元素的类型拓展整数集合底层数组的空间大小并为新元素分配空间。
  • 将底层元素所有的现有元素都转换成与新元素相同的类型并将转换后的元素放入正确的位置上,放置过程保证其有序。
  • 将新元素添加其中。
  • 最后将 intset 的 encoding 值改为当前的编码,并增加 length。

(3)升级策略的好处:

  • 提升灵活性:通过升级策略可以随意将各种不同类型的 int 值添加其中而不用担心类型错误。
  • 节约内存:只由需要的时候才扩大整数占用的位数,节约内存。

(4)整数集合不支持降级!

压缩列表

(1)压缩列表是 redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。压缩列表可以保存任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

(2)压缩列表的属性:

  • zlbytes:记录整个压缩列表占用的字节数
  • zltail:记录压缩列表的表尾节点距离压缩列表起始节点的偏移量,可以直接定位到表尾节点。
  • zllen:记录压缩列表的节点数量,当小于 65535 时为实际值,等于 65535 时需要遍历得到长度。
  • zlend:标记压缩列表的结尾。
  • entryx:表示各个节点

(3)每个压缩列表节点可以保存长度小于等于 63(2 的 6 次方-1)的字节数组,16383(2的 14 次方-1)的字节数组,和 2 的 32 次方减一的字节数组。以及 4 位长介于 0 到 12 的无符号整数,1 字节长的有符号整数,3 字节长的有符号整数以及 int16,int32 和 int64 类型。

(4)压缩列表节点的属性有 previous_entry_length(可以为 1 字节或 5 字节,表示前一个节点的长度,程序可以通过指针运算逆序查找),encoding(长度可以为 1 字节两字节或五字节,00、01、10 开头分别表示 1 字节长度、2 字节长度和 5 字节长度的字节数组,由于前两位保存了状态所以才有前面的 2 的 6 次方和 2 的 14 次方,注意 5 字节的数组第一个字节只用到前两位保存状态,长度数据只使用后 4 个字节;11 开头表示整数编码,整数编码只使用 1 字节,后六位不同分别对应不同的整数类型)和 content 组成。

(5)连锁更新:特殊情况下,如果更新使得长度小于 254 字节的元素大于 254 字节,则其之后的 previous_entry_length 会由 1 字节变为 5 字节,而恰巧更新的元素也由小于 254 字节变为大于等于 254 字节依次往后类推就会导致连锁更新,连锁更新最坏时间复杂度为O(n*n),但日常使用极少出现此情况所以对性能影响不大。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值