Redis核心技术-数据结构1-底层(动态字符串、双向链表、压缩列表、跳表)结构

Redis 使用哈希表保存键值对,实现快速查找。哈希表中的value是值的指针地址,通过计算key的哈希值定位。哈希冲突通过链式哈希解决,rehash时使用渐进式方法避免阻塞。当数据量变化时,Redis会根据特定条件进行哈希表的扩容或缩容。此外,Redis还使用多种底层数据结构如压缩列表、跳跃表等,以适应不同场景需求。
摘要由CSDN通过智能技术生成

  Redis的大部分操作都是对于键值对的增删改查,想要实现快速的查找并且操作键值对,底层的数据结构尤为重要。

  我们平常所熟悉的String、List、Set、Sorted Set、Hash等只是Redis键值对中值的数据类型,也就是数据的保存类型,而他们底层的实现是如何的呢?

Redis中Key和value是如何组织的?

  Redis使用哈希表保存所有的key和value,目的是为了从key到value的快速访问。

  哈希表中的value保存的并非值得本身,而是值对应的指针地址。
在这里插入图片描述

为什么使用哈希表?

  时间复杂度为O(1),只需要计算key的哈希值即可找到对应的value指针。key的哈希值的计算和数据量的大小没有关系,也就说不管哈希表里面有1万key和1000万的key,只需要计算一次key的哈希值即可找到对应value的指针。

哈希表的问题

  哈希冲突问题(不同的key相同的哈希值)和rehash问题带来的操作阻塞。

  具体的表现,当你往Redis写入大量的数据时,突然发现响应变慢了。

如何解决哈希冲突问题?

使用链式哈希,同一个哈希桶中的多个元素间用链表保存,元素间通过指针关联。如此一来,链表上的数据还是需要逐一查找后才能操作,意思是链表越长,查询和操作对应的耗时也就越长问题,如何解决呢?使用rehash(增加哈希桶的数量)。
在这里插入图片描述

rehash实现。

  采用渐进式rehash方式(分而治之的方式),避免rehash时大量可以的拷贝导致的操作阻塞。

什么是渐进式rehash?

Cluster模式下:
1.一个Redis实例对应一个RedisDB(db0);
2.一个RedisDB对应一个Dict;
3.一个Dict对应2个Dictht,正常情况只用到ht[0];ht[1] 在Rehash时使用。

/* Redis数据库结构体 */
typedef struct redisDb {
    // 数据库键空间,存放着所有的键值对(键为key,值为相应的类型对象)
    dict *dict;                 
    // 键的过期时间
    dict *expires;              
    // 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)
    dict *blocking_keys;       
    // 准备好数据可以解除阻塞状态的键和相应的client
    dict *ready_keys;           
    // 被watch命令监控的key和相应client
    dict *watched_keys;         
    // 数据库ID标识
    int id;
    // 数据库内所有键的平均TTL(生存时间)
    long long avg_ttl;         
} redisDb;

/* 字典结构定义 */
typedef struct dict { 
    dictType *type;  // 字典类型
    void *privdata;  // 私有数据
    dictht ht[2];    // 哈希表[两个](定义了两张哈希表,是为了后续字典的扩展作Rehash之用)
    long rehashidx;   // 记录rehash 进度的标志,值为-1表示rehash未进行
    int iterators;   //  当前正在迭代的迭代器数
} dict;

Redis通过dictCreate()创建词典,在初始化中,table指针为Null,所以两个哈希表ht[0].table和ht[1].table都未真正分配内存空间。只有在dictExpand()字典扩展时才给table分配指向dictEntry的内存。

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

2.将rehashindex的值设置为0,表示rehash工作正式开始

3.在rehash期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashindex索引上的所有键值对rehash到ht[1],当rehash工作完成以后,rehashindex的值+1

4.随着字典操作的不断执行,最终会在某一时间段上ht[0]的所有键值对都会被rehash到ht[1],这时将rehashindex的值设置为-1,表示rehash操作结束

rehash触发条件是什么?

1.扩容情况1:当前redis没有进行BGWRITEAOF或者BGSAVE命令,哈希表已使用节点数量/哈希表大小>=1,并且dict_can_resize=1(dict_can_resize指示字典是否启用 rehash 的标识)

2.扩容情况2:哈希表已使用的节点数量/哈希表大小>=5,无论是否在进行BGWRITEAOF或者BGSAVE命令,都会进行扩容(rehash)。

3.缩容情况1:哈希表已使用节点数量/哈希表大小< 0.1,dict就会触发缩减操作rehash。

注:已使用节点数量可理解为key数量

rehash扩容后哈希表大小为多少?

4×2^n >= ht[0].used*2的值作为字典扩展的size大小。
4是哈希表默认大小值。

static unsigned long _dictNextPower(unsigned long size) { 
    unsigned long i = DICT_HT_INITIAL_SIZE;  // 哈希表的初始值:4
 

    if (size >= LONG_MAX) return LONG_MAX; 
    /* 计算新哈希表的大小:第一个大于等于size的2的N 次方的数值 */
    while(1) { 
        if (i >= size) 
            return i; 
        i *= 2; 
    } 
}
rehash时key查询如何执行?

根据ht[0]计算key哈希值,判断是否存在,如果存在则返回;如果不存在根据ht[1]计算key哈希值,判断是否存在,如果存在则返回。

rehash时增删改如何执行?

根据ht[0]计算key哈希值,判断是否存在,如果存在则执行操作;如果不存在根据ht[1]计算key哈希值,判断是否存在,如果存在则执行操作,如果不存在则增加或者删除。

新key的操作在ht[1]执行
rehash时key的操作,图片来自美团技术团队
注:图片来自美团技术团队

  详细可以阅读:
Redis 高负载下的中断优化
美团针对Redis Rehash机制的探索和实践

6种底层数据结构

  每种数据结构特性不一样,操作时间也不一样。
在这里插入图片描述

  数据类型和底层数据结构对应关系。在这里插入图片描述

1.简单动态字符串

  Sds (Simple Dynamic String,简单动态字符串)。可以参考另外一篇文章“Redis核心技术-数据结构3-String”。

2.双向链表


/* list节点*/
typedef struct listNode {
    struct listNode *prev;        // 前向指针
    struct listNode *next;        // 后向指针
    void *value;                  // 当前节点值
} listNode;

/*链表结构*/
typedef struct list {
    listNode *head;                           // 头结点
    listNode *tail;                           // 尾节点
    void *(*dup)(void *ptr);                  // 复制函数
    void (*free)(void *ptr);                  // 释放函数
    int (*match)(void *ptr, void *key);       // 匹对函数
    unsigned long len;                        // 节点数量
} list;

3.压缩列表

  压缩列表(ziplist),Redis为了节约内存而开发的。

  ziplist包括zip header、zip entry、zip end三个模块。

1.zip entry由prevlen、encoding&length、value三部分组成。
2.prevlen主要是指前面zipEntry的长度,coding&length是指编码字段长度和实际- 存储value的长- 度,value是指真正的内容。
3.每个key/value存储结果中key用一个zipEntry存储,value用一个zipEntry存储。
在这里插入图片描述
注:图片来自“压缩列表

typedef struct ziplist{
     /*ziplist分配的内存大小*/
     uint32_t bytes;
     /*达到尾部的偏移量*/
     uint32_t tail_offset;
     /*存储元素实体个数*/
     uint16_t length;
     /*存储内容实体元素*/
     unsigned char* content[];
     /*尾部标识*/
     unsigned char end;
}ziplist;

/*元素实体所有信息, 仅仅是描述使用, 内存中并非如此存储*/
typedef struct zlentry {
     /*前一个元素长度需要空间和前一个元素长度*/
    unsigned int prevrawlensize, prevrawlen;
     /*元素长度需要空间和元素长度*/
    unsigned int lensize, len;
     /*头部长度即prevrawlensize + lensize*/
    unsigned int headersize;
     /*元素内容编码*/
    unsigned char encoding;
     /*元素实际内容*/
    unsigned char *p;
}zlentry;

4.哈希表

  Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表。

  Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?

  Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。

这两个阈值分别对应以下两个配置项:
1.hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
2.hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。

  在redis.conf中配置,默认值如下:

#元素数量小于512
hash-max-ziplist-entries 512

#字符串长度都小于64字节
hash-max-ziplist-value 64

  压缩列表转为了哈希表后,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。在节省内存空间方面,哈希表就没有压缩列表那么高效了。

5.跳表

  跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

  有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位
在这里插入图片描述

6.整数数组

  整数集合(intset)并不是一个基础的数据结构,是Redis自己设计的一种存储结构,是集合键的底层实现之一。当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。

//每个intset结构表示一个整数集合
typedef struct intset{
    //编码方式
    uint32_t encoding;
    //集合中包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;

  可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素

  将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。

参考:
https://blog.csdn.net/mccand1234/article/details/93411326
https://www.cnblogs.com/hunternet/p/11306690.html
https://www.cnblogs.com/hunternet/p/11248192.html
https://www.cnblogs.com/hunternet/p/11268067.html(Redis数据结构——整数集合)
https://mp.weixin.qq.com/s/7ct-mvSIaT3o4-tsMaKRWA
注:部分图片来自极客时间

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冲上云霄的Jayden

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值