redis 底层实现

一、String
String 是 redis 最基本的数据类型,一个 key 对应一个 value,主要应用场景就是单值缓存,分布式锁、计数器、共享session、分布式系统全局序列号等。当字符串是整数值且可以用long来表示时,会直接作为一个数值来存储,当保持的字符串时,会使用一个简单动态字符串(SDS)来保保存,我们也主要通过了解SDS的实现来理解redis对String的支持
 

 1.1、SDS
SDS(simple dynamic string)简单动态字符串,redis对字符串的抽象实现,用于在redis中保存可以被修改的字符串,我们在使用reids时,包含字符串的键值对在底层都是由SDS来进行存储,SDS最大可存储的字符串大小为512M,因为在对字符串数据进行操作时,API会对字符串的size进行校验,校验大小为512×1024×1024,此处代码为硬代码,无法通过配置修改

struct sdshdr{
    //保存的字符串实际长度,不是buf的长度,因为buf会存在预分配的情况
    int len;
    //buf中可用的字节数量
    int free;
    //字节数组,用来保存字符串,len+free等于它的大小
    char buf[];
}

1.3、SDS设计优点
通过SDS的定义可用看出,和直接使用c的字符相比,SDS具有如下特性,而redis对外支持的String类型也会继承其特点:

1) 常数复杂度获取字符串长度:因为SDS定义了len字段来记录自身长度,而C字符串的长度需要去遍历整个字符串,复杂度为O(n),这里的设计就提现出来redis在细节之处对性能的追求

2) 杜绝缓存区溢出:C字符串不记录自身长度,如果在执行字符串拼接操作时,缓存区没有足够的内存,一旦忘记分配新的内存,就会发生缓存区溢出,可能对它相邻的数据造成覆盖,而对SDS进行修改时,SDS可以通过保存的长度和空间信息进行空间检验,如果内存不足,会进行扩容操作

3) 减少内存重分配次数:对于C字符串的修改,无论是增长还是缩短,都需要进行一次内存的重新分配,这样就会导致大量的时间浪费在内存的分配上,而SDS通过free来记录未使用空间,解除了字符串长度和底层数组之间的长度关联,通过空间预分配和惰性空间释放的方式来减少内存重新分配的次数,优化reids对数据频繁修改场景下的性能

4) 二进制安全:C字符串通过末尾的空字符来确认读取的结束,这就对字符串的存储带来了限制,因为C字符串的中间不能出现空字符,否则会导致空字符后面的数据不能被读取,而SDS通过len字段保存了字符串的长度,这样在读取的时候就不需要通过空字符来确认是否读取完全,直接按照len来读取指定长度的字符串,这样使得redis不仅能保存字符串,还能保存任意格式的二进制数据

 

二、list(列表)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),主要的应用场景为消息队列、定时计算排行榜、非频繁更新的最新列表等,list(列表)实现的数据结构包括ziplist(压缩列表)和linkedlist(链表),当列表元素比较少(元素数量小于512个)且列表项是小整数值或者较短字符串(长度小于64个字节)时会用ziplist来作为的底层实现否则转为使用linkedlist(链表)

2.1、ziplist(压缩列表)
压缩列表,可以拆开来看,压缩是其特性(节约空间),列表是其功能,用来存储列表项集合,其主要目标就是通过紧凑的存储结构来达到节约内存的目标

zlbytes:记录列表占用的内存字节数

zltail:记录压缩列表尾节点距离列表起始地址的偏移量,用来快速定位表尾节点

zllen:节点数量

entry:保存数据的节点

zlend:标记列表的结尾

entry(节点)结构:

previous_entry_length:记录前一个节点的长度

encoding:节点保存的数据类型及长度

content:节点的值

2.1.1压缩列表的特点
从上面所给出的ziplist结构,我们可以结合来看看ziplist所具有的如下特性

ziplist压缩表是紧凑存储的,没有多余的空间,进而达到节约内存的目的,而同时也会导致ziplist不适合存储大量的数据数据或者大型数据,因为要保证数据都是紧凑存储的,每次添加元素都会扩展新的内存,如果当前内存块没有足够的空间用来扩展,那就需要重新分配新的内存空间幷进行数据拷贝,而数据拷贝是非常消耗性能的,特别是在redis单线程的特性下,会严重影响后续的操作,因此在列表项比较小且都是小整数值或者较短字符串时才会用ziplist来作为的底层实现

有专门的字段记录尾节点的偏移量,因此可以以O(1)的复杂度获取到尾结点

每个节点用previous_entry_length保存了前一个节点的长度,因此可以O(1)的复杂度获取当前节点的前一个节点地址

每个节点用encoding保存了数据的长度,因此可以O(1)的复杂度获取当前节点的下一个节点

综合3和4点,压缩列表可以同时满足向前或向后遍历

根据zltail和各个节点的previous_entry_length,可以从后向前遍历到每一个节点
 

2.2、linkedlist(链表)
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活的调整链表的长度,链表作为一种常用的数据结构,在redis中不仅作为列表和hash的底层实现之一,而且在reids本身的发布与订阅、慢查询、监视器等功能实现中都有使用
 

2.2.1、链表特点
reids的链表实现和我们平常接触的大部分双端链表实现相似,即节点包含了前置节点和后置节点,链表包含了头结点,尾结点及其他一些基础信息,因此redis的链表也具备了链表的一些特点:

链表节点带有prev和next,因此可以O(1)复杂度获取某个节点的前置节点和后置节点

可以O(1)复杂度获取头结点和尾结点

因为记录了节点数量,因此可以O(1)复杂度获取链表长度

具备从后到前、从前到后的遍历能力

2.3、压缩列表和链表
list(列表)实现的数据结构包括ziplist和linkedlist两个,对比上面两种数据结构的内部实现细节我们可以发现,redis同时使用这两个数据结构,是出于平衡内存和cpu的目的,ziplist压缩表是紧凑存储的,每次添加元素都是在数据后扩展新的内存,然后紧密存储,如果当前内存块没有足够的空间用来扩展,那就需要重新分配新的内存空间幷对已经存储的数据进行拷贝,可以看做是一种用时间换空间的考量,但是在数据较大或则数据量比较多的情况下,会出现因内存空间不够而导致的大量拷贝操作,这会导致cpu性能损耗过大,这时候就会转到linkedlist存储,以空间换时间。
 

三、hash对象
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,特别适合用于存储对象。哈希的内部实现数据结构也是两种:ziplist(压缩列表)和字典(hashtable)。和list一样,当存储的数据量较少(小于512个)且较小(小于64个字节)的情况下,使用压缩列表来存储数据,当数据量超过规定的阈值后转为用字典来存储
ziplist已经介绍过了,仅有的区别是相邻两个节点分别存储hash对象的健和值,保存键的节点在前, 保存值的节点在后,两两一组。

3.1、字典

字典又称为符号表,可以理解为java中的HashMap,在字典中,一个健和一个值进行映射,我们称之为键值对,在C中幷没有这种数据结构,于是redis构件了自己的实现--hash表

3.2、字典的定义

typedef struct dict {
    //类型特定函数,可以根据所存数据类型的不同,指向不同的数据操作实现
    dicType **type;
    /私有数据,配合type使用
    void *privdate;
    //hash表
    dictht ht[2];
    //rehash索引,当不进行rehash时值伟-1
    int trehashidx;
       
}dict;

//hash 表
typedef struct dictht {
    //哈希表数组
    dicEntry **table;
    //哈希表大小
    unsigned long size;
    //已有节点数量
    unsigned long used;
       
}dictht;
//hash表节点
typedef struct dicEntry {
    //键
    void *key
    //值
    union{
        void *val;
    }v;
    
    //下一个节点指向
    struct dicEntry *next;
       
}dicEntry;

 

3.4、字典关键点
健冲突:通过定义以及数据结构图可以看出,redis的字典和java中的HashMap相似,通过key的hash来确定table的下标,当两个不一样的key由于hash冲突定位到同一个table下标时,会通过链表的方式将多个冲突节点连接起来

rehash:在dic的定义中,我能可以看到dictht数组的容量是2,也就是有两个hash表,一般情况下只使用ht[0]来进行数据的存储,而ht[1]是用来在ht[0]进行内存扩展或收缩时协助其进行rehash的,也就是将数据从h[0]拷贝迁移到h[1],因为h[0],h[1]的大小不一样,在迁移时,根据表大小和健值计算的hash值会存在变化,所以叫这个迁移过程为rehash。reids的数据迁移是渐进式的,因为由于redis的单线程特性,在数据量较大的情况下,如果一次将h[0]的数据全部迁移到h[1]上,会导致工作线程的长时间占用,reids对外其他服务的中断,这是不可接受的,所以redis将rehash的操作分摊到了添加、删除、查找、更新操作中,在执行操作之后对相应的键值对进行rehash操作,在rehash完成之后,会将ht[0]指向rehash后的字典,而ht[1]重新指向一个空表,准备下一次的rehash操作

因为字典对数据的操作过程都是通过对健值的hash值来确定数组下标进而定位数据的,所以使用字典实现的hash对象在操作指定健的情况下可以达到O(1)级别的复杂度,当然,在数据量较大的情况下,hash冲突增多会存在遍历链表的情况,进而影响性能,但是redis会通过rehash操作来进行扩展或者缩容来进行内存空间的释放和扩张来控制ash冲突程度,也就是控制链表的长度,用空间来平衡时间。
 

四、集合(Set)
Redis 的 Set 是 一个无序集合,其特点是集合元素无序且不重复,根据其特性,set主要使用的场景有好友/关注/粉丝/感兴趣的人集合、 随机展示、黑名单/白名单等一系列需要排重的场景,集合存储数据的底层实现包括整数集合(intset)和字典(hashtable),当set保存的数据都为整数且元素个数不超过512个时,使用整数集合(intset),否则使用字典(hashtable)。

4.1、整数集合(intset)

整数集合是redis用于保存整数值的集合抽象数据结构,他可以保证集合中不会出现重复的元素,其定义如下

encoding:

这个字段主要用来确认需要为每个元素分配的内存空间大小,encoding的取值有三个:int16_t、int32_t、int64_t,每个类型表示contents数组里可放元素的大小,类似于shot、int、long,依次向上可存储的数据大小范围越大,通过这三个类型,intset可以实现更精细化的内存分配,到达节约内存的目的,比如insert里面存储的都是int16_t范围内的数据,那他就可以只为每个元素分配16位的内存,当都是int16_t数据的insert插入int32_t的数据时,int16_t会扩张内存幷将所有的int16_t转换为int32_t,来完成contents类型升级,进而能存储32位的数字,以此内推到int64_t,但是不能反着进行降级,也就是说即使这个32位的数字删除了,也不会再转回int16_t,这样可以防止数据跨度大且修改频繁导致反复升级降级的情况,影响性能,可以看出如果直接使用int64_t那肯定可以存储所有的可能数字,但是这样的话,就必须为每个元素分配64位的空间,如果我们都是存储int16_t范围的数据呢,这样就会造成空间的浪费,而且在实际业务使用过程中,一般集合存储的数据都是比较相近的,因此前面说到的类型升级一般不会很多。集合专门为整数存储设计对应的intset就是出于对内存的考虑,这里通过encoding又对内存做了进一步的优化。
 

 4.2、字典(hashtable)
字典在介绍hash的时候已经做了介绍,使用的是同一个定义,但是集合存储的是非重复的单个元素,所以使用上和hash有细微的差别,那就是字典的每个健都是一个字符串对象,用来存储元素,而字典对应的值则全部设置为null,所以集合在使用字典时,只用到了健存储。

五、有序集合(sorted set)
Redis 有序集合和集合一样元素的集合,且不允许有重复的成员,不同的是每个元素都会关联一个double类型的分数,redis通过分数来为集合中的成员进行从小到大的排序。有序集合的的应用场景和集合类似,只是多了自动排序的能力,主要的场景有:排行榜、权重队列等。其实现的底层数据结构也是两种:ziplist(压缩列表)和跳表(skipList)+字典

5.1、压缩列表
在介绍list列表的时候就介绍了压缩列表的数据结构,通过紧密相连的节点来存储元素,来达到节约内存的目的,有序集合和hash对象使用压缩列表实现的数据构造类似,都是相邻两个节点来存储一个元素的信息,只不过hash存储的是键值对,而这里存储的是元素值和排序用的double分数,数据结构图如下:


 

 

5.2、跳表

跳表全称为跳跃列表,是一个允许快速查询,插入和删除的有序数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。快速查询是通过维护一个多层次的链,通过有限次范围查找来实现的,他的效率和红黑树不相上下,但是实现原理相对于红黑树来说简单很多。

 

 
版权声明:本文为CSDN博主「zw147258369」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zw147258369/article/details/102391990

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值