Redis数据结构

Redis数据类型

Redis快除了因为其使用内存的一个原因之外,另一个原因就是Redis实现了基于CRUD的高效的数据结构。

Redis“基础”数据类型

Redis基础的数据类型也就是那5个常用的,String,List,Hash,Set,ZSet

String String是最基础的数据类型之一,一个key对应一个value。同时,String类型是二进制安全的,也就是说他可以包含任何的数据。比如,数字,字符串,图片,序列化对象等。

List Redis中的List是一个链表。使用List可以实现一些简单的消息队列的功能,复杂的消息队列还是建议用MQ去做。

Hash Hash是一个String类型的filed和value的映射表,一般比较适合用于存储对象

Set set是String类型的无序集合,和java一样set是没有重复元素的。

ZSet zset是一个有序集合,他跟无序集合一样都是String类型的集合,且不允许有重复的元素。有序集合是在每个元素上面关联了一个double类型的score,通过score对集合中的元素进行排序,score可以重复。

Redis“特殊”数据类型

Redis中有几种特殊的数据类型,分别是HyperLogLogs,Bitmap,Geospatial

HyperLogLogs 基数统计。HyperLogLogs也可以理解为是一种算法,适用于多个集合中取不重复元素的场景。比如,统计网站的在线用户数量。

Bitmap 位图。位图主要是操作二进制的方式进行记录,也就是他只有0和1两种状态。一般用于记录只有两种状态的场景,比如打卡,未打卡。布隆过滤器也是通过位图来实现的。

Geospatial 地理位置。他底层是由ZSet实现的,一般可以存放一些地理位置信息,比如附近的人,打车距离,朋友定位等与定位有关的场景。

Redis数据结构

Redis数据类型底层的数据结构随版本的更新一直变化,以5.0以上版本为例。
redis数据结构

String 数据结构(SDS)

SDS结构设计
sds数据结构
SDS数据结构中的成员,分别代表着:
len: 记录了字符串的长度,这样获取字符串的长度时,不需要遍历整个字符串,时间复杂度为O(1)
alloc: 分配给字符数组的长度。在修改字符串时可以直接通过alloc-len的方式计算是否需要扩容
flags: 表示不同类型的sds。主要包括sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64,5中类型。不同的数字代表着不同字节的对齐方式,比如sdshdr8就是按照8个字节的对其方式
buf[]: 字符数组,保存数据的。字符数组不仅可以保存字符串,还可以保存其他的二进制数据。

SDS的优势

获取字符串长度,时间复杂度为O(1)
Redis的sds结构上面维护了一个len变量,获取字符串长度只需要返回这个成员变量的值就可以。
而C语言中是通过strlen()函数去获取的字符串长度,这个函数需要去遍历整个字符串去统计,时间复杂度是O(n)。
不会发生缓冲区溢出问题
Redis的sds结构维护了一个alloc变量,通过 alloc-len 的方式可以计算出剩余可用的空间大小,这样修改的时候,可以从程序内部判断缓冲区是否可用。
C语言中大多数的时候是通过strcat函数追加的,是否满足需求的工作是由程序员去判断的,程序内部不会去判断是否溢出,当发生异常时会直接结束程序。

sds扩容
小于1MB翻倍扩容,大于1MB默认以1MB扩容

二进制安全
SDS通过维护的len变量记录字符串长度,所以可以存储包含’\0’的数据。为了兼容一些C语言的标准库函数,SDS字符串结尾还是会加上’\0’字符。在存储数据的时候sds会把数据直接存储在buf[]数组上,不会做其他的限制。
节省内存空间
SDS结构中维护了一个flags变量,他的值可以指定sds类型,sdshdr16 类型的len和alloc的数据类型都是unit16_t,表示字符数组的长度和分配的空间不超过2的16次方次。

使用不同的类型的结构,可以更灵活的保存不同大小的字符串,可以有效的节省内存空间。

List 链表

链表节点listNode

typedef struct listNode {
    //前驱    
    struct listNode *prev;
    //后继    
    struct listNode *next;    
    //值    
    void *value;
    } listNode;

从上面可以结构可以看出,List链表是一个双向链表,类似于下图所示结构
双向链表
上面只是链表的一个节点,Redis实际上是在节点的基础上有封装了一个list结构,这个结构维护了一个头指针head,尾指针tail,链表长度len,以及一些可以自定义实现的函数dup、free、match。大致结构如下图所示
list结构

压缩链表

压缩链表使用的是一块连续的内存空间,这样可以利用CPU缓存,并且可以节省内存的开销。

压缩链表数据结构
压缩链表

压缩链表维护了4个变量
zlbytes: 记录链表占用内存的字节数
zltail: 记录链表的尾部节点距离起始地址字节数
zllen: 记录链表的节点个数
zlend: 标记压缩链表的结束点
每个节点又包含了三部分内容
prevlen: 记录前一个节点的长度
encoding: 记录当前节点的数据类型和长度
data: 记录当前节点的实际数据

Redis中,根据数据大小和类型进行不同的空间大小分配的思想,随处可见,这也是Redis节省内存的精髓所在

这里需要注意的是,如果前一个节点的长度小于254个字节,那么prevlen属性需要用 1个字节的空间来保存长度值;如果前一个节点的长度大于254个字节,那么prevlen属性需要用5个字节的空间来保存长度值。
encoding 属性的空间大小跟数据是字符串还是整数有关,如果当前节点的数据是整数,那encoding会使用1个字节的存储空间;如果当前节点的数据是字符串,encoding会根据字符串的长度使用1字节/2字节/5字节的存储空间。

压缩链表还会存在连锁更新的问题
之后补充

Hash

Hash对象的底层实现之一是listpack,另一个底层实现是哈希表。

哈希表的好处就是查询的时间复杂度是O(1),但随着数据的不断增加,那哈希冲突的可能性就大大的增加。
Redis是通过 链式哈希 的方式解决哈希冲突的。在不扩容的前提下,通过将相同哈希值的数据形成链表。

哈希表的结构

  typedef struct dictht{
        dictEntry **table;  //哈希表数组
        unsigned long size;
        unsigned long sizemask;  //哈希表大小掩码,计算索引值
        unsigned long used; //该哈希表已有的元素数量
    } dictht;

从上面的结构可以看出,哈希表是一个数组,数组的每个元素都是一个指向(哈希节点)的指针

哈希表节点的结构

 typedef struct dictEntry {
        void *key;
        union {   //值
            void *value;
            unit64_t u64;
            unit64_t s64;
            double d;
        } v;
        struct dictEntry *next;
    } dictEntry;

哈希表节点中的值是一个 联合体 定义的,所以,键值对中的值,可以是一个指向实际值的指针,也可以是一个无符号的64位整数或是有符号的64位整数或者是double类型的值。 这样做的目的是为了节省内存空间。

哈希表结构图

哈希表结构

哈希冲突

上面的结构也可以看出来,哈希表实际上是一个数组,数组里每个元素就是一个哈希桶。当key经过hash计算之后得到哈希值,并经过取模的方法计算之后,得到的结果就是对应的元素位置,也就是在第几个哈希桶内。

哈希冲突是指当有两个以上的key经过计算之后得到的值在同一个哈希桶上时,这时发生了哈希冲突。

Redis是采用链式哈希的方法来解决哈希冲突的

链式哈希
链式哈希就是发生哈希冲突的几个节点,通过指针指向下一个节点的方式构成一个单向链表。

哈希表扩容 (rehash)

Redis定义了一个dict结构体

typedef struct dict {
  dictht ht[2];
  ...
} dict;

dict结构图
dict结构
在正常的情况下,插入数据的时候,会首先插入到哈希表1中,此时的哈希表2还没有被分配空间。随着数据的不断增加,就会触发rehash操作。会先给哈希表2分配内存空间,默认是原来的(哈希表1)的两倍;将哈希表1的数据迁移到哈希表2中;完成之后会释放掉哈希表1的空间,将哈希表2设置为哈希表1,同时新建一个哈希表。

这样就会存在一个问题,就是在rehash过程中,会涉及到大量的数据拷贝,就会对Redis造成阻塞,无法响应其他的请求

渐进式rehash
为了解决上面的问题,Redis采用了渐进式rehash的方式,去避免一次性的大量数据拷贝的情况。

渐进式rehash就是把一次迁移的工作,分为多次的去执行。这时,会同时存在两个哈希表,查找元素的时候,会先去第一个表查找,第一个没找到的时候,会去第二个表查找。在这个过程中,如果新增元素,会优先插入到第二个哈希表中。

触发rehash的条件

1.当负载因子大于等于 1 ,并且 redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作
2.当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行RDB 快照或 AOF重写,都会强制进行rehash 操作

负载因子 = 哈希表已保存的节点数量/哈希表大小

Set

整数集合

整数结合结构定义

typedef struct intset {
	uint32_t encoding;
	uint32_t length;
	int8_t contents[];
} intset;

整数集合是通过contents数组保存数据的,他实际的类型跟encoding属性的类型是一致的。

如果encoding属性值为INSET_ENC_INT16,那么contents就是一个int16_t类型的数组;如果encoding属性值为INSET_ENC_INT32,那么contents就是一个int32_t类型的数组;如果encoding属性值为INSET_ENC_INT64,那么contents就是一个int64_t类型的数组。
集合升级
上面说到contents的类型是跟encoding的值保持一致的,那就存在一个,当新插入的元素类型比之前的元素类型长时,就需要对整个数组的元素进行升级。

整数数组是在原本空间的大小之上通过扩容空间的方法,并把数组中之前的数据类型都转换为新的数据类型。

ZSet

跳表

跳表的定义

typedef struct zset {
	dict *dict;
	zskiplist *zsl;
} zset;

从上面的结构可以看出,跳表是由两个数据结构组成的,一个是跳表,一个是哈希表。

跳表的数据结构
跳表
从上面的结构可以看出,跳表是实现了一种多层的有序链表,这样的可以快速的对数据进行定位。

总结

Redis设计理念就是为了节约内存空间,在所有的数据类型中,都有或多或少的体现。还有quicklist和listpack这两个数据结构没有介绍,等以后有时间再做补充吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值