【学习笔记】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:负责保存节点的值
相关文章推荐
- opencv学习(4)部分基本数据结构的介绍
- 七种排序的实现
- 冒泡排序的优化方法
- 数据结构 线性双向链表
- 数据结构理论知识
- 数据结构·字符串的最小表示法
- 4-9 二叉树的遍历 (25分)
- MySQL索引数据结构及算法原理
- c++中的图链表数据结构
- 4-1 单链表逆转 (20分)
- 【慢速学数据结构】排序
- 无情链表的创建,,插入,,删除第一个位
- 选择排序--稳定
- 整形转换成字符串
- 希尔排序--插值排序
- 快速排序
- 数据结构与算法JavaScript - 数组
- 数据结构学习之路----------线性表顺序存储结构插入和删除节点平均移动次数
- poj 1634 简单数据结构
- 线性表知识点总结一(第三天)