Redis数据结构

常用数据结构

简单动态字符串(SDS)

  • 结构:

    struct sdshdr{
        // 记录buf中已使用的字节数量,也是字符串的长度
        int len;
        // 记录buf数组中未使用字节的数量
        int free;
        // 字节数组,用于保存字符串
        char buf[];
    };
    
  • 相较于C字符串的优势:

    1. 常数复杂度获取字符串的长度
    2. 杜绝缓存区溢出:如两个C字符串连接时,可能由于空间不够会出现内存溢出,但是SDS会自动检查free是否足够,并自动扩展空间
    3. 减少修改字符串时带来的内存重分配次数:无需每次修改字符串都重新修改内存大小,通过空间预分配和惰性空间释放两种策略进行优化
      • 空间预分配:当大小小于1M时,free空间大小分配为len长度大小,当大小大于1M时,free空间分配为1M,只有free无法满足扩展的大小时会进行重新分配大小
      • 惰性空间释放:缩短字符串后,不会释放空间,除非调用API,进行释放
    4. 二进制安全:buf中存储的是通过二进制转换的数据
    5. 兼容部分C字符串函数:由于buf最后以空字符串结尾,所以能够复用C字符串的函数

链表

  • 结构

    • 链表节点:

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

      typedef struct list {
          // 表头节点
          struct listNode * prev;
          // 表尾节点
          struct listNode * next;
          // 链表所包含的节点数量
          unsigned long len;
          // 节点值复制函数
          void *(*dup)(void *ptr);
          // 节点值释放函数
          void (*free)(void *ptr);
          // 节点值对比函数
          int (*match)(void *ptr,void *key);
      }list;
      
  • 特性:

    • 双端:链表节点往前和往后遍历都为O(1)
    • 无环:表头的prev和表尾的tail都指向null
    • 带表头表尾:表头表尾获取为O(1)
    • 链表长度计数:长度获取为O(1)
    • 多态:值可以存储为任意类型

字典

  • 结构:

    • 字典:

      typedef struct dict {
          // 类型特定函数
          dictType *type;
          // 私有数据
          void *privdata;
          // 哈希表,有两个是为了进行rehash操作
          dictht ht[2];
          // rehash 索引
          //当rehash,不在进行时,值为-1
          in trehashidx; /* rehashing not in progress if rehashidx == -1 */
      } dict;
      
    • 哈希表:

    typedef struct dictht {
        // 哈希表数组
        dictEntry **table;
        // 哈希表大小
        unsigned long size;
        // 哈希表大小掩码,用于计算索引值,总是等于size-1,key的hash值与该值进行与操作,从而计算出索引位置
        unsigned long sizemask;
        // 该哈希表已有节点的数量
        unsigned long used;
    } dictht;
    
    • 哈希表节点:

      typedef struct dictEntry {
          // 键
          void *key;
          // 值
          union{
              // 存指针
              void *val;
              // uint64整数
              uint64_tu64;
              // int64整数
              int64_ts64;
          } v;
          // 指向下个哈希表节点,形成链表,当有hash冲突时,新节点总是加入到表头的位置,从而保证插入的复杂度为O(1)
          struct dictEntry *next;
      } dictEntry;
      
  • 特点:

    • 每个字典有两个哈希表,一个用来存储数据,一个用来rehash使用

    • 解决hash冲突的方法使用单向链表解决

    • hash表扩展和收缩时使用渐进式的方式进行rehash

      • rehash触发条件:

        1. 服务器没有执行BGSAVE或者BGREWREITEAOF命令时,负载因子大于等于1

        2. 服务器在执行BGSAVE或者BGREWREITEAOF命令时,负载因子大于等于5

        3. 当哈希表负载因素小于0.1时,会执行搜索操作

          注:1. 负载因子 = 哈希表已保存数量 / 哈希表大小

          ​ 2. 是否在执行BGSAVE或者BGREWREITEAOF命令时,负载因子的大小不一样的原因是,在执行BGSAVE或者BGREWREITEAOF命令时,扩大负载因子,从而减少rehash,从而为执行命令节约内存

      • 渐进式rehash:

        • 将ht[0]拷贝ht[1]的动作均摊到每一次对hash表的添加、删除、查找或者更新操作上,从而避免集中式的庞大计算量,每拷贝ht[0]的一个索引,都会在rehashidx上进行记录拷贝的位置
        • 添加只会往ht[1]添加,删除、查找或更新则会先在ht[0]操作,若没有再到ht[1]操作
        • rehash完成后,ht[0]和ht[1]的引用对调,rehash索引变为-1

跳跃表

  • 结构:

    • 跳跃表结构:

      typedef struct zskiplist {
          // 表头节点和表尾节点
          structz skiplistNode *header, *tail;
          // 表中节点的数量
          unsigned long length;
          // 表中层数最大的节点的层数
              int level;
      } zskiplist;
      
    • 跳跃表节点结构:

      typedef struct zskiplistNode {
          // 层 每次都会随机生成一个1-32之间的值作为level数组的大小,也就是层的高度,生成的算法是志越大生成的概率越小
          struct zskiplistLevel {
              // 前进指针
              struct zskiplistNode *forward;
              // 跨度
              unsigned int span;
          } level[];
          // 后退指针
          struct zskiplistNode *backward;
          // 分值
          double score;
          // 成员对象
          robj *obj;
      } zskiplistNode;
      
  • 特点:

    • 相较于红黑树的缺点:占用更多的内存,每个节点的大小取决于节点的层数
    • 相较于红黑树的优点:
      1. 实现比红黑树简单
      2. 比红黑树更容易扩展
      3. 高并发时,红黑树需要旋转附近节点,需要加锁,跳跃表不需要考虑
  • 操作:

    • 根据分值查询:从最上层节点进行查询,当发现分值小于后续节点时,返回前一节点由次之的层级再次进行查询
    • 根据排名查询:从最上层节点进行查询,当跨度大于排名时,返回前一节点由次之的层级再次查询,直至跨度累计的和等于排名
    • 插入:
      1. 先根据分值查询插入的位置,查询时将每一层级到该节点的跨度rank[]和每一层级上一节点的引用update[]进行记录
      2. 随机生成该节点的层数,并创建节点
      3. 根据update[]插入节点,并对跨度信息进行更新
    • 删除:
      1. 先根据分值查询插入的位置,查询时将每一层级到该节点上一节点的引用update[]进行记录
      2. 将引用引向后续节点,跳过要删除的节点
      3. 释放删除节点的内存
    • 更新:
      • 若没有更新排名:修改节点的数据
      • 若更新排名:先删除,再插入

Redis3.2源码分析-跳跃表zskiplist

整数集合

  • 结构:

    typedef struct intset {
        // 编码方式,数组中长度最大的元素决定编码方式
        uint32_t encoding;
        // 集合包含的元素数量
        uint32_t length;
        // 保存元素的数组,不重复,有顺序地存储在数组中
        int8_t contents[];
    } intset;
    
  • 特点:

    • 支持升级:
      • 好处:提高灵活性,节约空间
      • 升级步骤:
        1. 内存空间先扩大
        2. 原有元素从大到小依次分配到新的空间位置
        3. 新元素分配到数组位置中(新元素最后加入的原因是,新元素导致升级,所以长度最长,所以新元素是最值)
    • 不支持降级

压缩列表

  • 结构:
    • 压缩列表结构:
      • zlbytes:记录压缩列表占用的内存大小。内存分配和计算zlend时使用
      • zltail:记录表尾节点距离压缩列表起始位置的字节数量,从而能够直接找到表尾节点
      • zlen:压缩列表的节点数量,当大于65535时,只能遍历求出数量
      • entryx:长度不定,会存在多个元素,记录节点数据,数据为字节数组或整数值
      • zlend:特殊值0xFF,用于标记是压缩列表的表尾位置
    • 压缩列表节点结构:
      • previous_entry_length:前一个节点的字节长度,用于从表尾向前遍历,找到指针
      • encoding:保存数据的类型和长度
      • content:保存数据,可以是整数或字节数组
  • 特点:
    • 压缩列表相较于链表,能够更好地节约内存空间
    • 遍历效率相对较低
    • 添加和删除节点会出现连锁更新的情况,但概率较低

对象

对象概述

  • 结构:

    typedef struct redisObject {
        // 类型,主要分为字符串对象、列表对象、哈希对象、集合对象和有序集合对象
        unsigned type:4;
        // 编码,主要分为 INT、EMBSTR、RAW、HT、LINKEDLIST、ZIPLIST、INTSET、SKIPLIST
        unsigned encoding:4;
        // 指向底层实现数据结构的指针
        void *ptr;
        // 对象的空转时长,用于计算对象多久没有被访问,当开启相关配置时,会先回收空转时长较长的对象
        unsigned lru:22} robj;
    
  • 特点:

    • redis的对象采用引用计数实现内存回收机制,当一个对象不被使用时,该对象的内存会自动释放
    • redis会共享0-9999的字符串对象

字符串对象

encoding区别转换
int存储整数数字操作时,直接操作,字符串相关操作时,先转换为raw,再进行操作
raw存储长度大于32字节的字符串数字操作时,看是否能转换为数字进行操作,不能返回错误提示直接操作,字符串相关操作时,直接操作
embstr存储长度小于等于32字节的字符串,相对于raw,内存分配和释放相对于raw的两次只需一次,原因是redisObject和sdshdr存储在连续的内存中数字操作时,看是否能转换为数字进行操作,不能返回错误提示直接操作,字符串相关操作时,转换为raw,再进行操作

列表对象

encoding区别转换
ziplist节约内存空间同时满足以下条件,使用ziplist,不满足使用linkedlist:1.列表存储的元素数量小于5等于12个;2.每个字符串元素的长度小于等于64字节(上限值可修改)
linkedlist插入、删除和遍历效率较高同上

哈希对象

encoding区别转换
ziplist节约内存空间同时满足以下条件,使用ziplist,不满足使用hashtable:1.哈希对象存储的键值对数量小于等于512个;2.每个键和值的字符串的长度小于等于64字节(上限值可修改)
hashtable插入、删除和查询效率较高同上

集合对象

encoding区别转换
intset存储整数,节约空间同时满足以下条件,使用intset,不满足使用hashtable:1.哈希对象存储的键值对数量小于等于512个;2.存储元素都是整数值(上限值可修改)
hashtable存储整数或字符串同上

有序集合对象

encoding区别转换
ziplist每个集合元素使用两个紧挨着的压缩列表节点存储,第一个节点保存元素成员,第二个节点保存分值,节约空间同时满足以下条件,使用ziplist,不满足使用skiplist:1.元素数量小于等于128个;2.存储元素的所有成员小于64字节(上限值可修改)
skiplist实际使用zset结构保存,一个zset包含一个字典和一个跳跃表,且由于字典和跳跃表的元素引用指向共用同一地址,所以不会浪费额外内存同上
  • zset结构:

    typedef struct zset {
        // 跳表保证范围查询的效率,平均O(logN),最坏O(N)
        zskiplist *zsl;
        // 字典保证定位元素分值大小的效率为O(1)
        dict *dict;
    } zset;
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值