您的位置:首页 > 理论基础 > 数据结构算法

【学习笔记】Redis(1)-数据结构

2016-07-20 22:45 330 查看

1. 字符串

1.1 结构

在 Redis 里,最常使用到的就是字符串了。Redis 有自己构建的字符串类型,叫做简单动态字符串(simple dynamic string,SDS),Redis 的键值对在底层都是由 SDS 实现的。下面是一个保存了 ‘Hello’ 这个字符串的 SDS 的结构:



(1)free:用来记录 buf 数组中未使用的字节数量

(2)len:用来记录 buf 数组中已使用的字节数量(不包括 '\0' )

(3)buf:是一个字节数组,用于保存字符串(buf 总长度 = len + free + 1)

SDS 遵循C字符串以 ‘\0',好处是可以直接重用一部分C字符串函数库里面的函数。由于C字符串不记录自身长度,那么在执行一些类似 strcat(dest, src) 函数的时候,如果 dest 没有足够的空间容纳下 src 的话,就会造成缓冲区溢出,而 SDS 通过 free 这个值来判断有没有足够的空间来容纳下 src,空间不足的话就会自动进行空间分配,这样就杜绝了缓冲区的溢出。

1.2 空间的分配与释放

当对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序会为 SDS 分配修改所需要的空间和额外未使用的空间。通过这种策略,可以减少连续执行字符串增长操作所需的内存重分配次数,分配的策略如下:

● 如果对 SDS 进行修改后,SDS 的 len 小于 1MB,那么程序将分配和 len 属性同样大小的未使用空间。

● 如果对 SDS 进行修改后,SDS 的 len 大于 1MB,那么程序将分配 1MB 的未使用空间。

当使用 SDS 的API 对 SDS 保存的字符串进行缩短的时候,程序并不会立即回收多余的字节,而是通过使用 free 属性将这些空闲的字节数量记录下来,以备之后使用。当然 SDS 也提供了相应的 API 可以让我们在有需要时,真正的释放未使用的空间。

1.3 二进制安全

C字符串因为使用了 '\0' 作为字符串的结尾标识,导致了C字符串只能保存文本数据,不能保存像图片、视频等这样的二进制数据。而 SDS 使用 len 属性的值来判断一个字符串是否结束,并且 Redis 不是使用 SDS 的 buf 数组来保存字符,而是用它来保存一系列的二进制数据,所以 SDS 不仅可以保存文本数据,还可以保存任意格式的二进制数据。

2. 链表

Redis 中使用的链表是一个双端链表,它的特性如下:

● 双端:链表节点带有 prev 和 next 指针,使得它获取某个节点的前置节点和后置节点的复杂度都为 O(1)

● 无环:对链表的访问以 NULL 为终点

● 表头表尾指针:通过 head 和 tail 指针,使得获取表头或表尾节点的复杂度为 O(1)

● 带有 len 属性:使得程序获取链表长度的复杂度为 O(1)

● 多态:链表节点使用 void* 指针来保存节点值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值

下面是一个 Redis 中链表的结构(如果对链表的概念不是很熟悉的话,可以自己去查阅资料,这里就不多做介绍了):



3. 字典

3.1 结构

字典被广泛用于实现 Redis 的各种功能,在字典中,一个键和一个值进行关联,这些关联的键和值就称为键值对,字典中的每个键都是唯一的,Redis 字典中所使用的哈希表的结构如图所示:



(1)table:是一个数组,数组中的每个元素都是一个 dictEntry 结构的指针,每个 dictEntry 结构保存着一个键值对

(2)size:记录了哈希表的大小,即table数组的大小

(3)sizemask:值总是等于 size-1,它和哈希值一起决定一个键应该被放到table数组的哪个索引上

(4)used:记录了哈希表目前已有的节点数量

Redis 的哈希表采用链地址法来解决键冲突问题(有关链地址法和相关的哈希算法在这里不多做介绍)。

Redis 的字典由哈希表实现,具体的结构如下图所示:



(1)type:是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数

(2)privdata:保存了需要传给那些类型特定函数的可选参数

(3)ht:是一个包含两个项的数组,每一个项都是一个 dictht 哈希表,一般情况下只使用 ht[0],ht[1] 只会在对 ht[0] 进行 rehash 时使用

(4)rehashidx:用来记录 rehash 的当前进度,当没有进行 rehash 时,它的值为 -1

3.2 rehash

当哈希表保存的键值对数量太多或太少时,程序就会对哈希表的大小进行相应的扩展或收缩,这个过程通过执行 rehash 操作来完成,Redis 的 rehash 步骤如下:

(1)为字典的 ht[1] 分配空间,空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对数量:

如果执行的是扩展操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used*2 的 2^n(2的n次方)
如果执行的是收缩操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n(2的n次方)

(2)将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面,即重新计算键的哈希值和索引值,然后再放置到 ht[1] 上。

(3)当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0] 变为空表),释放 ht[0],将 ht[1] 设置为 ht[0],并为 ht[1] 新建一个空白哈希表,为下一次 rehash 做准备。

Redis 在执行 rehash 的时候并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。如果不这样做,那么当一次性把大量的键值对 rehash 到 ht[1] 时,会导致服务器在一段时间内停止服务。因此,为了避免 rehash 对服务器性能造成影响,服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1],而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1]。渐进式 rehash 的详细步骤如下:

(1)为 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。

(2)在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为 0,表示 rehash 工作正式开始。

(3)在 rehash 进行期间,每次对字典执行添加、删除、查找或更新操作时,程序除了执行指定的操作之外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对转移到 ht[1],当 rehash 工作完成之后,程序将 rehashidx 属性的值加一。

(4)随着字典操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 到 ht[1] 上,这时程序将 rehashidx 属性值设置为 1,表示 rehash 操作完成。

因为在进行渐进式 rehash 的过程中,字典 ht[0] 中的键值对会不断被转移到 ht[1] 中,所以对字典的操作会在两个哈希表上进行。例如,要在字典里面查找一个键的话,程序会先在 ht[0] 里面进行查找,如果没有找到的话,就会继续到 ht[1] 里面进行查找。另外,在渐进式 rehash 执行期间,新添加到字典的键值对都会被保存到 ht[1] 里面,而 ht[0] 则不在进行任何添加操作,这保证了 ht[0] 的键值对数量只会减少,并随着 rehash 操作的执行,最后变为空表。

4. 跳表

跳表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。Redis 只在两个地方用到了跳表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。跳表的结构如下图所示(关于跳表本身的具体实现在这里不多做介绍):



位于最左边的是 zskiplist 结构:

(1)header:指向跳表表头节点的指针

(2)tail:指向跳表的表尾节点的指针

(3)level:记录跳表中层数最大的那个节点的层数(除表头节点外)

(4)length:记录跳表的长度,即包含节点的个数(不计表头节点)

位于右边的是 zskiplistNode 结构:

(1)level:记录了各个层,L1 代表第一层,L2 代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,跨度记录了前进指针所指向节点和当前节点的距离,跨度是用来计算 rank 的。图中的带有数字的箭头代表前进指针,数字代表跨度。

(2)backward:是一个后退指针(图中的 BW),它指向位于当前节点的前一个节点,在程序从表尾向表头遍历时使用。

(3)score:保存节点的分值(图中的 5.0、10.0、15.0)。

(4)obj:指向保存对象的指针(图中的 o1、o2、o3)。

在跳表中,各个节点保存的成员对象是唯一的,但是节点的分值可以是相同的。节点按照分值从小到大排列,分值相同的节点按照成员对象在字典序中的大小从小到大排序。

我们现在为上图的四个节点进行标号,表头节点标号为①、o1 节点标号为②、o2 节点标号为③、o3 节点标号为④,那么当查找上图跳表中分值为 10.0 的成员时,经过的路径为:①L4 → ②L2 → ③,此时经过的跨度和为 2,所以这个对象的 rank 为 2。

5. 压缩列表

压缩列表是列表键和哈希键的底层实现之一。只有当列表键(哈希键)包含少量列表项(键值对),并且每个列表项(键值对)要么是小整数值,要么是长度较短的字符串,Redis 才会使用压缩列表来作为底层实现。

压缩列表的主要目的是为了节约内存,它是由连续内存块组成的顺序型数据结构。压缩列表的组成如下图所示:



(1)zlbytes:记录整个压缩列表占用的内存字节数

(2)zltail:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节

(3)zllen:记录压缩列表包含的节点数

(4)entry_*:表示压缩列表包含的各个节点

(5)zlend:是一个特殊值 OxFF,用于标记压缩列表的末端

压缩列表的每个节点由三部分组成,分别为previous_entry_length、encoding和content,具体如下:

(1)previous_entry_length:记录了前一个节点的长度,通过这个属性可以很方便的计算出前一个节点的起始地址

(2)encoding:记录了节点的content属性所保存数据的类型和长度

(3)content:负责保存节点的值
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: