Redis 的数据结构总结

386 阅读8分钟

提到Redis,大家的第一反应是去做Redis缓存,为什么呢?因为“快”是Redis的最大特点,用于做缓存,减少I/O操作,Redis非常适合,但为什么Redis会这么快呢?

实际上,这跟两方面有关,一方面,Redis是一个内存数据库,几乎所有的操作都在内存上完成,内存的访问速度相对于磁盘来说当然是非常快的;另一方面,得益于Redis的数据结构,Redis为了更加快速高效的完成增删改查操作,设计了一套适合于自己的数据结构,本文就Redis的数据结构进行简单分析。

一、数据类型与数据结构

常用Redis的同学可能会立刻说出,Redis有五种常用的数据类型:String(字符串)、List(列表)、Hash(哈希表)、Set(集合)、SortedSet(有序集合)。Redis为这五种常用的数据类型设计了底层的数据结构实现,包括SDS(简单动态字符串)、LinkedList(双向链表)、HashTable(哈希表)、SkipList(跳跃表)、IntSet(整数集合)、ZipList(压缩列表)等,他们的对应关系如下图所示:

image.png

可以看出,除了String只使用简单动态字符串实现,其他四种数据类型都是使用底层数据结构实现的,这是因为面对不同的情况,Redis在实现一个数据类型时会使用不同的底层数据结构来优化存储,可以具体看下:

列表(List)

当列表同时满足以下两个条件,列表使用ziplist编码:

  • 列表保存的所有字符串元素的长度都小于64字节;
  • 列表对象保存的元素数量小于512个; image.png

不能满足这两个条件的列表编码会使用linkedlist编码:

image.png

哈希表(Hash)

当哈希表同时满足以下两个条件,哈希表使用ziplist编码:

  • 哈希表保存的所有键值对的键和值的字符串长度都小于64字符;
  • 哈希表保存的键值对数量小于512个; image.png

不能满足这两个条件的哈希表需要使用hashtable image.png

集合(Set)

当集合同时满足以下两个标间,集合使用intset编码:

  • 集合保存的所有元素都是整数值;
  • 集合保存的元素数量不超过512个;

image.png

不能满足这两个条件的集合对象需要使用hashtable:

image.png

有序集合(SortedSet)

有序集合同时满足以下两个条件,有序集合使用ziplist编码:

  • 有序集合保存的元素数量小于128个;
  • 有序集合保存的所有元素成员的长度都小于64字符;

image.png

不能满足这两个条件的集合对象需要使用skiplist:

image.png

通过Redis的底层原理,深入了解了下Redis的底层结构到底是怎么设计的。

二、简单动态字符串

Redis自己构建了一种抽象类型:简单动态字符串(simple dynamic string, SDS),用作Redis的默认字符串表示:

image.png

  • free 属性值为0,表示SDS没有分配任何未使用空间;
  • len 属性值为5,表示SDS保存了一个5字节长的字符串;
  • buf 属性值是 char 类型数组,数组最后一个字节为"\0"; 由此可见,获取SDS长度的时间复杂度是O(1)。

SDS在性能上的优化

SDS作为“动态字符串”,支持扩充字符串时通过重分配操作(先检查SDS的空间是否满足修改所需要求,如果不满足自动扩展至所需大小)防止出现缓冲区溢出的问题;同时,SDS在缩短字符串时,程序也会释放字符串不再使用的那部分空间。同时,在频繁修改字符串的场景下,通过空间预分配惰性空间释放两种策略优化了性能:

空间预分配:当SDS被修改进行空间扩展时,Redis不仅会为SDS分配修改必须的空间,还会分配额外的空间:

  • SDS长度小于1MB,Redis会分配和len属性同样大小的未使用空间;
  • SDS长度大于等于1MB,Redis会分配1MB的未使用空间; 通过空间预分配,SDS可以减少修改之后的空间分配次数。

惰性空间释放:当SDS字符串被缩短时,Redis不会回收缩短后的字节,改为用free存下来。 通过惰性空间释放,SDS避免了缩短字符串后的内存重分配,并为预期字符串的增长提供了有利条件。

SDS对字符串操作进行的一系列优化,提高了Redis读写的速度。

三、双向链表

链表作为一种常用的非线性结构,提供了高效的节点重排能力,在Redis中,通过双向链表来实现一系列功能:

image.png 双向链表带有表头指针和表尾指针,这样获取头节点和尾节点就是O(1);另外,通过len属性,获取链表长度也是O(1)。

四、哈希表

哈希表是Redis字典的底层数据结构:

image.png sizemask属性的值总是等于size-1,这个属性和哈希值做&运算,决定一个键应该被放到table数组的哪个索引上。

解决键冲突

Redis的哈希表用链地址法来解决键冲突;并且,为了更快的速度,Redis总是将新节点添加到链表的表头位置(时间复杂度为O(1))。

rehash

随着操作的不断执行,当哈希表保存的键值对数量太多或者太少,Redis会对哈希表的大小进行响应的扩展和收缩,简单来说,就是利用空闲的哈希表进行扩展或收缩操作,并将默认哈希表重分配到指定的哈希表上,最后设置新的哈希表为默认哈希表。

当然,rehash的动作不是一次性的,而是渐进的过程,这么做是为了防止rehash节点过多导致服务器在一定时间内停止访问。在渐进式rehash过程中,删除/查找/更新的操作会在两个哈希表同时进行,添加的操作一律会被保存在新的哈希表上。

什么时候会触发rehash呢?有两种情况:

  1. Redis没有执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子为1,自动扩展;
  2. Redis正在执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子为5,自动扩展;
  3. 哈希表的负载因子小于0.1,自动收缩;

其中,负载因子的计算公式:

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

五、跳跃表

跳跃表是一种有序数据结构,支持平均O(logN),最坏O(n)复杂度的节点查找,如果一个有序集合包含元素比较多的时候,Redis就会使用跳跃表来作为有序集合的底层实现:

image.png 每次创建一个新跳跃表的节点时,Redis就会根据幂次定率随机生成一个介于1到32之间到值作为lavel数组的大小,这个就是层的大小(或高度)。层与层之间可以通过前进指针快速到达最近的层,也可以通过后退指针(BW)后退至前一个节点。

节点的分值是一个double类型的浮点数,跳跃表的所有节点都是按照分值从小到大排列。节点的成员对象是指向一个字符串对象的指针,分值相同的节点按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面

六、整数集合

当一个集合只包含整数值元素,并且集合元素数量不多时,就会用到整数集合:

image.png encoding 属性可能为 INTSET_ENC_INT16/INTSET_ENC_INT32/INTSET_ENC_INT64,如果我们要将一个新元素添加到集合中,并且新元素的类型比集合中现在所有的类型都要长,则需要先讲整数集合升级,才能将新元素添加进来(整数集合不支持降级),这是为了节约内存。

七、压缩列表

压缩列表是Redis为了节约内存而开发的,一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者整数值:

image.png 其中:

  • zlbytes:表示压缩列表总长度
  • zltail:存储表尾节点的偏移量
  • zllen:表示压缩列表节点数 通过压缩列表,可以令Redis针对一些简单的数据进一步节省空间。

八、扩展数据类型

Redis 面对海量计算等特殊场景时,还给我们提供了Bitmap、HyperLogLog和GEO三种特殊的类型。

  • Bitmap本身时用String类型作为底层数据结构实现的一种统计二值状态的数据类型,String类型是会保存为二进制的字节数组,Redis就把字节数组的每个bit位利用起来,用来表示一个元素的二值状态。
  • HyperLogLog是一种用于统计技术的数据集合类型,当集合元素数量非常多时,它计算基数所需的空间总是固定的,常用于各种统计场景。
  • GEO是面向LBS服务场景的数据类型,使用GeoHash编码对经纬度到排序集合中元素权重分数到转换,相当于使用排序集合“有范围”的查找

总结

本文主要就Redis的数据结构进行简要解析,包含常用的数据类型的底层结构以及Redis为什么这么快的真实秘诀。