Redis五大数据类型,及其底层数据结构实现原理

本文详细介绍了Redis的五大基本数据结构,包括String、Hash、List、Set、SortedSet,阐述了其底层实现及特点。如String基于SDS,Hash采用渐进式rehash策略。还介绍了ZipList、LinkedList等底层数据结构,分析了它们的优缺点及适用场景。

五大基本数据结构

redis存储的都是 key:value格式的数据,key为字符串格式,value有5种

  • String:字符串:sds动态字符串
  • hash:map格式:数组+链表
  • list:linklist(允许重复):3.2之前是ziplist linkdelist,之后是quicklist
  • set:集合类型(不允许重复:intset或hashtable
  • sortedset:有序集合:zset或ziplist。zset使用字典+跳表实现

String

底层是通过SDS简单动态字符串实现的

struct sdshdr {    
  // 用于记录buf数组中使用的字节的数目
  // 和SDS存储的字符串的长度相等  
	int len;    
  // 用于记录buf数组中没有使用的字节的数目   
	int free;    
  // 字节数组,用于储存字符串
	char buf[];   //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
};

在这里插入图片描述


free属性的值为0,表示这个SDS没有分配任何未使用空间。
len属性的值为5,表示这个SDS保存了一个五字节长的字符串。
buf属性是一个char类型的数组,数组最后保存了空字符‘\0’。

SDS除了用来保存数据库中的字符串之外,SDS还被用作缓冲区(buffer),

如AOF模块中的AOF缓冲区,

以及客户端状态中的输入缓冲区

好处:

  1. 获取字符串长度快

    • 复杂度为1,而c语言中的字符串是要遍历获取长度的,复杂度为n
  2. 防止缓冲区溢出

    • c语言中,需要手动释放内存,容易内存溢出。而当使用sds进行修改字符串的时候,会先检测,如果不够则拓展空间。之后再进行操作。每次操作之后,len和free的值会做相应的修改。
  3. 减少修改字符串时带来的内存重分配次数

    • SDS通过空间预分配惰性空间释放两种优化策略来减少内存重分配次数。

    • 空间预分配
      Redis通过额外分配未使用的空间,优化了SDS的字符串增长操作,减少了连续执行字符串增长操作所需的内存分配次数。

      惰性空间释放
      惰性空间释放用于优化SDS的字符串缩短操作。当SDS缩短时,程序并不会立即回收缩短后多出来的空间,而是使用free属性将这些字节的数量记录起来,等待将来使用
      注:如果需要真正地释放SDS的未使用空间,我们可以使用相应的API。

  4. 二进制安全

    • C字符串除了末尾之外不能出现空字符,否则会被程序认为是字符串的结尾。这就使得C字符串只能存储文本数据,而不能保存图像,音频等二进制数据。

    • 使用SDS就不需要依赖控制符,而是用len来指定存储数据的大小,所有的SDS API都会以处理二进制的方式来处理SDS的buf的数据。程序不会对buf的数据做任何限制、过滤或假设,数据写入的时候是什么,读取的时候依然不变。

  5. 兼容部分C字符串函数

    • SDS的buf的定义(字符串末尾为’\0’)和C字符串完全相同,因此很多的C字符串的操作都是适用于SDS->buf的。比如当buf里面存的是文本字符串的时候,大多数通过调用C语言的函数就可以。

Hash

redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构.当发生 hash 碰撞时将会把元素追加到链表上.值得注意的是在 Redis 的 Hash 中 value 只能是字符串.

在 Java 中 HashMap 扩容是个很耗时的操作,需要去申请新的数组,为了追求高性能,Redis 采用了 渐进式 rehash 策略.这也是 hash 中最重要的部分。

在扩容的时候 rehash 策略会保留新旧两个 hashtable 结构,查询时也会同时查询两个 hashtable.Redis会将旧 hashtable 中的内容一点一点的迁移到新的 hashtable 中,当迁移完成时,就会用新的 hashtable 取代之前的.当 hashtable 移除了最后一个元素之后,这个数据结构将会被删除.如图所示:

在这里插入图片描述

List

在版本3.2之前,Redis 列表list使用两种数据结构作为底层实现:

  • 压缩列表ziplist
  • 双向链表linkedlist
    因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现。
list-max-ziplist-value 64 
list-max-ziplist-entries 512 

默认情况下当压缩列表entry数据超过512、或单个value 长度超过64,底层就会转化成linkedlist编码;

但是在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。

Set

底层使用了intset和hashtable两种数据结构存储的,intset我们可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)。

set的底层存储intset和hashtable是存在编码转换的,使用intset存储必须满足下面两个条件,否则使用hashtable,条件如下:

  • 结合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个

SortedSet

sortedset的一些命令:

存储:zadd key score value

获取:zrange key start end

获取:同时获取分数:zrange key start end with score

删除:zrem key value

存储的时候我们可以发现,是有一个score(分数)的,这个就是用来排序的字段。

下面我们看源码:

首先,我们来看几个参数:

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

在理解这两个参数前,我们先简单了解下 ziplist 这种数据结构,顾名思义:压缩列表,怎么个压缩法,简单来说就是对于大的数据会用比较多点字节来存储,对于小的数据就用比较少的字节来存储。

if (
    field-value对的数量 > ziplist.entries.size ||
    任意一个filed或value长度 > zset-max-ziplist-value
) {
    // 使用 zset 进行存储
} else {
    // 使用 ziplist 进行存储

也就是说,redis会根据配置,选用zset,或ziplist进行存储。

而zset就是使用字典+跳表实现的:

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

zset 结构体里有两个元素,一个是 dict,用来维护 数据 到 分数 的关系,一个是 zskiplist,用来维护 分数所在链表 的关系

dict 里通过维护 哈希表 存储了 张三=>100,李四=>90 的分数关系

跳表:

在这里插入图片描述

跳表的插入过程:

在这里插入图片描述

插入之前也要先经历一个类似的查找过程,在确定插入位置后,再完成插入操作。

普通跳表有很多限制和问题,如下:

(1)分数不允许重复

(2)第一层链表为单向链表,不够灵活

于是,Redis 改良版的 zskiplist 被发明了

在这里插入图片描述

允许重复分数;第一层链表改为了双线链表,方便进行倒序范围查找;增加span(跨度)概念,方便计算正向和反向排名

底层数据结构

ZipList

压缩列表 ziplist 是为 Redis 节约内存而开发的。

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点 (entry),每个节点可以保存 一个字节数组 或者 一个整数值 。

在这里插入图片描述

1、zl bytes:用于记录整个压缩列表占用的内存字节数

2、zl tail:记录要列表尾节点距离压缩列表的起始地址有多少字节

3、zl len:记录了压缩列表包含的节点数量。

4、entryX:要说列表包含的各个节点

5、zl end:用于标记压缩列表的末端

压缩列表是一种为了节约内存而开发的顺序型数据结构

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

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

添加新节点到压缩列表,可能会引发连锁更新操作。

LinkedList

就是个双向链表

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

QuickList

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

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

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

在这里插入图片描述

在这里插入图片描述

// 快速列表
struct quicklist {
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素总数
    int nodes; // ziplist 节点的个数
    int compressDepth; // LZF 算法压缩深度
    ...
}
// 快速列表节点
struct quicklistNode {
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向压缩列表
    int32 size; // ziplist 的字节总数
    int16 count; // ziplist 中的元素数量
    int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
    ...
}
typedef struct quicklistNode {
    struct quicklistNode *prev; //上一个node节点
    struct quicklistNode *next; //下一个node
    unsigned char *zl;            //保存的数据 压缩前ziplist 压缩后压缩的数据
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    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;



struct ziplist_compressed {
    int32 size;
    byte[] compressed_data;
}

struct ziplist {
    ...
}

相对于链表它压缩了内存。进一步的提高了效率。

字典

Redis定义了dictEntry(哈希表结点),dictType(字典类型函数),dictht(哈希表)和dict(字典)四个结构体来实现字典结构,下面来分别介绍这四个结构体。

//哈希表的table指向的数组存放这dictEntry类型的地址。定义在dict.h/dictEntryt中
typedef struct dictEntry {//字典的节点
    void *key;
    union {//使用的联合体
        void *val;
        uint64_t u64;//这两个参数很有用
        int64_t s64;
    } v;
    struct dictEntry *next;//指向下一个hash节点,用来解决hash键冲突(collision)
} dictEntry;

//dictType类型保存着 操作字典不同类型key和value的方法 的指针
typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);      //计算hash值的函数
    void *(*keyDup)(void *privdata, const void *key);   //复制key的函数
    void *(*valDup)(void *privdata, const void *obj);   //复制value的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);  //比较key的函数
    void (*keyDestructor)(void *privdata, void *key);   //销毁key的析构函数
    void (*valDestructor)(void *privdata, void *obj);   //销毁val的析构函数
} dictType;

//redis中哈希表定义dict.h/dictht
typedef struct dictht { //哈希表
    dictEntry **table;      //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
    unsigned long size;     //哈希表table的大小,初始化大小为4
    unsigned long sizemask; //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
    unsigned long used;     //记录哈希表已有的节点(键值对)数量。
} dictht;

 //字典结构定义在dict.h/dict
typedef struct dict {
    dictType *type;     //指向dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据。
    void *privdata;     //私有数据,保存着dictType结构中函数的参数。
    dictht ht[2];       //两张哈希表。
    long rehashidx;     //rehash的标记,rehashidx==-1,表示没在进行rehash
    int iterators;      //正在迭代的迭代器数量
} dict;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值