redis数据结构

0 基本数据类型应用场景

0.1 string

1.计数器
2.记录用户的token

0.2 list

1.消息队列
2.文章列表

0.3 set

1.标签。例如博客网站中使用的标签:mysql,java,计网等。发布的文章可以指定属于某个标签
2.朋友圈点赞,用户的好友关注,两用户的共同关注

0.4 zset

1.排行榜:按照时间,播放量,点赞数对视频,帖子等排名
2.做带权重的队列。普通消息score=1,重要消息score=2。工作线程可以选择按score的倒序获取工作任务。
3.商品评价标签,(好,中,差)

0.5 hash

购物车,对象缓存

注意:本文中redis版本为2.9

1. redis的存储方式

redis是key-value存储系统,key类型一般为字符串,value类型为RedisObject对象
redis是键值对存储的方式,每存储一条数据,redis至少产生2个对象,一个是redisObject,另一个是具体存储的数据
redisObject,用来描述具体数据的类型,比如用的是哪种数据类型,底层用了哪种数据结构

2. redis为什么要使用2个对象,好处

1.redis在执行命令的时候,可以通过redisObject的类型和编码确定是否可以执行相应的命令,不用等操作具体的数据时才发现不行
2.可以基于redisObject中的refcount 引用计数进行内存回收机制,自动释放对象所占用的内存
3.redis可以根据lru 记录最后一次访问时间,针对时间较长对象的进行删除
4.针对不同的场景,可以为对象设置不同的数据结构,从而优化了对象在不同场景下的使用效率
5.可以让多个数据库来共享一个对象来节省空间

3. redisObject对象解析

typedef struct redisObject{
    //当前值对象的数据类型
    unsigned type:4;
 
    //当前值对象的底层编码类型
    unsigned encoding:4;
 
    //对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS
 
    //引用计数
    int refcount
 
    //指向底层实现数据结构的指针
    void *ptr;
}

解释上文中的字段

  • type
value对象的数据类型:(string,hash,list,set,zset)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TN2H7lVy-1662517314341)(D:\redis面试知识\photo\Snipaste_2022-08-31_16-15-36.png)]

  • encoding
当前值对象底层编码的实现方式。不同type对象对应不同的编码。每种对象至少对应了两种编码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xq3e3j2S-1662517314342)(D:\redis面试知识\photo\Snipaste_2022-08-31_16-16-30.png)]

每种编码对应一种底层数据结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yipmmLJ6-1662517314343)(D:\redis面试知识\photo\Snipaste_2022-08-31_16-17-49.png)]

  • lru
记录此对象最后一次访问的时间。当redis内存回收算法设置为volatile-lru或allkeys-lru时,redis会优先释放最久没有被访问的数据
  • refcount
用于共享计数,当refcount=0时,表示没有其他对象引用,可以释放此对象
  • ptr
指向对象的底层实现数据结构

4. String类型

redis设计了可变的字符串长度对象,叫SDS(simple dynamic string)

简单动态字符串结构体

struct sdshdr{
    //记录buf数组中已存的字节数量,也就是sds保存的字符串长度
    int len;
 
    // buf数组中剩余可用的字节数量
    int free;
 
    //字节数组,用来存储字符串的
    char buf[];
}

这样设计的优点:

1.杜绝缓冲区溢出。SDS在合并字符串时,sds api会先检查空间是否满足需求,如果满足,直接执行修改操作;如果不满足,将空间修改至满足需求的大小,然后再执行修改操作。在C语言中进行两个字符串拼接时可以使用strcat函数,如果没有足够的内存空间,会造成缓冲区溢出
2.空间预分配
	2.1如果修改后sds的len小于1MB,程序会给free分配与len相同的空间。即若len=600k,则free=600k
	2.2惰性空间释放,当缩短sds的存储内容时,不会立即使用内存重分配来回收字符串缩短后的空间,通过free接收len中多余的空间。真正需要释放内存的时候,通过调用api来释放内存
	2.3redis有效的减少了执行字符串增长所需要的内存分配次数,C语言字符串不记录字符串长度,如果要修改字符串需要重新分配内存
	2.4如果修改后len大于1MB,则给free分配1MB空间
3.结构体中定义了len,获取字符串长度的时间复杂度O(1)。C语言中是O(n)
4.每个字符串都是以空字符串结尾,重用了C语言库中<string.h>中一部分函数
5.二进制安全。C语言中字符串以空字符串作为字符串结束的标识,对于一些二进制文件(如图片),内容可能包括空字符串,会导致C语言中字符串无法正确存取。SDS中的API都是以二进制的方式处理buf中的元素,并且SDS不是以空字符串来判断是否结束,而是以len表示的长度判断字符串是否结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3pPRx80-1662517314343)(D:\redis面试知识\photo\66a5953275c342bc8f82a6a2cb5b0b28.png)]

4.1 int整数值实现

如果一个字符串对象存储的数据是整数,且这个整数值可以用long类型表示时,字符串对象会将整数值 保存在字符串对象结构的ptr属性中(将void * 转换成long)。并将字符串对象的编码设置为int。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJTCsder-1662517314343)(D:\redis面试知识\photo\3614eb65fccb45899a53d9458b5d517f.png)]

4.2 embstr

当存储的数据是字符串时,且字节数<=32,使用embstr编码(3.2版本之前字节数<=39,3.2版本之后字节数<=44)

解析

优点:
    1.相比于raw编码调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,embstr编码只调用一次内存分配函数,其分配了一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。
    2.释放embstr编码的字符串对象 只需调用一次内存释放函数
    3.所有数据存放在一块连续的内存中,可以更好的利用缓存带来的优势
缺点:
	1.当字符串增加时,其长度会增加,需要重新分配内存,导致redisObject和sdshdr都需要重新分配空间,影响性能。所以,用embstr实现string时,只允许读。如果要修改数据,编码格式转为raw。
	2.由于是连续的空间,只适合长度较小的字符串

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ir8sKtRw-1662517314344)(D:\redis面试知识\photo\e1690e7b1df442789a39d0293953d557.png)]

4.3 raw

当存储的数据是字符串时,且字节数>32,使用embstr编码(3.2版本之前字节数>39,3.2版本之后字节数>44)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-evR8TyEc-1662517314344)(D:\redis面试知识\photo\210b7cf6f24740668c7a16a152f8c669.png)]

4.4 关于为什么<=44字节用emstr编码(32,39,44随版本变化)

此处以redis版本3.2之后解释

redisObject结构

/*
 * Redis 对象
 */
typedef struct redisObject {
    // 类型 4bits
    unsigned type:4;
    // 编码方式 4bits
    unsigned encoding:4;
    // LRU 时间(相对于 server.lruclock) 24bits
    unsigned lru:22;
    // 引用计数 Redis里面的数据可以通过引用计数进行共享 32bits
    int refcount;
    // 指向对象的值 64-bit
    void *ptr;
} robj;
redisObject占用空间
1byte = 8bits
4+4+24+32+64 = 128bits = 16字节

sdshdr8占用空间
1(uint8_t)+1(uint8_t)+1(unsigned char)+1(buf[]中结尾的'\0'字符) = 4字节
初始最小分配为64字节 所以只分配一次空间的embstr最大为64 - 16 - 4 = 44字节

5. List类型

5.0 各版本底层数据结构的变化

Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist

当list存储的数据量比较少且同时满足一下条件时,list底层使用ziplist数据结构存储数据

  • list中保存的每个元素的长度小于64字节
  • list中的数据个数少于512个

Redis3.2及之后的底层实现方式(redis6.4之后取消):quicklist

quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和压缩链表的优点

Redis5.0引入listpack

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;

5.1 ziplizt压缩链表

ziplist由多个部分组成

  • zlbytes
记录整个压缩列表占用的字节数
用于压缩列表内存重分配或计算zleng位置
  • zltail
记录最后一个节点离列表起始位置有多少个字节。
通过这个偏移量,不用遍历整个列表就能知道 尾节点的位置
  • zllen
记录列表中的节点数,当值小于unint16_max(65535)时,该值就是列表中的节点数。如果该值等于65535,需要遍历整个列表
  • entry()

    列表中的各个节点,长度由各个节点决定
    

    entry由一下几个部分组成

    • previous_entry_length:保存前一个节点的长度。如果前一个字节长度小于254字节,该属性长度为1字节,前一字节的长度就保存在这一个字节中。如果大于等于254字节,该属性长度为5字节。其中第一个字节为0xFE(十进制值254),后四个字节用于保存前一字节的长度。
    • encoding:记录content属性所保存的数据的数据类型及长度
    • content:保存实际的数据
  • zlend

特殊值,用于标记列表的末端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PULsjMeK-1662517314344)(D:\redis面试知识\photo\21a85a2211df4a98a7a3740b1b11f2f6.png)]

优点:

存储空间连续,更能节省内存空间

缺点:

1.极端情况下会出现连锁更新现象。
entry的previous_entry_length属性,记录上一个节点的长度。如果每个节点的长度都在250-253之间。此时在头部插入一个新的节点,且该节点长度大于254字节。则之后的每个节点的previous_entry_length属性都会从1字节变成5字节。导致整个列表的节点都需要更新。
2.只能存储小数据的值,且存储的数据量小
ziplist是一段连续的内存,插入,删除的时间复杂度为O(n)。每次插入或删除一个元素,都需要频繁调用realloc()函数进行内存的扩展或减少,然后进行数据搬移

5.2 linkedlist双向链表

使用list存储数据时,如果存储数据较大,ziplist无法满足,则使用linkedlist

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hv5iQLtI-1662517314345)(D:\redis面试知识\photo\05445fd09c48434a8c7f357e3b30459b.png)]

优缺点:

优点:
插入,删除,修改节点的效率高
缺点:
需要保存前后节点,会占用内存
节点分散寻址麻烦

5.3 quicklist快速链表

quicklist是一个双向链表,链表中每个元素是一个ziplist

typedef struct quicklist {
    // 指向quicklist的头部
    quicklistNode *head;
    // 指向quicklist的尾部
    quicklistNode *tail;
    //所有ziplist中节点的个数
    unsigned long count;
    //quicklistNode的个数
    unsigned int len;
    // ziplist大小限定,由redis.conf文件的list-max-ziplist-size给定
    int fill : 16;
    // 节点压缩深度设置,由redis.conf文件的llist-compress-depth给定
    unsigned int compress : 16;
} quicklist;

typedef struct quicklistNode {
    // 指向上一个ziplist节点
    struct quicklistNode *prev;
    // 指向下一个ziplist节点
    struct quicklistNode *next;
    // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
    unsigned char *zl;
    // 表示指向ziplist结构的总长度(内存占用长度)
    unsigned int sz;
    // ziplist数量
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    // 预留字段,存放数据的方式,1--NONE,2--ziplist
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    // 扩展字段
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

typedef struct quicklistLZF {
    // LZF压缩后占用的字节数
    unsigned int sz; /* LZF size in bytes*/
    // 柔性数组,存放压缩后的ziplist字节数组
    char compressed[];
} quicklistLZF;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CnzrX5md-1662517314345)(D:\redis面试知识\photo\20170418155917948.png)]

作用:

减少了数据插入时内存空间的重新分配,以及内存数据的拷贝。同时,quicklist限制了每个节点上ziplist的大小,如果一个ziplist过大,就会新增quicklist节点。但由于quicklist中使用quicklistNode结构指向每个ziplist,增加了内存开销。

5.4 listpack

redis5.0之后 产生了listpack。为了减少内存开销和避免ziplist的连锁更新问题

struct listpack<T>{
    int32 total_bytes; //总字节数
    int16 size;        //元素个数
    T[] entries;       //紧促排列的元素列表
    int8 end;          //与zlend一样,恒为0xFF
}
struct lpentry{
    int<var> encoding; //当前元素的编码类型
    optional byte[] content; //元素数据
    int<var> length; //编码类型和元素数据这两部分的长度
}

listpack的节点中不再存储前一个节点的长度,避免了连锁更新。listpack支持正、反向查询列表的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NurZYLNE-1662517314345)(D:\redis面试知识\photo\40a2b7d4a38d2ac3203a43c8197a9eb2.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t79pyBwt-1662517314345)(D:\redis面试知识\photo\9505524ce496d27eb601b2b4c227a40b.png)]

6. Hash类型

当hash中数据项较少且数据长度较小时,使用ziplist。否则使用dict(字典)

例子

hash可以理解为key代表一条记录,val中包含的(key-val)代表这条记录的每个字段及其值

1001(key) -> val
key为编号
val 为多个键值对 
	name:"tom"
	age:10
	height:180
	weight:75
	country:china

6.1 哈希表dictht

redis的字典使用哈希表作为底层实现,一个哈希表里面可以由多个哈希表节点。每个哈希表节点保存了字典中的一个键值对

typedef struct dictht {
    //哈希表节点数组
    dictEntry ••table;
    //数组长度
    unsigned long size;
    //哈希表大小掩码,用于计算索引值
    //总是等于size一1
    unsigned long sizemask;
    //该哈希表巳有节点的数量
    unsigned long used;
} dictht;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-meABSsWq-1662517314346)(D:\redis面试知识\photo\Snipaste_2022-09-01_15-43-54.png)]

6.2 哈希表节点dicthtEntry

typedf struct dictEntry{

    // 键
    void *key;
    
    // 值,可以是一个指针,也可以是不同类型的整数,浮点数
    union{
        void val;
        unit64_t u64;
        int64_t s64;
        double d;
    }v;
    
    // 指向下一个节点的指针。将多个哈希值相同的键值对连接起来,解决哈希冲突
    struct dictEntry *next;
    
}dictEntry;

k1和k0计算的哈希值相同,使用dictEntry中的next连接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T10SjdSC-1662517314346)(D:\redis面试知识\photo\Snipaste_2022-09-01_15-50-03.png)]

6.3 字典dict

typedf struct dict{
    
    // 类型特定函数,包括一些自定义函数
    // 这些函数使得key和value能够存储
    dictType *type;
    
    // 私有数据
    void *private;
    
    // 两张hash表 
    dictht ht[2];
    
    // rehash索引,字典没有进行rehash时,此值为-1
    int rehashidx;
    
    // 正在迭代的迭代器数量
    unsigned long iterators; 
    
}dict;

type和privdata是针对不同类型的键值对,为创建多态字典而设置的。

ht两个哈希表,一般只使用ht[0]哈希表,当ht[0]进行rehash时使用ht[1]。

rehashidx记录reshah的进度,如果此时没有进行rehash,值为-1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-elhhNIG4-1662517314346)(D:\redis面试知识\photo\Snipaste_2022-09-01_16-01-51.png)]

6.4 hash表的扩展和收缩

Redis中,三条关于扩容和缩容的规则:

  • 没有执行BGSAVEBGREWRITEAOF指令的情况下负载因子大于等于1时进行扩容
  • 正在执行BGSAVEBGREWRITEAOF指令的情况下哈希表的负载因大于等于5时进行扩容
  • 负载因子小于0.1时Redis自动开始对哈希表进行缩容操作;

dict中 负载因子 = 哈希表中已存在节点数/哈希表大小

扩容后哈希表节点数组 长度 *2

缩容后哈希表节点数组 长度与之前一致,缩容的过程是将数组中元素位置重排,变得紧凑

6.5 渐进式rehash

过程

1.为ht[1]分配空间,让字典同时拥有两个哈希表
2.将索引计数器rehashidx置为0 表示开始进行rehash
3.在rehash进行期间,每次对字典进行CRUD时,将ht[0]中下标为rehashidx的元素rehash到ht[1]中。rehash完成后,rehashidx+1
4.当ht[0]中所有元素都rehash到ht[1]中后,将rehashidx置为-1
5.将ht[0]释放,将ht[1]设置成ht[0],为ht[1]分配一个空白哈希表

进行rehash时,只能对ht[0]元素执行查询和删除操作;可以对ht[1]元素执行查询,删除,插入,这样将rehash的操作分摊到每次CRUD上,避免集中式rehash。

7. Set类型

实现set的底层数据结构:dict和intset

当满足以下条件时,使用intset

  • 存储的数据都是整数
  • 存储的数据元素个数<512

7.1 intset整数集合

typedef struct intset {
    // 编码类型 int16_t、int32_t、int64_t
    uint32_t encoding;  
 
    // 长度 最大长度:2^32
    uint32_t length;  
 
    // 用来存储数据的柔性数组  
    int8_t contents[];  
} intset;

contents数组中没有重复元素,元素在数组中按从小到大排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3K20JHq-1662517314346)(D:\redis面试知识\photo\Snipaste_2022-09-01_17-06-42.png)]

注意:

当插入数据时,如果插入的元素类型 大于intset的encoding的值(例如插入元素为int32_t,而encoding值为int16_t)。为了防止溢出,会对intset进行升级操作。将数组中原有元素变成新插入元素的类型

升级整数集合并添加新元素分为三个步骤:

1.根据新元素类型,扩展intset的contents数组的空间大小,并为新元素分配空间
2.将contents中元素转成插入元素的类型,并将转换后的元素放到正确的位置上,元素有序性不变
3.将新元素加入contents中

intset优缺点:

优点:
1.通过升级数据类型,可以将多种数据类型元素插入数组
2.intset能同时保存三种不同类型的值,又可以确保升级操作只会在有需要的时候进行,节省内存
缺点:
1.不支持降级
2.添加和删除均需要realloc(动态内存调整)操作

8. ZSet类型

实现ZSet的底层数据结构:ziplist和skiplist(跳表)

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值