Redis数据结构与对象

Redis数据结构与对象

简单动态字符串
struct sdshdr{
    // 记录buf已使用字节数量
    int len;
    // 记录buf未使用字节数量
    int free;
    char buf[];
}
  • 常数复杂度获取长度

  • 杜绝缓冲区溢出

  • 减少修改字符带来内存重分配次数

    • 空间预分配
    • 惰性空间释放
  • 二进制安全

  • 兼容部分C字符串函数

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

typedef struct list{
    //表头节点
    listNode *head;
    //表尾节点
    listNode *tail;
    //链表所包含的节点数量
    unsigned long len;
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void (*free)(void *ptr);
    //节点值对比函数
    int (*match)(void *ptr,void *key);
}list;
  • 双端:链表节点带有prev和next指针,获取前置和后置节点的复杂度都是O(1)
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL
  • 头尾指针:将程序获取头尾节点的复杂度降为O(1)
  • 长度计数器:将程序获取表长的复杂度降为O(1)
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值
字典

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对

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

typedef struct dictEntry{
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

typedef struct dict{
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引
    //当rehash不在进行时,值为-1
    int rehashidx;
} dict;

typedef struct dictType{
    //计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);
    //复制键的函数
    void *(*keyDup)(void *privdata,const void *key)
    ...
}

在这里插入图片描述

哈希算法

  • Redis计算哈希值方法: hash=dict->type->hashFunction(key);
    计算索引值的方法:index=hash & dict->ht[x].sizemask;

解决键冲突

  • 当有两个或以上的键被分配到哈希表的同个索引,那么就发生了冲突
  • Redis使用链地址法来解决冲突,被分配到索引的多个节点使用链表连接
  • 为了提高速度,每次都是将新节点添加到链表的表头位置。

rehash

  • 服务器目前没有在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子>=1

    服务器正在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子>=5

  • 为字典ht[1]哈希表分配空间,大小取决于要执行的操作与ht[0]当前键值对的数量

    将保存在ht[0]中的所有键值对存放到ht[1]指定的位置

    当ht[0]的所有键值对都迁移完毕后,释放ht[0],并指向ht[1],并在ht[1]上创建一个空的哈希表,为下次rehash准备

跳跃表

Redis只有在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中作为内部数据结构。

typedef struct zskiplist{
    //表头节点和表尾节点
    structz zskiplistNode *header,* tail;
    //表中节点的数量
    unsigned long length;
    //表中层数最大的节点的层数
    int level;
} zskiplist;

typedef strct zskiplistNode{
    //后退指针
    struct zskiplistNode *backward;
    //分值
    double score;
    //成员对象
    robj *obj;
    //层
    struct zskiplistlevel{
        //前进指针
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    }level[];
} zskiplistNode;

在这里插入图片描述

整数集合

当一个集合只包含整数元素,并且元素不多时,Redis就会使用整数集合作为集合键的底层实现

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

升级

  • 根据新元素类型,扩展数组空间,为新元素分配空间
  • 将底层数组现有所有元素都转为新元素相同类型,并将类型转换后的元素放到正确位置
  • 将新元素添加到底层数组

整数集合不支持降低,一旦升级就不能降级

压缩列表

压缩列表是列表键哈希键底层实现之一

当一个列表键只包含少量列表项,且每个列表项要么是小整数,要么是长度比较短的字符串,Redis就使用压缩列表来做列表键的底层实现

在这里插入图片描述

压缩列表节点的组成

  • previous_entry_length记录压缩列表前一个节点的长度
    • 前一个节点的长度<254字节时,该属性只有2位,且前一节点的长度就保存在这两位
    • 前一个节点的长度>=254字节时,该属性有10位,且前两位表示这是一个5字节的长度,后8位表示前一个节点的长度
  • encoding记录了节点的content属性所保存数据类型长度高两位表示存储的是字节数组还是整数。
  • content存储节点的值。

连锁更新问题

对象

Redis没有直接使用前文的数据结构来实现键值对数据库,而是基于这些数据结构构建了一个对象系统,通过对象组织数据结构,包括字符串对象,列表对象,哈希对象,集合对象有序集合对象这5种对象

typedef struct redisObject{
    //类型
    unsigned type :4;
    //编码
    unsigned encoding:4;
    //指向底层实现数据结构的指针
    void *ptr;
    ...
} robj;

在这里插入图片描述

字符串对象

  • 如果字符串对象保存的是整数值,且这个数值可用long表示,底层就会以**REDIS_ENCODING_INT**编码来实现。

  • 如果字符串对象是一个字符串值,且这个字符串长度**>32字节**,字符串将使用一个SDS保存,底层编码为**REDIS_ENCODING_RAW**。

  • 如果字符串对象保存的是字符串,且这个字符串长度**<=32字节**,底层编码就是**REDIS_ENCODING_EMBSTR**,使用embstr编码的方式保存字符串。

  • embstr编码

    • embstr编码则通过调用一次内存分配函数来分配一块连续空间,空间依次包括redisObject和sdshdr俩结构
  • 编码的转换

    • int->raw:对int编码的字符串对象执行后,保存的不再是整数值,而是字符串值时。比如整数追加字符串。

    • embstr->raw:Redis没有为embstr编写修改程序,所以是只读的,当embstr编码的字符串修改后,就变成raw编码的字符串对象。

列表对象

  • 当列表可以同时满足以下两个条件时,列表对象使用ziplist编码:

    • 列表对象保存的所有字符串元素的长度都**<64字节**

    • 列表对象保存的元素数量**<512个**

  • 否则使用linkedlist编码

哈希对象

  • 当哈希对象可以同时满足下两个条件时,使用ziplist编码

    • 哈希对象保存的所有键值对的值和键都<64字节
    • 哈希对象保存的键值对数量**<512个**
  • 否则使用hashtable编码

集合对象

  • 当集合对象同时满足以下两个条件时,使用intset编码:

    • 集合对象保存的所有元素都是整数值

    • 集合对象保存的元素数量**<=512个**

  • 否则使用hashtable编码

有序集合对象

  • 当有序集合对象同时满足以下两条件时,对象使用ziplist编码:

    • 有序集合保存的元素数量**<128个**
    • 有序集合保存的所有元素成员的长度都<64字节
  • 否则使用skiplist编码,使用一个zset结构(同时包含ziplist和skiplist)

    • typedef struct zset{
          zskiplist *zsl;
          dict *dict;
      } zset;
      
    • dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:键保存元素,值保存分值

    • 通过字典以O(1)查找给定成员的分值。有序集合元素都是字符串对象,分值都是double类型浮点数

    • zset的跳跃表和字典通过指针来共享相同元素的成员和分值,不会浪费额外内存
      在这里插入图片描述

类型检查的实现

  • 类型检查的实现

    • 为了确保只有制定类型的键可以执行某些特定命令,在执行前,Redis会先通过RedisObject的type属性检查输入键的类型是否正确
  • 多态命令的实现

    • 根据值对象的编码方式,选择正确的命令实现代码来执行
  • 内存回收

    • 由于C语言没有内存回收机制,Redis在对象系统中构建了引用计数器技术实现内存回收机制。每个对象的引用计数器信息由redisObject的refcount来记录。当对象的引用计数值为0时,所占用的内存会被释放
  • 对象共享

    • 引用计数器还有共享对象的作用。如果两个不同键的值都一样(必须是整数值的字符串对象),则将数据库键的值指针指向一个现有的值对象,然后将被共享对象的引用计数加一。如果不是整数值的对象,则需要耗费大量的时间验证共享对象和目标对象是否相同,复杂度较高,消耗CPU时间,所以Redis不会共享包含字符串的对象
    • Redis在初始化服务时,会创建很多字符串对象,包含0~9999的整数(和Integer的常量池有点像),当需要时,就能直接复用
  • 对象的空转时长

    • redisObject还包含了lru属性,记录对象最后一个被命令程序访问的时间。object idletime命令可打印键的空转时长,就是当前时间减去lru时间计算得到的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值