我用了一万字,介绍了一下Redis五种类型底层实现

  • Redis 字符串是以字节数组形式存在的,是一个带长度信息的字节数组;

  • Redis 字符串长度不能超过 512M 字节;

  • Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储。具体表现于 redisObject 对象的 encoding 属性;

  • Redis 扩容时,字符串长度小于 1M 之前,加倍扩容。当超度超过 1M 之后,每次只扩容 1M;

[](()二、hash

=========================================================================

[](()2.1 字典


dict 是 Redis 服务器中出现最为频繁的复合型数据结构,比如

  • hash 结构的数据会用到字典;

  • 整个 Redis 数据库的所有 key 和 value 也组成了一个全局字典;

  • 带过期时间的 key 集合也是一个字典;

  • zset 集合中存储 value 和 score 值的映射关系也是通过 dict 结构实现的;

[](()2.2 dict 内部结构


在这里插入图片描述

dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之

struct dict {

dictht ht[2];

}

[](()2.2.1 hashtable

在这里插入图片描述

hashtable 的结构和 Java 的 HashMap 几乎是一样的,都是通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。

struct dictEntry {

void* key;

void* val;

dictEntry* next; // 链接下一个 entry

}

struct dictht {

dictEntry** table; // 二维

long size; // 第一维数组的长度

long used; // hash 表中的元素个数

}

其中,dict代表字典,dictht代表哈希表,dictEntry代表哈希表节点可以看出,dictEntry是一个数组,这很好理解,因为一个哈希表里要包含多个哈希表节点。而dict里包含2个dictht,多出的哈希表用于rehash。当哈希表保存的键值对数量过多或过少时,需要对哈希表的大小进行扩展或收缩操作,在Redis中,扩展和收缩哈希表是通过rehash实现的

[](()2.2.2 渐进式rehash

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)

{

long index;

dictEntry *entry;

dictht *ht;

// 这里进行小步搬迁

if (dictIsRehashing(d)) _dictRehashStep(d);

if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)

return NULL;

// 如果字典处于搬迁过程中,要将新的元素挂接到新的数组下面

ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];

entry = zmalloc(sizeof(*entry));

entry->next = ht->table[index];

ht->table[index] = entry;

ht->used++;

/* Set the hash entry fields. */

dictSetKey(d, entry, key);

return entry;

}

为了避免rehash对服务器性能造成影响,rehash操作不是一次性地完成的,而是分多次、渐进式地完成的。渐进式rehash的详细过程如下:

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

  2. 在字典中的索引计数器rehashidx设置为0,表示rehash操作正式开始;

  3. 在rehash期间,每次对字典执行添加、删除、修改、查找操作时,程序除了执行指定的操作外,还会顺带将ht[0]中位于rehashidx上的所有键值对迁移到ht[1]中,再将rehashidx的值加1

  4. 随着字典不断被访问,最终在某个时刻,ht[0]上的所有键值对都被迁移到ht[1]上,此时程序将rehashidx属性值设置为-1,标识rehash操作完成。

rehash期间,字典同时持有两个哈希表,此时的访问将按照如下原则处理:

  • 新添加的键值对,一律被保存到ht[1]中;

  • 删除、修改、查找等其他操作,会在两个哈希表上进行,即程序先尝试去ht[0]中访问要操作的数据,若不存在则到ht[1]中访问,再对访问到的数据做相应的处理。

[](()2.2.3 扩容操作

当满足以下任何一个条件时,程序会自动开始对哈希表执行扩展操作:

  1. 服务器目前没有执行bgsave或bgrewriteof命令,并且哈希表的负载因子大于等于1

  2. 服务器目前正在执行bgsave或bgrewriteof命令,并且哈希表的负载因子大于等于5

当元素个数和容量达到 1:1,则开始扩容(如正在执行 bgsave、bgaof 则不执行,如达到 1:5 则强制执行)

哪些指令会触发扩容:增删改。

扩容时新增、查询、修改、删除操作该如何定向?

  • 新增:当新增元素时会将元素迁移到新的 hashtable 中;

  • 查询、删除、修改:先查询 ht[0] ,若存在则迁移这个 key 所在的桶并返回元素,若不存在则到 ht[1] 中查找元素。

[](()2.2.4 缩容操作

int htNeedsResize(dict *dict) {

long long size, used;

size = dictSlots(dict);

used = dictSize(dict);

return (size > DICT_HT_INITIAL_SIZE &&

(used*100/size < HASHTABLE_MIN_FILL));

}

当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考虑 Redis 是否正在做 bgsave

[](()2.3 总结


  • Redis 为了节约内存空间使用,zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表(ziplist)进行存储,压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有冗余空隙;

  • 当 set 集合容纳的元素都是整数并且元素个数较小时,Redis 会使用 intset 来存储结合元素;

[](()三、set

========================================================================

Redis 里面 set 的结构底层实现也是字典,只不过所有的 value 都是 NULL,其它的特性和字典一模一样

[](()四、zset

=========================================================================

[](()4.1 ziplist


Redis 为了节约内存空间使用,zsethash 容器对象在元素个数较少的时候,采用压缩列表 (ziplist)进行存储压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。

在这里插入图片描述

输出的 encoding 字段是 ziplist,这表示内部采用压缩列表结构进行存储。

struct ziplist {

int32 zlbytes; // 整个压缩列表占用字节数

int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点

int16 zllength; // 元素个数

T[] entries; // 元素内容列表,挨个挨个紧凑存储

int8 zlend; // 标志压缩列表的结束,值恒为 0xFF

}

压缩列表的结构如下图所示:

在这里插入图片描述

压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一个元素,然后倒着遍历。

entry 块结构:

struct entry {

int prevlen; // 前一个 entry 的字节长度

int encoding; // 元素类型编码

optional byte[] content; // 元素内容

}

prevlen 字段表示前一个 entry 的字节长度,当压缩列表倒着遍历时,需要通过这个字段来快速定位到下一个元素的位置。它是一个变长的整数,当字符串长度小于 254(0xFE) 时,使用一个字节表示;如果达到或超出 254(0xFE) 那就使用 5 个字节来表示。第一个字节是 0xFE(254),剩余四个字节表示字符串长度。你可能会觉得用 5 个字节来表示字符串长度,是不是太浪费了。我们可以算一下,当字符串长度比较长的时候,其实 5 个字节也只占用了不到(5/(254+5))<2%的空间。

encoding字段存储了元素内容的编码类型信息,ziplist 通过这个字段来决定后面的 content 内容的形式。

Redis 通过这个字段的前缀位来识别具体存储的数据形式。下面我们来看看 Redis 是如何根据encoding的前缀位来区分内容的:

在这里插入图片描述

[](()4.1.1 增加元素

因为 ziplist 都是紧凑存储,没有冗余空间 (对比一下 Redis 的字符串结构)。意味着插入一个新的元素就需要调用 realloc 扩展内存。取决于内存分配器算法和当前的 ziplist 内存大小,realloc 可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。所以 ziplist 不适合存储大型字符串,存储的元素也不宜过多。

[](()4.2 skiplist


对于 zset ,在 redis.conf 文件中,其中有两个配置,会触发其内部编码 ziplist 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 和 skiplist 之间的变化。

在这里插入图片描述

上图中配置的含义是,有序集合的元素个数小于 zset-max-ziplist-entries 配置的值且每个元素的值的长度都小于zset-max-ziplist-value配置的值时,zset 的内部编码是 ziplist,反之则用 skiplist。

在这里插入图片描述

此时有序集合的元素个数是 2,内部编码采用的是 ziplist 的结构。

大致的储存就是这样:

在这里插入图片描述

[](()4.2.1 ziplist 到 skiplist 的变化

先验证 zset-max-ziplist-value 配置,往 memberscore 元素中塞入一个长度大于 64的字节(zset-max-ziplist-value 默认配置)的值:

在这里插入图片描述

此时的内部编码采用的是 skiplist。

那么接下来,让我们步入正题。

[](()4.2.2 什么是 skiplist?

跳表是一种可以用来代替平衡树的数据结构,跳表使用概率平衡而不是严格的平衡,因此,与平衡树相比,跳表中插入和删除的算法要简单得多,并且速度要快得多。

跳表的查找复杂度为平均O(logN),最坏O(N),效率堪比红黑树,却远比红黑树实现简单。跳跃表是在链表的基础上,通过增加索引来提高查找效率的。

有序链表插入、删除的复杂度为O(1),而查找的复杂度为O(N)。例:若要查找值为50的元素,需要从第1个元素依次向后比较,共需比较5次才行,如下图:(head节点已省略)

在这里插入图片描述

head -> 10 -> 20 -> 30 -> 40

跳表是从有序链表中选取部分节点,组成一个新链表,并以此作为原始链表的一级索引。再从一级索引中选取部分节点,组成一个新链表,并以此作为原始链表的二级索引。以此类推,可以有多级索引,如下图:

在这里插入图片描述

这样做有什么好处呢?当结构稍微一变,变成了上图的样子之后,查询路径40就是:

head -> 10 -> 30 -> 40

这个情况下我们找到指定的元素,不会超过 ( n / 2 ) + 1 个节点

我们再举个例子,找到50这个元素,到底是怎么找到的。我给大家重新画个示意图:

在这里插入图片描述

**“多了”一个向下的指针。**所以,查询 50 的路径是这样的,红色虚线箭头指示的方向:

在这里插入图片描述

按照这个往上抽节点的思想,假设我们抽到第四层:

在这里插入图片描述

我们查询 60 的时候,只需要经过 2 次。第一步就直接跳过了 50 之前的所有元素。

[](()4.3 IntSet 小整数集合


当 set 集合容纳的元素都是整数并且元素个数较小时,Redis 会使用intset来存储结合元素。intset 是紧凑的数组结构,同时支持 16 位、32 位和 64 位整数。

struct intset {

int32 encoding; // 决定整数位宽是 16 位、32 位还是 64 位

int32 length; // 元素个数

int contents; // 整数数组,可以是 16 位、32 位和 64 位

}

在这里插入图片描述

在这里插入图片描述

注意观察 debug object 的输出字段 encoding 的值,set里面放进去了非整数值时,存储形式立即从intset 转变成了 hash结构。

[](()五、list

=========================================================================

Redis 早期版本存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表 linkedlist,也就是元素少时用 ziplist,元素多时用 linkedlist。

// 链表的节点

struct listNode {

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值