本文将详细介绍Redis的六种底层数据结构:简单动态字符串、双向链表、字典、跳跃表、整数集合和压缩列表。
Redis没有直接使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的可以被修改的抽象类型,并将SDS用作Redis的默认字符串表示。
SDS结构定义如下:
注:
free属性的值为0,表示这个SDS没有分配任何未使用空间。
len属性的值为5,表示这个SDS保存了一个五字节长的字符串。
buf属性是一个char类型的数组,数组最后保存了空字符‘\0’。
SDS通过获取len属性就可以得到字符串的长度,时间复杂度为:O(1)O(1)O(1)
c字符串需要遍历字符串,时间复杂度为:O(N)O(N)O(N)
c字符串如果没有重新分配空间,直接修改字符串的话,可能会造成数据溢出。
当SDS的API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的需求,如果不满足,则自动将SDS空间扩展至所需大小。
SDS通过空间预分配和惰性空间释放两种优化策略来减少内存重分配次数。
SDS API会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何的限制、过滤或者假设,所以SDS的API都是二进制安全的。
SDS之所以在末尾保存一个空字符’\0’,是为了使用一些c字符串
例如 字符串对比函数:
字典,又被称为符号表、关联数组或映射,是一种用于保存键值对的抽象数据结构。
字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等。
字典用途:Redis数据库底层实现、哈希键底层实现。
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
接下来的三个小节将分别介绍Redis的哈希表节点、哈希表以及字典的实现。
字典、哈希表和哈希表节点关系图:
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上。
若果字典被用做数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
MurmurHash2算法用来计算键的哈希值,特点是运算性能高,碰撞率低。
详细参考:https://blog.csdn.net/thinkmo/article/details/26833565
随着操作的不断执行,哈希表保存的键值对会主键增多或者减少,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。
扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,步骤如下:
1)为字典的ht[1]哈希表分配空间,空间大小根据实际情况而定;
2)将ht[0]中所有键值对rehash到ht[1]中
注意:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上
3)释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建一个空表,为下次rehash做准备
rehash操作的结构变化如下:
注:
rehash操作是渐进式的。
渐进式的rehash将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上。
之所以这样做,是考虑到如果哈希表保存的键值对的数量是百万级甚至千万级时,一次性进行rehash可能会导致服务器停止服务,渐进式地rehash避免了对服务器性能造成影响。
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群结点中用作内部数据结构,除此之外,跳跃表没有其他用途。
仅靠过个跳跃表节点就可以组成一个跳跃表,但通过使用一个zskiplist结构来持有这些节点,就可以很方便地对整个跳跃表进行处理。
zskiplist结构如图:
下图分别展示了完整的跳跃表和单个节点的详细结构图:
整数集合是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为 int16_t 、int32_t 或者 int64_t 的整数值,并且保证集合中不出现重复值。
如果我们想要添加一个新元素到整数集合里面,但是新元素的类型比整数集合原有的元素类型都要长时,我们就要对整数集合进行升级,然后才能将新元素添加到整数集合里面。
另外,还需注意,Redis的整数集合不支持降级。
升级整数集合并添加新元素共分为三步进行:
1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
2)将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素继续维持底层数组的有序性质不变。
3)将新元素添加到底层数组里面。
升级步骤图解:
压缩列表是列表键和哈希键的底层实现之一。当一个列表键值包含少量列表键,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点 (entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表结构:
压缩列表节点的结构如图:
假设压缩列表中所有节点的previous_entry_length属性都是用1字节来保存,那么节点的长度只要小于等于253字节previous_entry_length都可以记录,但是,如果添加一个长度大于253字节的节点,那么下一个节点的previous_entry_length就无法保存该长度的值,同样的,下下个节点也无法保存上个节点的长度,由此将导致连续多次空间扩展操作。
添加节点和删除节点都可能导致连锁更新,但是这种操作出现的几率很低。