美文网首页我爱编程redis学习
Redis的数据结构与对象

Redis的数据结构与对象

作者: 茶还是咖啡 | 来源:发表于2018-05-22 00:38 被阅读0次

SDS

数据结构

struct sdshdr{
    //记录buf数组中已使用的字节数量
    //等于SDS所保存字符串的长度
    int len;

    //记录buf数组中未使用的字节数量
    int free;

    //字节数组,用于保存字符串
    char buf[];
};

SDS 与 C

  1. C获取字符串长度:O(n),SDS获取字符串长度:O(1)。
  2. SDS对字符串操作时会先检查空间是否满足需求,去过不满足的话会对SDS进行扩容,C不会检查,所以C会造成缓冲区溢出。
  3. SDS分配为字符串分配空间时,如果要存储的字符串长度小于1MB,那么会分配空余时间和len属性同样大小的预留空间,如果len的属性大于1MB,那么会分配1MB的预留空间。
    a) 会减少内存重分配的次数。
    b) 如果字符串长度变短,也不会立即释放多余的空间。
  4. SDS用len属性记录字符串的长度,C用‘\0’记录字符串的长度,所以SDS可以存储的字符串的范围比C大。

链表

数据结构

链表节点

typedef struct listNode{
    //前置节点
    listNode *prev;

    //后置节点
    listNode *next;

    //节点的值
    void* value;
}listNode;

管理链表的节点


typedef struct list{
    //表头节点
    listNode *head;

    //表尾节点
    listNode *tail;

    //链表包含的节点
    unsigned long len;

    //节点复制函数
    void *(*dup)(void *ptr);

    //节点释放函数
    void(*free)(void *ptr);
}list;

链表特点

  1. 双端:双向链表访问前一个节点和后一个节点的时间复杂度为O(1)。
  2. 无环:表头节点的prev指向NULL,表尾的next指向NULL,对链表的遍历以NULL结束。
  3. 带表头和表尾指针:访问表头和表尾节点的时间复杂度为O(1)。
  4. 记录链表的长度变量:获取链表长度的时间复杂度O(1)。
  5. 多态:采用函数指针实现多态。

链表用途

列表键、发布与订阅、慢查询、监视器。

字典

数据结构

//hash节点
typedef struct dictEntry{
    //键
    void *key;

    //值
    union{
        void *val;
        unsigned __int64 u64;
        __int64 s64;
    }v;

    //指向下一个哈希表节点形成链表
    struct dictEntry *next;
}dictEntry;
//哈希表
typedef struct dictht{
    //哈希表数组
    dictEntry **table;

    //哈希表大小
    unsigned long size;
    
    //哈希表掩码,用于计算索引值
    //大小总是等于size-1
    unsigned long sizemask;

    //hash表已有节点数量
    unsigned long used;
}dicht;
//管理hash表的节点
typedef struct dict{
    //私有数据
    void *privdata;
    
    //哈希表
    dictht ht[2];

    //rehash索引
    //当rehash不进行时,值为-1
    int trehashidx;
}dict;
字典

hash算法:

首先根据key计算出hash值,
Index=hash&sizemask
解决键冲突:拉链法,头插的方式。

扩容收缩

  1. 当服务器没有执行BGSAVE命令或者BGREWRITEAOF命令,并且hash表的负载因子大于等于1。
  2. 服务器正在执行BGSAVE命令或者BGREWRITEAOF命令,并且hash表的负载因子大于等于5。
  3. Hash表的负载因子小于0.1时,程序会自动开始对hash表进行收缩操作。
    负载因子=hash表已保存的节点数量/hash表大小。
    Eg: load_factory=ht[0].used/ht[0].size
提高负载因子的原因:

执行上面的命令时,Redis需要创建当前服务进程的子进程,大多数系统采用写时复制来优化子进程的使用效率,所以子进程存在期间,服务器会提高扩展操作所需的负载因子,从而避免在子进程存在期间进行扩容操作,进而避免不必要的内存写入操作,尽最大限度的节约内存。

扩容和收缩的尺寸:

扩容:扩容后的大小2n 第一个大于等于已有节点的数量2的2n
eg:已有节点数量 6
6
2=12 <= 24 = 16 扩容后为16
收缩:收缩后的大小2n 第一个大于等于已有节点的数量的2n
eg:已有节点数量 8
6<=23=8 收缩后为8
ht[1]用来扩容使用,扩容完毕后释放ht[0]的指向,并将ht[1]扩容好的hash表重新用ht[0]指向。

渐进rehash

Rehash的动作不是一次完成,而是分多次,渐进式完成的。(如果键值对很多的,一次完成可能会导致服务器停止服务)

  1. 在字典中维持一个索引计数器rehashidx,值设为0,表示rehash正式开始。
  2. 在rehash期间,每次对字典进行添加、删除、查找或者更新,程序除了执行指定的操作外,还会将ht[0]哈索希表在rehashidx索引上的所有键值对rehash到ht[1]上。
    Rehash执行期间字典会同时使用ht[0]和ht[1]两个hash表,所以如果执行删、改、查时,会先检查ht[0]上是否有对应的键,如果没有会到ht[1]中查找,如果执行增操作,回直接对ht[1]进行添加。

跳跃表

数据结构

//跳跃表
typedef struct zskiplistNode{
    //后退指针
    struct zskiplistNode *backward;

    //分值
    double score;

    //成员对象
    void *obj;

    //层级
    struct zskiplistLevel{
        //前进指针
        struct zskiplistNode *forward;

        //跨度
        unsigned int span;
    }level[];
}zskiplistNode;
  1. 层:跳跃表的level数组可以包含多个元素。每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快其他节点的访问速度,层的数量越多,访问节点的速度就越快。层的范围(1~32)
  2. 前进指针:用于指向表尾方向的前进指针。
  3. 跨度:用于记录连个节点之间的距离,跨度越大距离越远。指向NULL的所有前进指针的跨度为0,因为没有指向任何节点。也可以用跨度来计算该节点在跳表中的位置。
  4. 后退指针:每次只能访问前一个节点不能跳级访问,
  5. 分值和成员:
    a) 分值是一个double型的浮点数,跳表中的节点都按照从小到大进行排序。
    b) 成员对象指向一个字符串对象,字符串对象中保存着SDS的值。


    跳跃表

跳表属性

跳表属性

  1. 跳跃表示有序集合的底层实现之一。
  2. 跳表表头保存表头节点表尾节点,长度;跳表节点保存分值和成员。
  3. 一个跳表中可以有多个相同的分值,但是相同的分值对象是不同的,分值相同时,节点按照对象的大小进行排序。
  4. 跳表时有序集合底层实现的原理之一。

整数集合

可以存储int_16,int_32,int_64的整数值,有序并且不会出现重复值。

数据结构

typedef struct intset{
    //编码方式
    unsigned __int32 encoding;

    //集合包含的元素数量
    unsigned __int32 length;

    //保存元素的数组
    __int8 contents[];
}intset;

升级降级

升级整数集合并添加新元素的类型,扩展为3步:

  1. 根据新元素的类型拓展整数数组的空间大小,并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位置上,并且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。
    【注意】
  4. 因为每次向集合添加新元素都有可能引起升级,每次升级都会对底层数组进行内存重分配,所有元素也会进行类型转换,所以向整数集合中添加元新元素的时间复杂度是O(n)。
  5. 因为引发升级的元素长度总是比现有整数的长度大,也就是说新元素可能都小于现有元素,也可能都大于现有元素,新元素摆放的位置要么在集合的最前面要么在集合的末尾(length-1)
  6. 整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

升级好处

  1. 尽可能的节约了内存。
  2. 提升了集合的灵活性:可以任意添加各种长度的整数集合,不用担心出现类型错误的情况。

压缩列表

为了节约内存而设计的一种特殊编码的连续内存组成的数据结构。
常用作列表键和hash键的底层实现之一。

数据结构

压缩列表数据结构
  1. zlbytes :记录压缩列表占用内存字节数;在对压缩列表进行内存重分配或者记录zlen的位置时使用。
  2. ztail:记录压缩列表表尾节点距离压缩列表起始地址有所少个字节,通过这个偏移量可以直接计算出表尾节点的地址。
  3. zllen:记录压缩列表包含节点的数量。(节点的数量小于65536才能计算出来)
  4. entry:压缩列表包含的各个节点。节点可以保存一个字节数组或者一个整数值。
  5. zlend:用于标记压缩列表的末端。
    压缩列表节点的构成:


    压缩列表节点的构成
  1. previous_entry_length:记录前一个节点的长度。程序可以根据前一个节点的长度,当前节点的地址,计算出前一个节点的起始地址,实现元素的逆向遍历。

  2. encoding:保存当前节点数据类型及其长度。

  3. content:可以保存一个字节数组或者一个整数值。值的长度和类型由encoding决定。

对象

数据结构

typedef struct redisObject{
    //类型
    unsigned type : 4;

    //编码
    unsigned encoding : 4;

    //指向底层数据结构的指针
    void *ptr;

    //...
}robj;

Redis数据库保存的键值对来说:
键总是一个字符串对象,而值则可以是字符串对象,列表对象,hash对象,集合对象或者有序集合对象其中的一种。

类型:记录了对象的类型。例如:字符串对象,列表对象,hash对象,集合对象,有序集合对象。
编码:对象的encoding属性记录了对象使用的编码,通过这种方式提升了Redis的灵活性和效率,
例如:列表对象包含元素少时使用压缩列表,压缩列表比双向链表更加节约内存,而且地址是连续的,可以更快的被载入内存。但是如果元素渐渐增多,压缩列表将失去优势,转换为双向链表。

字符串对象

字符串对象的编码可能是int ,raw,embstr。
如果一个字符串对象保存的是整数值可以用long类型表示那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面。
embstr是专门用来保存短字符串的一种优化编码方式。
Raw会调用两次内存分配分别创建对象和底层sds,embstr只会调用一次内存分配一块连续的空间来创建对象和底层sds。

Embstr好处

embstr将内存分配的次数降低为了1次,所以对于对象的创建,内存的回收以及缓存都会有优势。
如果是浮点数,redis会采用字符串的形式进行保存,对浮点数进行运算时,redis会先取出字符串,之后转换为浮点数进行运算,最后将运算后的浮点数转换成字符串保存起来。

编码转换

在整数后面追加一个字符串,会将int转换为raw
对一个embstr字符串进行修改时,embstr会转换为raw,(embstr没有任何的修改程序)

列表对象

列表对象的编码可以是ziplist或者linkedlist。

Hash对象

Hash对象编码可以是ziplist或者hashtable

Ziplist编码实现

每当有新的键值对要加入到hash对象中时,程序会将保存了键的压缩列表节点推入到列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。

特点:
  1. 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
  2. 先添加hash对象中的键值对会被放在压缩列表的表头方向,而后来添加hash对象中的键值对会被放在压缩列表的表尾方向。
如图:
压缩列表实现hashtable

集合对象

集合对象的编码实现可以是intset或者hashtable
使用hashtable作为编码的时候:字典的每个键都是一个字符串对象,字典的值都为NULL。

有序集合

有序集合的编码可以是ziplist或者skiplist和字典。

使用skiplist和字典的原理:

跳跃表的score属性保存了元素的分值,obj属性保存了元素的成员,字典的key属性保存了元素的成员,value属性保存了元素的分值。
他们是通过两个指针指向同一块内存实现的,所以不会出现内存浪费。

为什么要同时使用跳表和字典实现zset?

这属于取两者的长度综合起来使得zset元素的访问效率变高,跳表查找元素的时间复杂度为O(NlogN),而字典的查找元素的效率为O(1),但是跳表是有序的数据结构,所以对于范围查询,时间复杂度为O(1),但是字典的范围查询首先要讲整个字典进行排序,所以最快时间复杂度为O(NlogN),除此之外还需要O(N)的额外内存空间,所以可以将两种数据结构综合起来使用,达到效率最佳。

内存回收

C语言不具备内存回收功能,redis构建了引用计数实现内存回收。
创建一个对象时,该对象的引用计数值为1,被一个程序使用后引用+1,不在被一个程序使用,引用-1,引用为0,对象所占内存会被释放。

对象共享

Redis会在初始化服务器的时候创建一万个字符串对象对象包含了0-9999的所有整数值,服务器用到这些对象时,直接使用,不再创建。

相关文章

  • Redis专题

    1 数据结构与对象 1.Redis数据结构与对象——简单动态字符串2.Redis数据结构与对象——哈希3.Redi...

  • Redis设计与实现-笔记(一)

    数据结构与对象 Redis的底层数据结构,了解Redis的底层数据结构有助于我们更好的运用Redis。 SDS R...

  • 01-redis数据结构与对象

    3. redis数据结构与对象 redis对外支持数据结构 字符串 (string) 字符串列表(list) 字符...

  • redis对象

    本文对redis的对象进行概述,知识来源于《redis设计与实现》 我们可知,redis的用到的主要的数据结构有简...

  • Redis源码及实战分析(一) 数据结构与对象

    根据国人黄俊宏先生建议的阅读顺序 redis源码的设计与解析这部分记录下redis的数据结构与对象 有些人觉得理论...

  • 第 8 章(对象)

    Redis Object Redis 基于之前的那些数据结构创建了一个系统对象,这个系统包含字符串对象、列表对象、...

  • redis的设计与实现学习记录

    redis的学习分为以下几个部分 第一部分“数据结构与对象” redis里面数据库里面的每个键值对都是由对象组成的...

  • Redis设计与实现-笔记(二)

    数据结构与对象 跳跃表 跳跃表是有序集合的底层实现之一, 除此之外它在 Redis 中没有其他应用。 Redis ...

  • Redis 5种数据类型与11种编码方式

    1,redis核心对象结构 1)Redis object对象的数据结构,定义在src/server.h中。 2)c...

  • 1.4 字符串以及List底层实现

    Redis 并没有直接使用数据结构来构建键值对,而是基于这些数据结构创建了一个对象系统。 该对象保存与数据有关的三...

网友评论

    本文标题:Redis的数据结构与对象

    本文链接:https://www.haomeiwen.com/subject/yuecjftx.html