1.前言
Redis(REmote DIctionary Server)数据库每个键值对(key-value)都是对象构成的,其中key总是一个字符串对象;value可以是
- 字符串对象 string
- 列表对象 list
- 哈希对象 hash
- 集合对象 set
- 有序集合对象 zset
五种中的某一种.
本文主要介绍和总结这些对象底层使用的数据结构.
2.数据结构
2.1 简单动态字符串
在Redis中,包含字符串值的键值对在底层都会使用一种名为简单动态字符串(simple dynamic string,SDS)的类型作为实现.
sds.h/sdshdr结构如下图所示:
- free 记录字节数组buf未使用的长度,上图的free值为0.
- len 记录字节数组buf已使用的长度,上图的len值为5,刚好等于buf数组的长度.
- buf char类型的数组,用来保存字符串,最后一个字节保存空字符‘\0’,不计入长度.
Redis使用这种SDS类型有以下几个特点:
- 获取字符串长度时间复杂度为O(1).直接获取sdshdr的len属性值即可.
- 防止缓冲区益处.当需要对sds修改时,会先检查free的值是否满足,不满足则会自动扩展到所需的大小.
- 减少修改字符串长度时所需的内存重分配次数,空间预分配策略和惰性空间释放策略.
2.2 链表
Redis构建了自己的链表实现.链表由链表节点组成,先来看链表节点的结构,如下图:
虽然由多个listNode即可组成一个双端的链表,但是Redis使用单独的adlist.h/list结构来表示链表,如下图:
- head 指向链表中第一个listNode节点的指针.
- tail 指向链表中最后一个listNode节点的指针.
- len 链表长度计数器.
- dup 用于复制链表节点所保存的值的函数.
- free 用于释放链表节点所保存的值的函数.
- match 用于对比链表节点所保存的值和输入值比较的函数.
Redis的链表实现有以下几个特点:
- 双端链表,获取节点的前后节点时间复杂度为O(1).
- 无环链表,表头节点的prev指向NULL,表为节点的next指向NULL.
- 链表长度计数器用来获取链表长度的时间复杂度为O(1).
- 多态,listNode节点使用void*指针来保存节点值,链表可以用于保存各种不同类型的值.并且可以通过list结构的dup,free,match函数操作节点值.
2.3 字典
在字典中,一个键(key)和一个值(value)进行关联,这些关联的键和值称为键值对,Redis的字典使用哈希表作为底层的数据结构,一个哈希表里面有多个哈希节点,每个哈希节点就保存了字典中的一个键值对.
Redis字典所使用的哈希表由dict.h/dictht结构定义,如下图:
table是一个数组,数组中的每一个元素都是指向哈希表节点dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对.
哈希表节点dict.h/dictEntry结构如下图:
key属性保存键值对中的键,v属性保存键值对的值,值可以是一个指针,或者是一个uint64_t整数,或是一个int64_t整数.
next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接起来,用来解决键冲突问题.如上图,键k1和k0哈希值相同,则被连接在一起.
介绍完哈希表和哈希表节点,接下来看字典,Redis中字典由dict.h/dict结构表示,如下图:
- type 属性是一个指向dictType结构的指针,每个dictType结构保存了一些用于操作特定类型键值对的函数.
- privdata属性保存了需要传给那些特定类型函数的可选参数
- ht是一个数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]进行rehash时才会使用.
- rehashidx 记录rehash当前的进度.
Redis的字典结构有如下几个特点:
- 字典被广泛使用于Redis的各种功能,其中包括数据库和哈希键.
- 字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个在进行rehash时使用.
- 哈希表使用链表结构解决键冲突,键冲突的键值对会被连接成一个单向链表.
- 对哈希表进行扩展或收缩时,会进行rehash,把ht[0]哈希表所有的键值对rehash到ht[1]哈希表中,这个过程不是一次性完成的,而是渐进式完成的.
2.4 跳跃表
skiplist是一种有序数据结构,通过每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.跳跃表主要作为有序集合的底层实现之一.
Redis的跳跃表由跳跃表节点redis.h/zskiplistNode和跳跃表redis.h/zskiplist两个结构定义,其中跳跃表节点保存有序集合的分值和成员对象,跳跃表则用于保存跳跃表节点.
跳跃表节点redis.h/zskiplistNode定义如下图:
- level[] level数组可以有多个元素,每个数组中的元素都包含指向其他跳跃表节点前进指针,每次创建节点是会随机生成一个介于1和32之间的值作为level数组的大小.
- 前进指针 每个层都有一个指向表尾方向的前进指针,用于从表头项表尾方向访问节点.
- 跨度 层的跨度用于记录两个节点之间的距离.
- 后退指针 节点的后退指针用于从表尾向表头方向访问节点.每个节点只能有一个后退指针,每次只能后退至前一个节点.
- 分值 double类型的浮点数,跳跃表中的所有节点都按分值从小到大排序.
- 成员 节点的成员对象是一个指针,指向一个字符串对象,而字符串对象保存着一个SDS.
多个跳跃表节点就可以组成一个跳跃表,但Redis通过zskiplist结构来持有这些节点.
跳跃表redis.h/zskiplist定义如下图:
- header 指向跳跃表的表头
- tail 指向跳跃表的表尾
- length 记录节点的数量
- level 记录最大的那个节点的层数.
Redis的跳跃表结构有如下几个特点:
- 每个跳跃表节点的层高都是1至32之间的随机数
- 同一个跳跃表中,多个节点可以有相同的分值,但是成员对象必须是唯一的
- 跳跃表中的节点按照分值从小到大排序,当分值相同时,节点按照成员对象的大小进行排序.
2.5 整数集合
整数集合intset是集合对象的底层实现的数据结构之一,当一个集合只包含整数值元素,并且集合元素数量不多时,Redis会选择整数集合作为集合的底层实现.
整数集合用intset.h/intset结构来表示,如下图:
- encoding 编码方式,可以取值为16,32,64位,当编码方式的取值代表着contents就是一个多少位的数组.
- length length属性记录集合的长度.
- contents 保存集合的元素,按从小到大排序,且不重复.
Redis的整数集合结构有如下几个特点:
- 整数集合的底层实现为数组,这个数组以有序,无重复的方式保存集合元素.
- 对集合添加新元素,在有需要时,整数集合会做升级编码操作,比如从16位升级到32位.
- 升级后的整数集合不能降级.一旦对数组升级,编码会一直保持升级后的状态.
2.6 压缩列表
压缩列表是Redis为节约内存开发的顺序型数据结构.