文章目录
Redis底层数据结构一、字典1.1 Hash表节点Entry1.2 Hash表dictht1.3 字典dict1.4 字典reHash1.5 小结
二、跳跃表2.1 跳跃表节点zskiplistNode2.2 跳跃表zskiplist2.3 有序集合zset2.4 跳跃表操作2.5 小结
三、参考
Redis底层数据结构
Redis底层数据结构是Redis基本数据类型和诸多功能实现的基础,字典和跳跃表是最重要的两种数据结构。
一、字典
Redis中的字典是一种以Hash表为基础的数据结构,在Redis的很多功能实现中都使用到了字典。我们来看看Hash表和字典在Redis源码中的定义等信息。下面代码在Redis源码的src/dict.h文件中,可阅读参考文章[2]。
1.1 Hash表节点Entry
dictEntry是hash表存储的单个元素,类似于Java的HashMap中的Entry
typedef struct dictEntry
{
void *key
;
union {
void *val
;
uint64_t u64
;
int64_t s64
;
} v
;
struct dictEntry
*next
;
} dictEntry
;
1.2 Hash表dictht
typedef struct dictht
{
dictEntry
**table
;
unsigned long size
;
unsigned long sizemask
;
unsigned long used
;
} dictht
;
1.3 字典dict
dict是Redis的字典,其中包含两个哈希表dictht,这是为了方便进行rehash操作。在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上面,完成之后释放空间并交换两个dictht 的角色。其实字典本身的数据结构特点和Hash表是一样的,内部保存2个 Hash表以及rehashindex等辅助信息是便于实现渐进式rehash。
typedef struct dict
{
dictType
*type
;
void *privdata
;
dictht ht
[2];
int rehashidx
;
int iterators
;
} dict
;
字典的rehash操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的rehash操作给服务器带来过大的负担。渐进式rehash通过记录dict的rehashidx完成,它从0开始每执行一次rehash都会递增。例如在一次rehash中,要把dict[0] rehash到 dict[1],会把dict[0]上table[rehashidx]的键值对rehash到dict[1]上,dict[0]的table[rehashidx]指向null,并令rehashidx++。在rehash期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式rehash。采用渐进式rehash 会导致字典中的数据分散在两个dictht 上,因此对字典的查找操作也需要到对应的dictht去执行。
1.4 字典reHash
下面是字典结构的rehash操作函数。我对比了源码,这个是Redis2.8及一下的版本源码,3.0及以上版本源码略有不同。
int dictRehash(dict
*d
, int n
) {
if (!dictIsRehashing(d
)) return 0;
while(n
--) {
dictEntry
*de
, *nextde
;
if (d
->ht
[0].used
== 0) {
zfree(d
->ht
[0].table
);
d
->ht
[0] = d
->ht
[1];
_dictReset(&d
->ht
[1]);
d
->rehashidx
= -1;
return 0;
}
assert(d
->ht
[0].size
> (unsigned)d
->rehashidx
);
while(d
->ht
[0].table
[d
->rehashidx
] == NULL) d
->rehashidx
++;
de
= d
->ht
[0].table
[d
->rehashidx
];
while(de
) {
unsigned int h
;
nextde
= de
->next
;
h
= dictHashKey(d
, de
->key
) & d
->ht
[1].sizemask
;
de
->next
= d
->ht
[1].table
[h
];
d
->ht
[1].table
[h
] = de
;
d
->ht
[0].used
--;
d
->ht
[1].used
++;
de
= nextde
;
}
d
->ht
[0].table
[d
->rehashidx
] = NULL;
d
->rehashidx
++;
}
return 1;
}
1.5 小结
字典也是和Hash表一样的k-v存储集合,但是是基于Hash表来实现的。在字典的内部维护了2个Hash表,来实现渐进式rehash。rehash的时候不是一次性完成,而是渐进式的将元素从一个Hash表迁移到另一个Hash表,注意一次迁移的不是一个k-v对结构dictEntry,而是按照桶为单位进行迁移的。在Redis的本身源码中,字典是很多基础功能实现的基础数据结构,比如在Redis集群中使用字典来保存集群的所有服务器节点信息,在Redis中默认包含16个数据库,每一个库对应一个redisDb的数据结构,在redisDb内部就是通过一个dict字典来保存该库下的全部键值对字典可以理解为一个优化后的Hash表,其功能和Hash几乎一样,但是做rehash的时候性能更好,以空间换取了时间。
二、跳跃表
跳跃表是一种数据结构,它基于多指针有序链表实现的,可以看成多个有序链表。在Redis中它是有序集合的底层实现之一。另外在Redis中一些涉及到排序的场景也会使用到跳跃表,比如Redis分片集群中使用跳跃表保存了槽和key之间的关系,以槽为分值键为成员对槽进行排序,便于进行区间操作,或者处理槽和键之间的关系和对槽进行rehash。
Redis中的有序集合是一种以跳跃表为基础的数据结构,我们来看看跳跃表和有序集合在Redis源码中的定义等信息。下面代码在Redis源码的src/redis.h文件中,可阅读参考文章[2]。
2.1 跳跃表节点zskiplistNode
zskiplistNode是对应跳跃表节点的数据结构。节点除了包含目标的key之外,还包含分值、前后指针、跨度等信息。
typedef struct zskiplistNode
{
robj
*obj
;
double score
;
struct zskiplistNode
*backward
;
struct zskiplistLevel
{
struct zskiplistNode
*forward
;
unsigned int span
;
} level
[];
} zskiplistNode
;
2.2 跳跃表zskiplist
zskiplist是对应跳跃表的数据结构。包含一个跳跃表的头尾节点指针、节点数量和最大层数。
typedef struct zskiplist
{
struct zskiplistNode
*header
, *tail
;
unsigned long length
;
int level
;
} zskiplist
;
2.3 有序集合zset
zset是对应有序集合的数据结构。zset内部维护一个字典来保证按照成员取出分值,提示维护一个跳跃表来支持按照分值操作成员,比如排序等。
typedef struct zset
{
dict
*dict
;
zskiplist
*zsl
;
} zset
;
2.4 跳跃表操作
跳表的查找,插入等操作,可以阅读参考文章[4]
2.5 小结
跳跃表也是一种加速查找的数据结构,内部通过多层指针和双向指针来实现加速查找,在排序和范围操作时性能较好,并基于它实现了Redis中的有序集合。相比于红黑树这样的查找结构,有下面优点,为什么Redis中不使用红黑树呢?后面给了一个表格
A.插入快。因为不需要进行旋转等操作来维护平衡性;
B.更容易实现。实现比红黑树简单许多;
C.支持无锁操作。
对比项红黑树复杂度跳表复杂度
插入元素O(log n)O(log n)删除元素O(log n)O(log n)查找元素O(log n)O(log n)有序输出元素O(log n)O(log n)区间操作元素不如跳表性能很好
区间操作时,红黑树定位到端点后,再从首位置开始每次都要查找后继节点,相对来说是比较耗时的。跳表的区间操作,定位到2端的元素之后,顺序遍历即可,显然性能更好
三、参考
[1] CS-Notes/Redis[2] Redis源码-带注释[3] Redis 设计与实现-黄健宏[4] 拜托,面试别再问我跳表了!