redis有五种基本数据结构:字符串、hash、set、zset、list。可是你知道构成这五种结构的底层数据结构是怎样的吗? 今天咱们来花费五分钟的时间了解一下。 (目前redis版本为3.0.6)redis
SDS是"simple dynamic string"的缩写。 redis中全部场景中出现的字符串,基本都是由SDS来实现的数据库
set msg "hello world"
中的key msg.RPUSH fruits "apple" "banana" "cherry"
中的"apple" "banana" "cherry"free:还剩多少空间 len:字符串长度 buf:存放的字符数组数组
为减小修改字符串带来的内存重分配次数,sds采用了“一次管够”的策略:服务器
为避免缩短字符串时候的内存重分配操做,sds在数据减小时,并不马上释放空间。微信
就是redis中存放的各类数字 包括一下这种,故意加引号“”的数据结构
长这样:app
分两部分,一部分是“统筹部分”:橘黄色,一部分是“具体实施方“:蓝色。函数
主体”统筹部分“:ui
head
指向具体双向链表的头tail
指向具体双向链表的尾len
双向链表的长度具体"实施方":一目了然的双向链表结构,有前驱pre
有后继next
this
由list
和listNode
两个数据结构构成。
压缩列表。 redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各类语言的数组相似,它是由连续的内存块组成的,这样一来,因为内存是连续的,就减小了不少内存碎片和指针的内存占用,进而节约了内存。
而后文中的entry
的结构是这样的:
先找到列表尾部元素:
而后再根据ziplist节点元素中的previous_entry_length
属性,来逐个遍历:
再次看看entry
元素的结构,有一个previous_entry_length
字段,他的长度要么都是1个字节,要么都是5个字节:
previous_entry_length
长度为1字节previous_entry_length
长度为5字节假设如今存在一组压缩列表,长度都在250字节至253字节之间,忽然新增一新节点new
, 长度大于等于254字节,会出现:
程序须要不断的对压缩列表进行空间重分配工做,直到结束。
除了增长操做,删除操做也有可能带来“连锁更新”。 请看下图,ziplist中全部entry节点的长度都在250字节至253字节之间,big节点长度大于254字节,small节点小于254字节。
哈希表略微有点复杂。哈希表的制做方法通常有两种,一种是:开放寻址法
,一种是拉链法
。redis的哈希表的制做使用的是拉链法
。
总体结构以下图:
也是分为两部分:左边橘黄色部分和右边蓝色部分,一样,也是”统筹“和”实施“的关系。 具体哈希表的实现,都是在蓝色部分实现的。 先来看看蓝色部分:
这也分为左右两边“统筹”和“实施”的两部分。
右边部分很容易理解:就是一般拉链表实现的哈希表的样式;数组就是bucket,通常不一样的key首先会定位到不一样的bucket,若key重复,就用链表把冲突的key串起来。
再来看看哈希表整体图中左边橘黄色的“统筹”部分,其中有两个关键的属性:ht
和rehashidx
。 ht
是一个数组,有且只有俩元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx和哈希表的rehash
有关。
rehash
指的是从新计算键的哈希值和索引值,而后将键值对重排的过程。
加载因子(load factor) = ht[0].used / ht[0].size
。
扩容:
加载因子
大于等于1。加载因子
大于等于5。收缩:
加载因子
小于0.1时,程序自动开始对哈希表进行收缩操做。扩容:
ht[0].used * 2
的2^n
(2的n次方幂)。收缩:
ht[0].used
的2^n
(2的n次方幂)。(如下部分属于细节分析,能够跳过直接看扩容步骤) 对于收缩,我当时陷入了疑虑:收缩标准是加载因子
小于0.1的时候,也就是说假如哈希表中有4个元素的话,哈希表的长度只要大于40,就会进行收缩,假若有一个长度大于40,可是存在的元素为4即(ht[0].used
为4)的哈希表,进行收缩,那收缩后的值为多少?
我想了一下:按照前文所讲的内容,应该是4。 可是,假如是4,存在和收缩后的长度相等,是否是又该扩容? 翻开源码看看:
收缩具体函数:
int dictResize(dict *d) //缩小字典d
{
int minimal;
//若是dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERR
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used; //得到已经有的节点数量做为最小限度minimal
if (minimal < DICT_HT_INITIAL_SIZE)//可是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal); //用minimal调整字典d的大小
}
复制代码
int dictExpand(dict *d, unsigned long size) //根据size调整或建立字典d的哈希表
{
dictht n;
unsigned long realsize = _dictNextPower(size); //得到一个最接近2^n的realsize
if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不够大返回出错标志
return DICT_ERR;
if (realsize == d->ht[0].size) return DICT_ERR; //若是新的realsize和本来的size同样则返回出错标志
/* Allocate the new hash table and initialize all pointers to NULL */
//初始化新的哈希表的成员
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) { //若是ht[0]哈希表为空,则将新的哈希表n设置为ht[0]
d->ht[0] = n;
return DICT_OK;
}
d->ht[1] = n; //若是ht[0]非空,则须要rehash
d->rehashidx = 0; //设置rehash标志位为0,开始渐进式rehash(incremental rehashing)
return DICT_OK;
}
复制代码
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE; //DICT_HT_INITIAL_SIZE 为 4
if (size >= LONG_MAX) return LONG_MAX + 1LU;
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
复制代码
由代码咱们能够看到,假如收缩后长度为4,不只不会收缩,甚至还会报错。(😝)
咱们回过头来再看看设定:题目可能成立吗? 哈希表的扩容都是2倍增加的,最小是4, 4 ===》 8 ====》 16 =====》 32 ======》 64 ====》 128
也就是说:不存在长度为 40多的状况,只能是64。可是若是是64的话,64 X 0.1(收缩界限)= 6.4 ,也就是说在减小到6的时候,哈希表就会收缩,会缩小到多少呢?是8。此时,再继续减小到4,也不会再收缩了。因此,根本不存在一个长度大于40,可是存在的元素为4的哈希表的。
在"扩容步骤"和"收缩步骤" 两幅动图中每幅图的第四步骤“将ht[0]中的数据利用哈希函数从新计算,rehash到ht[1]”,并非一步完成的,而是分红N多步,按部就班的完成的。 由于hash中有可能存放几千万甚至上亿个key,毕竟Redis中每一个hash中能够存2^32 - 1
键值对(40多亿),假如一次性将这些键值rehash的话,可能会致使服务器在一段时间内中止服务,毕竟哈希函数就得计算一阵子呢((#^.^#))。
哈希表的refresh是分屡次、渐进式进行的。
渐进式refresh和下图中左边橘黄色的“统筹”部分中的rehashidx
密切相关:
甚至在进行期间,每次对哈希表的增删改查操做,除了正常执行以外,还会顺带将ht[0]哈希表相关键值对rehash到ht[1]。
以扩容步骤为例:
整数集合是集合键的底层实现方式之一。
跳表这种数据结构长这样:
redis中把跳表抽象成以下所示:
看这个图,左边“统筹”,右边实现。 统筹部分有如下几点说明:
实现部分有如下几点说明:
redis中并无直接使用以上所说的各类数据结构来实现键值数据库,而是基于一种对象,对象底层再间接的引用上文所说的具体的数据结构。
结构以下图:
其中:embstr和raw都是由SDS动态字符串构成的。惟一区别是:raw是分配内存的时候,redisobject和 sds 各分配一块内存,而embstr是redisobject和raw在一起内存中。
互联网技术窝
或者加微信共同探讨交流: