Redis底层数据结构

  Redis以高性能著称,不仅其因为采用了单线程,IO多路复用,纯内存读写等等,它还在底层的数据结构实现上用了很大的心思。针对不同的场景采用了不同的数据结构,以提升数据读写效率,降低数据存储空间。

Redis底层使用的六种数据结构

  

Hash表

  首先Redis的底层实现采用的是Hash表数据结构,其实hash表本身就是一个数组,将key通过hash算法计算得出hash值对数组长度取模,用得到的值作为数组下标,然后把value保存在数组下标的位置。由于存储结构是数组,所以hash表的读复杂度是O(1)。

应用场景

  1.Redis键值对的底层实现。

  2.当一个哈希键包含的键值对比较多,又或者键值对总的元素都是比较长的字符串时,Redis会使用hash表作为哈希键的底层实现。

  3.集合键的底层实现之一。

哈希冲突问题

  当hash表的负载因子过大时,会频繁出现hash冲突的问题,不同的key经过hash计算和取模得到相同的数组下标。redis使用链表法,将数组下标相同的key使用链表串联起来,以解决哈希冲突的问题。但是当哈希冲突过多,链表长度过长时(需遍历链表,查找key完全匹配的数据),那么它的查询效率也将会降低到O(n),这个时候就要考虑增大数组的长度,执行rehash操作。

渐进式rehash

    rehash操作需要创建一个新的长度*2的数组,将所有的key进行hash计算并对新的数组长度取模,然后把数据放在新的数组下标中,这样可以大幅度减少哈希冲突的比例,防止查询效率的退化。但是在生产系统中的数据量是很大的,如果一下子对所有的key进行rehash计算并迁移,那么会导致cpu负载过重,响应正常的对外提供服务。redis采用了渐进式的rehash方式,分批次的将所有key完成rehash,然后将指针引用到新的数组地址。

链表

  Redis实现了自定义的双向链表数据结结构,在redis的底层被广泛应用。在链表中查询一个给定的值的复杂度是O(N),将一个新的节点插入到链表头部/尾部的复杂度是O(1)。

应用场景

  • 链表是list键的底层实现之一,当一个列表键包含了数量比较多的元素,又或者列表中保存的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
  • 发布与订阅
  • 慢查询
  • 监视器等

跳表

  跳表是一种有序的数据结构,它通过在每个节点中维护多个指向其他节点的指针,从而达到快速访问节点的目的。跳表支持平均O(logN),最坏O(N)复杂度的节点查找,在大部分情况下,跳表的效率可以和平衡树媲美,并且实现更为简单,所以有不少程序都是用跳表代替平衡树。

          

应用场景

  redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳表来作为有序集合键的底层实现。

整型集合

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

  整数集合的底层实现为数组,这个数组以有序,无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的数据类型,改变这个数组的数据类型(升级操作)。

  升级操作为整数集合带来了操作的灵活性,并且尽可能的节约了内存。

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

数据结构

typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组,虽然声明为 int8_t 类型,并不保存任何 int8_t 类型的值,真正的类型取决于 encoding 的值  
    int8_t contents[]; 
} intset;

升级图解

 

 

 

压缩列表

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

应用场景 

  压缩列表时列表键和哈希键的底层实现之一。

  当一个列表键只包含少量列表项,并且每个列表项都是小整数值或长度较短的字符串时。

  当一个哈希键只包含少量的键值对,并且每个键值对都是小整数值或长度较短的字符串时。

设计实现

 

  压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数;

  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;

  • zllen,记录压缩列表包含的节点数量;

  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

  在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素。

 

 

  压缩列表节点包含三部分内容:

  • prevlen,记录了「前一个节点」的长度;

  • encoding,记录了当前节点实际数据的类型以及长度;

  • data,记录了当前节点的实际数据;

 连锁更新

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

 

posted @ 2022-04-25 14:29  唐磊(Jason)  阅读(2723)  评论(0编辑  收藏  举报