redis数据结构

本文详细介绍了Redis中的五种数据类型及其底层实现,包括String的SDS结构、Set的IntSet、List的QuickList、Hash的Dict结构以及ZSet的SkipList。讲解了每种数据类型的特性和内存优化策略,如SDS的内存预分配、IntSet的类型升级、Dict的rehash过程以及QuickList的压缩列表和双向链表结合。
摘要由CSDN通过智能技术生成

在这里插入图片描述

1. redis数据类型

  • redis有五种基本数据类型
    • string
    • list
    • set
    • sorted set
    • hash
  • 这五种基本数据类型其实是逻辑数据类型,每种数据类型都有不止一种的实现方式
    • 逻辑数据类型,规定了它有哪些操作,有哪些特性。
    • 如set规定了它无序,不能重复
    • 如list规定了它有序,操作队首或队尾时间复杂度为O(1)
    • 所以它们都是规定了有哪些特性,至于你到底怎么实现,取决于你。
    • 如hash这个数据类型,在元素少的时候底层实现用的是zipList,元素多了用Dict实现。

2. redis数据结构介绍

2.1 SDS

  • 它是String的底层实现方式
  • SDS是一个结构体,源码如下
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* buf已保存的字符串字节数,不包含结束标示*/
    uint8_t alloc; /* buf申请的总的字节数,不包含结束标示*/
    unsigned char flags; /* 不同SDS的头类型,用来控制SDS的头大小 */
    char buf[];
};
  • len:已保存的字节数
  • alloc:分配的字节数
  • flags:用来标识SDS的头类型
  • buf:字节数组,存放真实的数据

2.1.1 SDS的内存预分配

在这里插入图片描述
二进制安全的意思就是,不光能存字符串,也可以存图片或者安装包等格式文件,因为SDS的结束标志不是根据\0,而是根据属性len

2.2 IntSet

  • IntSet是redis中Set集合的一种实现方式,基于整数数组来实现的。并且具备长度可变,有序等特征
  • 结构如下
typedef struct intset {
    uint32_t encoding; /* 编码方式,支持存放16位、32位、64位整数*/
    uint32_t length; /* 元素个数 */
    int8_t contents[]; /* 整数数组,保存集合数据*/
} intset;
  • encoding:整数的编码方式,即一个整数所占字节数,如2字节,4字节,8字节
  • length:intset元素个数
  • contents:整数数组,保存数据的集合

2.2.1 IntSet升级

  • redis中使用秉持的最优原则,假如最开始我们往IntSet存储的整数比较小,两个字节就可以存下,那么我们就用两个字节表示一个整数。
  • 但是假如我们突然向其中添加一个数字如50000,这时候两个字节无法存储下。那么IntSet会自动升级编码方式为4个字节。
    在这里插入图片描述

2.2.2 特性

  • IntSet可以看做是一个特殊的整数数组
  • 它可以根据需要改变自己的元素类型(2字节,4字节,8字节)
  • redis会确保IntSet中的元素唯一、有序(有序是为了可以利用二分查找加快查询速度)
  • 具备类型升级机制,可以节省内存空间
  • 所以当Set中存储的元素都是整数,并且个数不是很多时,它的底层实现就是用IntSet

2.3 Dict

  • Dict由三个部分组成,分别是字典(Dict)、哈希表(DictHashTable)、哈希节点(DictEntry)
typedef struct dict {
    dictType *type; // dict类型,内置不同的hash函数
    void *privdata;     // 私有数据,在做特殊hash运算时用
    dictht ht[2]; // 一个Dict包含两个哈希表,其中一个是当前数据,另一个一般是空,rehash时使用
    long rehashidx;   // rehash的进度,-1表示未进行
    int16_t pauserehash; // rehash是否暂停,1则暂停,0则继续
} dict;
  • type:dict类型,内置不同的hash函数
  • privdata:私有数据,在做特殊hash运算时使用
  • ht:哈希表数组,一个字典会包含两个哈希表,其中一个是当前数据,另一个一般为空,rehash时使用
  • rehashidx:rehash的进度,-1表示此时未进行rehash
  • pauserehash:rehash是否暂停,1则暂停,0则继续
typedef struct dictht {
    // entry数组
    // 数组中保存的是指向entry的指针
    dictEntry **table; 
    // 哈希表大小
    unsigned long size;     
    // 哈希表大小的掩码,总等于size - 1
    unsigned long sizemask;     
    // entry个数
    unsigned long used; 
} dictht;
  • table:哈希表中的哈希数组
  • size:哈希表中的数组大小
  • sizemask:哈希表数组大小的掩码
  • used:已经存放的dictEntry个数
typedef struct dictEntry {
    void *key; // 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; // 值
    // 下一个Entry的指针
    struct dictEntry *next; 
} dictEntry;
  • key:键,其实肯定是字符串类型
  • v:值,它是一个联合体,可以存任意类型
  • next:下一个entry的指针
    在这里插入图片描述

2.3.1 字典的rehash

  • 计算新hash表的realSize,该值决定了当前要做的是扩容还是收缩
    • 如果是扩容,则新size为第一个大于等于dict.ht[0].used+1的2的n次方
    • 如果是收缩,则新size为第一个大于等于dict.ht[0].used的2的n次方(不得小于4)
  • 按照新的realSize申请内存空间,创建dictht,并赋值给dict.ht[1]
  • 设置dict.rehashidx=0,标识开始rehash(0标识将要开始进行转移的数组下标)
  • 之后每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidex是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]
  • 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存
  • 将rehashidx赋值为-1,代表rehash结束
  • 在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空

2.4 ZipList

  • ZipList是一种特殊的“双端列表”,由一系列特殊编码的连续内存块组成。可以在任意一段进行压入/弹出操作,并且该操作的时间复杂度为O(1)
    在这里插入图片描述
    在这里插入图片描述
  • 每个ZipListEntry都表示集合中一个单个元素
  • ZipList中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后。

2.4.1 ZipListEntry中的encoding编码

在这里插入图片描述
在这里插入图片描述

2.4.2 ZipList的连锁更新问题

在这里插入图片描述

2.4.3 zipList特性

  • 压缩列表可以看做一种连续内存空间的“双向链表”
  • 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
  • 如果列表数据过多,导致链表过长,可能影响查询性能
  • 增加或删改较大数据时有可能发生连续更新问题(但是概率很低)

2.5 QuickList

  • ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占用过多,申请内存效率就会很低
  • 所以一般ZipList存储的元素长度和每个元素大小一般不会很大
  • QuickList其实是LinkedList和ZipList的结合。它是一个双端链表,只不过这个链表中的每一个节点都是一个ZipList
typedef struct quicklist {
    // 头节点指针
    quicklistNode *head; 
    // 尾节点指针
    quicklistNode *tail; 
    // 所有ziplist的entry的数量
    unsigned long count;    
    // ziplists总数量
    unsigned long len;
    // ziplist的entry上限,默认值 -2 
    int fill : QL_FILL_BITS;         // 首尾不压缩的节点数量
    unsigned int compress : QL_COMP_BITS;
    // 内存重分配时的书签数量及数组,一般用不到
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;
  • head:头节点指针
  • tail:尾节点指针
  • count:存储的总元素个数(所有ziplist的entry之和)
  • len:每个ziplist的的限制
    • 值为正,则表示每个ziplist允许的entry个数的最大值
    • 值为负,则代表了这个ziplist的最大内存大小,如-2代表不能超过8kb (对应关系下去自己去查)
  • fill:首位不压缩的节点数量 (为0表示不压缩)
typedef struct quicklistNode {
    // 前一个节点指针
    struct quicklistNode *prev;
    // 下一个节点指针
    struct quicklistNode *next;
    // 当前节点的ZipList指针
    unsigned char *zl;
    // 当前节点的ZipList的字节大小
    unsigned int sz;
    // 当前节点的ZipList的entry个数
    unsigned int count : 16;  
    // 编码方式:1,ZipList; 2,lzf压缩模式
    unsigned int encoding : 2;
    // 数据容器类型(预留):1,其它;2,ZipList
    unsigned int container : 2;
    // 是否被解压缩。1:则说明被解压了,将来要重新压缩
    unsigned int recompress : 1;
    unsigned int attempted_compress : 1; //测试用
    unsigned int extra : 10; /*预留字段*/
} quicklistNode;
  • prev:前一个节点的指针
  • next:后一个节点指针
  • zl:ziplist指针
  • sz:当前节点的ziplist的字节大小
  • count:当前zipList的entry个数
  • encoding:编码方式 1表示ziplist方式,2表示压缩的ziplist模式
  • container:目前数据的容器类型,2表示ziplist(其他的都是预留,方便扩展)
    在这里插入图片描述

2.5.1 特性

  • QuickList是一个节点为zipList的双端链表
  • 节点采用zipList,解决了传统链表的内存占用问题
  • 控制了zipList大小,解决连续内存空间申请效率问题
  • 中间节点可以压缩,进一步节省了内存

2.6 SkipList

在这里插入图片描述

  • 跳表首先是链表,但是与传统链表有些差异
    • 元素按照升序排列存储
    • 节点可能包含多个指针,指针跨度不同
// t_zset.c
typedef struct zskiplist {
    // 头尾节点指针
    struct zskiplistNode *header, *tail;
    // 节点数量
    unsigned long length;
    // 最大的索引层级,默认是1
    int level;
} zskiplist;
  • header,tail: 头尾节点指针
  • length:节点数量
  • level:最大索引层级。
// t_zset.c
typedef struct zskiplistNode {
    sds ele; // 节点存储的值
    double score;// 节点分数,排序、查找用
    struct zskiplistNode *backward; // 前一个节点指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 下一个节点指针
        unsigned long span; // 索引跨度
    } level[]; // 多级索引数组
} zskiplistNode;
  • ele:节点中存储的值
  • score:节点分数
  • backward:前一个节点指针
  • level:多级索引数组
    在这里插入图片描述

2.6.1 特性

  • 跳跃表是一个双向链表,每个节点都包含score和ele值
  • 节点按照score值排序,socre值一样则按照ele字典排序
  • 每个节点都可以包含多层指针,层数在1~32之间的随机数
  • 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
  • 增删改查效率与红黑树一致,实现却更简单

3. RedisObject

  • 前面咱们说过了,redis中有五种基本数据类型。其中我们key一直都是String类型
  • 其实,在redis中五种数据类型都会被封装为一个RedisObject,也叫作redis对象
    在这里插入图片描述
  • type:对象类型,五种基本数据类型中的一种
  • encoding:底层编码方式,包含了11种
    • 0:raw编码动态字符串
    • 1:long类型的整数字符串
    • 2:dict
    • 4:linkedList
    • 5:zipList
    • 6:intset
    • 7:skipList
    • 8:embstr的动态字符串
    • 9:quickList
    • 10:stream流

3.1 五种数据类型的编码方式

3.1.1 String

  • 有三种编码方式:int,embstr,raw
  • 其基本编码方式是raw,是基于SDS实现,存储上限是512mb
  • 如果存储的SDS长度小于44字节,则会采用embstr编码,此时Object head与SDS是一段连续的空间。申请内存时只需要调用一次内存分配函数,效率更高。
  • 如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码,直接将数据保存在redisObject的ptr指针位置(刚好8字节),这样就不在需要SDS了。
    在这里插入图片描述

3.1.2 List

  • 3.2版本前,通过ziplist和linkedlist来实现List。
    • 当元素数量小于512并且元素大小小于64字节使用ziplist编码
    • 超过则采用linkedlist编码
  • 3.2版本之后,统一使用Quicklist来实现List
    在这里插入图片描述

3.1.3 Set

  • Set要具有以下特点
    • 不保证有序性
    • 保证元素唯一
    • 方便求并集,交集,差集
  • 为了方便查询效率和唯一性,set采用HT编码(Dict)。Dict中key用来存储元素,value统一为NULL
  • 当存储的所有数据都是整数,并且元素数量没超过set-max-intset-entries时(默认512),Set会采用IntSet编码,以节省内存。
    在这里插入图片描述

3.1.4 ZSet

  • ZSet有以下特点
    • 每个元素都需要指定一个score值和member值
    • 需要根据score值排序
    • member必须唯一
    • 能快速根据member查询score
// zset结构
typedef struct zset {
    // Dict指针
    dict *dict;
    // SkipList指针
    zskiplist *zsl;
} zset;
  • dict:包含一个字典
  • zsl:包含一个跳表
    在这里插入图片描述
  • 当元素数量不多时,dict和SkipList的优势不明显,而且更加消耗内存。因此zset会采用zipList结构来节省内存,不过需要同时满足两个条件
    • 元素数量小于zset_max_ziplist_entries,默认128
    • 每个元素都小于zset_max_ziplist_value字节,默认64
  • ziplist本身没有排序功能,而且没有键值对的概念,因此需要zset通过编码实现:
    • ziplist是连续内存,因此score和element是紧挨一起的两个entry,element在前,score在后
    • score越小越接近队首,score越大越接近队尾,按照score值升序排列
      在这里插入图片描述

3.1.5 Hash

  • Hash需要有以下特点
    • 都是键值存储
    • 可以根据键快速获取值
    • 键必须唯一
  • Hash结构默认采用ziplist编码,用以节省内存。ziplist中相邻的两个entry分别保存field和value
  • 当数据量较大时,Hash结构会转为HT编码(dict),其出发条件有两个
    • ziplist中元素数量超过了hash-max-ziplist-entries(默认512)
    • ziplistz中的任意entry大小超过了hash-max-ziplist-value(默认64字节)
      在这里插入图片描述
      在这里插入图片描述

根据b站视频总结https://www.bilibili.com/video/BV1cr4y1671t

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值