Redis的底层数据结构

1、Redis的五种数据类型及七种底层结构

键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合

对于Redis来讲,对于键值对来说,键总是字符串,值就是五个中的一个,所以我们只用关心值的类型。**值有五种数据类型**

String Hash List Set ZSet

​​​​​​​Redis的底层数据结构_第1张图片

 

​​​​​​​Redis的底层数据结构_第2张图片

Redis的底层数据结构_第3张图片

Redis的底层数据结构_第4张图片

Redis的底层数据结构_第5张图片

Redis的底层数据结构_第6张图片

1、Redis的底层数据结构

Redis底层数据结构有⑦种:

1、简单动态字符串

2、链表

3、字典

4、跳跃表

5、整数集合

6、压缩列表

7、快速列表

2、Redis的底层数据结构

Redis底层数据结构之hash - 凡尘多遗梦 - 博客园讲的挺详细的

1、简单动态字符串(SDS)

(1)Redis默认字符串底层存储结构,比如set k1 v1,键k1是一个字符串,底层实现是保存着字符串k1的SDS,值v1也是一个字符串,底层实现是保存着字符串v1的SDS

(2)每个sds.h/sdshdr表示一个SDS,结构如下

struct sdshdr {
    //记录buf数组中已使用字节的数量,相当于保存的字符串的长度
    int len;
    //记录buf数组中未使用的字节数量
    int free;
    //字节数组,用于保存字符串
    char buf[];
}

底层是一个SDS,核心是字节数组 char buf[];注意是字节数组,因为Redis会使用SDS API以处理二进制的方式来处理SDS存在buf数组里的数据。

(3)优点:

1>获取字符串长度的复杂度为O(1)。

  • C字符串不记录自身长度,获取C字符串长度时必须遍历整个字符串计数得到,复杂度是O(N);

  • SDS字符串自身记录维护len长度属性,获得SDS字符串长度的复杂度是O(1);

2>杜绝缓存区溢出。因为其API会进行空间扩展,扩展之后未使用字节数量free和已使用字节数量len一样

和重分配的区别是:这是在修改之前,预先判断内存空间是否够用,不够用则进行拓展

C字符串不记录长度,由于两个C字符串在内存存储上紧邻,在执行字符串拼接strcat时,如果不提前分配足够空间,很可能发生修改s1的数据溢出到s2所在的空间中;(缓冲区溢出) SDS完全杜绝了缓冲区溢出问题,它记录了长度,当修改SDS字符串之前,API都会检查SDS的空间是否满足修改的要求,不满足API会自动进行空间扩展。

3>减少字符串修改时的内存重分配次数,因为有free(预分配),所有在最坏的情况下就是修改n次,重分配n次。

在修改之后,进行预分配(len<1mB,free=len,大于1MB,free=1MB)

C字符串每次修改增加或减少时,都会对保存这个C字符串的数组进行一次内存重分配; 拼接操作,在执行之前,程序需要先通过内存重分配来扩展底层数组的空间大小——如果忘了这一步就会产生缓冲区溢出。 截断操作,在执行之后,程序需要通过内存重分配来释放字符串不再使用的那部分空间——如果忘了这一步就会产生内存泄漏。 内存重分配算法复杂还需要执行系统调用,如果发生频繁修改可能对性能造成影响。

SDS通过free未使用空间,实现了空间预分配和惰性空间释放两种优化策略。 空间预分配 空间预分配:增长操作时,Redis可以减少连续执行字符串增长操作所需的内存重分配次数; 当SDS做增长操作,不仅会分配修改所必须要的空间,还会额外分配未使用空间,具体分配未使用空间如下2种方式: 一:如修改后长度len小于1MB,就分配和len属性相同大小的未使用空间;free=len; 二:如修改后长度len大于等于1MB,就分配1MD的未使用空间;free=1MB;

4、二进制安全。

C字符串的字符必须符合某种编码,除结尾空字符以外,字符串内部不允许有空字符串。存储有局限性; SDS存储的都是二进制安全的字符串,SDS API都会以处理二进制的方式来处理SDS存在buf数组里的数据。(这也是SDS的buf称之为字节数组的原因因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据)

C字符串只能存储文本数据,SDS可以存储二进制数据

5)兼容部分C字符串函数。

  • 虽然SDS是二进制安全的,但是还秉承C字符串以空字符结尾的特性。很多函数是可以共用C字符串的不需要重写;

img

用处:

1、String通常用于保存单个字符串或JSON字符串数据 2、因为String是二进制安全的,所以可以把保密要求高的图片文件内容作为字符串来存储 3、计数器常规Key-Value缓存应用,如微博数、粉丝数。INCR本身就具有原子性特性,所以不会有线程安全问题

是String字符串的底层数据结构

2、字典(dictht)(比如将键设计为对象的名称?)

Redis hash是一个string类型的field和value的映射表,hash特别适用于存储对象

可以通过字典(dictht)这种结构实现,类似于HashMap(结构是数组+链表)

数组叫做哈希表数组(table),链表节点为dictEntry,每个Entry包含键和值,并且Entry连接到哈希表的索引下标处

1)字典又称为符号表、关联数组或映射,是一种用于保存键值对的抽象数据结构,如果了解java7的HashMap的底层实现,那么这个自然就懂了。

(2)使用哈希表由dict.h/dictht结构定义

struct dictht{
    //哈希表数组 
    dictEntry **table;
    //哈希表大小 
    unsigned long size;
    //大小掩码,用于计算索引值,总是等于size-1
    unsigned long sizemask;
    //已有节点的数量  
    unsigned long used;
}dictht;

table是一个数组,类型是指向dict.h/dictEntry结构的指针。每个dictEntry保存着一个键对值,结构如下

struct dictEntry{
    //键
    void *key;
    //值,下面三个的其中一个
    union{
        //指针
        void *val;
        //uint64_t整数
        uint64_tu64;
        //int64_t整数
        int64_ts64;
    }v;
    //指向下一个节点的指针
    struct dictEntry *next;
}dictEntry;

如果此时表中有一个entry的键为k0,插入键为k1的entry的时候,k1、k0它俩的hash值都一样,这时候就发生了哈希冲突,那么此时k1就会放在table中对应的索引下,k1的next就会指向k0,这个就是解决hash冲突的实现。

Redis的底层数据结构_第7张图片

总结:由哈希表数组+字典Entry构成,其中哈希表数组定义了存储数组的大小,已有Entry的个数对于相同key的字典Entry存储到相同的数组索引下标处,并且以链表的形式连接。采用的存储结构是数组+链表

解决哈希冲突的方法:拉链法

用处:

存储对象的属性信息

应该是一个对象对应着一个table,一个属性对应着一个dictEntry,每个Entry是由键值对构成的

根据以上字典的特性,我们知道,字典和 HashMap 的特性是差不多的,也是基于数组和链表的组合,所以一般都是用来存储键值对的。不过在实际运用场景中,我们会经常用来存储一个对象的信息。例如:一个对象的属性如 name ,age ,weight 等属性,可以直接当做一个个键值对存储在这个字典下,这样做的好处是,在需要用到这个对象的属性时,不用把这个对象全部序列化,只需要取出特定的就可以,而在传统的需要全部序列化,相比来讲就比较消耗资源。

这里用处写的很模糊

是哈希(Hash)和集合的底层数据结构

怎么解决属性值为Object类型?

redis hash每一个hashEntry只能存储key(String)->value(string)字符串类型数据

讲了redisTemplate的hash数据类型存储{key(String)->value(string)}的方法; 但是实际清楚我们存储对象应该是{key(string)->value(object)}类型的。 开始测试的时候,报错为 object can not cast to object,发现redis的hash类型只能存储string类型的数据。后经翻阅资料,找到存储序列化对象的方法解决。 实现serializable接口的作用就是可以把对象转化为字节流,并且可以反序列化恢复,对象实现序列化可以进行网络传输,在分布式应用中,就需要进行序列化对象的操作。

3、LinkedList(链表)

使用ListNode链表作为底层是一个双向链表,并且有头指针和尾指针。

双向链表!!!!!

(1)redis的list数据类型的底层实现之一,类似于java集合类LinkedArrayList。

(2)每个链表节点用一个adlist.h/listNode结构来表示

struct listNode{
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点的值
    void *value;     
}listNode;

多个listNode可以通过prev和next指针组层双端链表

(3)链表通过结构adlist.h/list来构建

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

结构如下: Redis的底层数据结构_第8张图片

用处:

阻塞队列和文章列表或者数据分页展示的应用

redis的链表结构可以在左边和右边插入数据

消息队列:Redis 的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过 Lpush 命令从左边插入数据,多个数据消费者,可以使用 BRpop 命令阻塞的“抢”列表尾部的数据。不过这种消息队列没有办法和专业的消息队列相比

常用命令:lpush:在头部插入元素,rpush在尾部插入元素,blpop在头部取出元素,brpop在尾部取出元素

文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用 Redis 的列表,列表不但有序同时还支持按照范围内获取元素可以完美解决分页查询功能。大大提高查询效率。

是列表的底层数据结构

4、整数集合

全部是整数值,无重复, 有序,这里的有序是指取出的顺序和放入的顺序一致,而不是会对元素大小自动排序

当一个集合中的*元素都是整数值,且元素不多的时候,*整数集合就会作为集合 Set 的底层实现。

Redis 中的整数集合 intset 是用来保存多个不重复的整数值且有序的集合抽象数据结构,可以保存类型为 int16-t 、int32-t 或者 int64-t 的整数值。它是实现集合键底层之一。

并且保证集合中不会出现重复元素

typedef struct intset {
​
    // 编码方式
    uint32_t encoding;
​
    // 集合包含的元素数量
    uint32_t length;
​
    // 保存元素的数组
    int8_t contents[];
​
} intset;

encoding是整个整数数组的存储类型,假设添加一个新元素比原来数组的encoding要大,比如int64, 原数组会进行升级,将encoding变为int64

升级操作步骤:时间复杂度为O(N) 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。 将新元素添加到底层数组里面。 将enconding改为当前类型,长度加一。

所有的元素都变为新的encoding类型

升级的好处

整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。

(1)提升灵活性

因为整数集合可以通过自动升级底层数组来添加元素,所以可以任意添加不同类型的数值(int16_t,int32_t,int64_t),不必担心类型错误。

(2)尽可能的节约内存

不必上来就定义int64_t类型(或者更长的类型)的数组,而是在需要的时候在扩展为int64_t(或者更长的类型);

特点:

全部元素为整数,无重复,有序,可以升级不能降级

是集合(Set)的底层数据结构

5、跳跃表

跳跃表()

(1)跳跃表是一种有序的数据结构,通过每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。这种数据结构可以在小于等于O(n)的情况下找到相应的数据。

1>由很多层组成

2>每一层都是一个有序的链表

3>最底层的链表包含了所有的元素;

4>如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);

5>链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;

Redis的底层数据结构_第9张图片

简单的理解就是当前层节点之间的间隔都比下一层更大,但是每一层都必须是有头结点和尾节点。

特点:

有序,多层,去重,构成有序集合的底层结构(ZSET)

是有序列表的底层结构

跳跃表查询:

从上层链表开始查询,遇到比查询值小的节点向后查询,遇到比查询值大的节点返回前一个结点并向下查询

6、压缩列表

Redis数据结构——压缩列表 - Mr于 - 博客园

传统的数组存储元素的时候,每个元素占用的内存大小都是一样的,比如Int64,这样就会造成内存的浪费,但是好处是因为数组元素的内存地址都是连续的,所以好计算元素的位置,比如比较小的数。因此这是redis为了节省内存自己设计的一种数据结构,它是对于数组的每一个元素,都带有前一元素占用内存的大小,这样,元素就不需要全部占用一样的内存了,从而减少内存开销

数组:

Redis压缩列表的机构示意图:

每个压缩列表节点可以保存一个字节数组或者一个整数值。

上面的图只是形象的表示,实际上是存储的前一个节点的内存大小

节点的结构:

分别定义了前一节点占用内存大小,此节点的编码方式,此节点的内容

对于前一节点的占用内存大小:

前一节点长度小于254字节,则该字段长度为一字节保存其长度大小

前一节点长度大于254字节,该字段长度使用5个字节记录前一节点长度

对于此节点的内容:可以是字节数组或者整数

特点:

  • 压缩列表是Redis为节约内存自己设计的一种顺序型数据结构。

  • 压缩列表被用作列表键和哈希键的底层实现之一。

  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。

  • 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。

7、快速列表

考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。

后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist

quickList 是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。

Redis的底层数据结构_第10张图片

这样就减少了双向链表的前后指针,并且中间是使用的是zipList,其内部是连续的,也减少了内存的碎片化

传统链表的缺点:前后指针占用内存大,并且内存空间不连续,容易产生内存碎片

快速列表使用了双向链表,并且每个节点就是一个zipList,这样减少了前后指针,也减少了内存碎片化

优点是:节省内存以及较少内存碎片化

3、Redis的数据类型以及使用场景

1、String类型

一种实现方法

底层使用简单动态字符串(SDS)

使用场景

1、String通常用于保存单个字符串或JSON字符串数据 2、因为String是二进制安全的,所以可以把保密要求高的图片文件内容作为字符串来存储 3、计数器常规Key-Value缓存应用,如微博数、粉丝数。可以快速实现计数和查询的功能。INCR本身就具有原子性特性,所以不会有线程安全问题

4、缓存功能:String 字符串是最常用的数据类型,不仅仅是 Redis,各个语言都是最基本类型,因此,利用 Redis 作为缓存,配合其它数据库作为存储层,利用 Redis 支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。

2、列表(List)

三种实现方式

可以通过链表或者压缩列表或者快速列表实现

用处:

阻塞队列和文章列表或者数据分页展示的应用

redis的链表结构可以在左边和右边插入数据

消息队列:Redis 的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过 Lpush 命令从左边插入数据,多个数据消费者,可以使用 BRpop 命令阻塞的“抢”列表尾部的数据。(应该是新加入的元素为头部?) 文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用 Redis 的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

快速列表也可以作为列表的一种实现方式

3、Hash

两种实现方式

底层由字典或者压缩列表实现

用处:

存储对象属性信息

zipList怎么实现hash

ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:

保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后; 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

4、Set

两种实现方式

底层由整数集合或者字典实现

一种去重无序的集合(这里的无序是说没有对其中的元素自动排序,而不是存取的顺序不同)

用处:

对数据进行自动去重或者进行交集并集差集的操作

Set 是会自动去重的无序集合。直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果需要对一些数据进行快速的全局去重,当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果某个系统部署在多台机器上呢?得基于 Redis 进行全局的 Set 去重。

可以基于 Set 玩交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁。

dict字典为什么能作为set的一种实现方式

set对外与list类似,都是提供列表功能,即单键多值,唯独多了一个自动去重功能。

在这里插入图片描述

5、Sorted Set

底层使用跳跃表实现

用处:

Sorted set 是排序的 Set去重但可以排序,写进去的时候给一个分数,自动根据分数排序。

排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。 用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score 的倒序来获取工作任务。让重要的任务优先执行。

4、Redis常用的命令?

4、Redis的数据对象类型是什么?

redisObject

redisObject是Redis类型系统的核心,数据库中的每个键、值,以及Redis本身处理的参数,都表示为这种数据类型。

//Redis 对象
typedef struct redisObject {
    // 类型,记录了对象所保存的值的类型
    unsigned type:4
    // 对齐位
    unsigned notused:2;
    // 编码方式,记录了对象所保存的值的编码
    unsigned encoding:4;
    // LRU 时间(相对于 server.lruclock)
    unsigned lru:22;
    // 引用计数
    int refcount;
    // 指向对象的值,指向实际保存值的数据结构, 这个数据结构由type属性和encoding属性决定
    void *ptr;
} robj;

type:有五种分别代表Redis的五种数据类型

Redis的底层数据结构_第11张图片

你可能感兴趣的:(java,redis,数据结构,java)