Redis数据结构和编码

支持的数据结构

String/Hash/Set/Zset/List

RedisObject 核心对象

image.png
image.png
image.png
/*
 * Redis 对象
 */
typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码方式
    unsigned encoding:4;

    // LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
    unsigned lru:LRU_BITS; // LRU_BITS: 24

    // 引用计数
    int refcount;

    // 指向底层数据结构实例
    void *ptr;

} robj;
对象共享机制

redis会将常见的值放入一个共享对象中,避免了程序重新分配的麻烦,类似于jvm中的常量池。
预分配的对象如下:

  1. 各种命令的返回值,如成功时返回的OK,错误的ERROR,命令入队事物时返回的QUEUE等。
  2. 包括0在内,小于REDIS_SHARED_INTEGERS(默认为10000)的所有整数。
    注意:共享对象只能被字典和双向链表这种带有指针的数据结构引用。
引用计数和对象的销毁

redis内的refcount,如果为0,则表示可以回收。

  1. refcount表示这个对象被引用了多少次
  2. 当第一次被创建时,refcount为1
  3. 当对这个对象进行共享时,refcount+1
  4. 当使用完对象或引用被消除时,refcount-1
  5. 当refcount为0时,这个redisobject和引用的内存结构会被消除

SDS字符串

Redis3.2之前

typedef char *sds
struct sdschar {
    // buf[] 中已使用的字节数
    int len;
    // buf[] 中未使用的字节数
    int free;
    // 字符数组,用于实际存储字符串内容
    char buf[];
}
image.png

Redis3.2之后

image.png

  1. 杜绝缓冲区溢出
    1. 通过len来获取字符串的长度,而不是空字符来决定结尾,在内存重分配下,杜绝缓冲区溢出的情况。
  2. 获取字符串的长度是O(1)复杂度
    1. 通过len来获取长度,而C中是通过遍历得来,是O(n)复杂度。
  3. 减少修改字符串时,带来的内存重分配次数
    1. 在修改的过程中,通过len和free来确定空间大小长度,在内存空间够用的情况下,可以减少内存重分配。
  4. 惰性空间释放
    1. SDS提供了API来进行空间释放,对于空闲空间,可以在有需要的情况下进行释放。
  5. 二进制安全
    1. C中不能保存空字符,会被认为是字符串的结尾,这种限制使得C只能保存文本数据,而不能保存图片/音频/视频等压缩的二进制。
    2. 但是Redis中不是通过空字符,而是通过len来判断字符串长度,所以不存在这个问题。
  6. 兼容部分C的函数
    1. 虽然SDS的API都是二进制安全的,但是依然遵循C中以空字符结尾的规定。
image.png

ZipList 压缩表

整体存储格式:


image.png
  • zlbytes: unint32_t,存储的是整个ziplist占用的字节数
  • zltail: unint32_t,指最后一个entry的偏移量,为了快速完成定位,实现pop等操作
  • zllen: unint16_t,指整个ziplist内entry的数量,最大值为65535,如果entry的数量超过了最大值限制,那么该值会固定为65535,获取全部entry数量则需要通过遍历来获得
  • zlend: 1byte,终止字节,为0xFF,ziplist保证在任何情况下,entry的首字节都不会为0xFF
entry结构
  1. 一般结构

    • prevlen: 前一个entry的大小
      1. 当前一个entry的长度小于254时,prevlen为1个字节。
      2. 当大于254时,为5个字节,且第一个字节设置为254,后4个字节表示前一个字节的长度。
    • encoding: 编码格式
      1. 前2位表示类型,为11时,表示存储的是int,此时encoding的后几位存储的是int的数据。
      2. 不为11时,存储的是string,此时encoding的后几位表示的是string的字符串长度。
    • entry-data: 用于存储entry的数据
  2. 特殊结构

    • 当encoding为int时,encoding和entry-data合并,都在encoding中展示,为了节省空间。
为什么ziplist特别省内存
  1. 对于数组,每个元素占用的内存是一致的,取决于最大的元素,需要预留空间。
  2. ziplist尽量细化每个元素,通过encoding的格式,按照每个元素实际的大小来存储。
  3. 在遍历时如何定位元素,为了解决遍历,增加记录上个元素的length,所以增加了prevlen。
ziplist的缺点
  1. ziplist不预留内存空间,每次操作都会重新申请内存空间,每次删除也会立即缩容。
  2. 节点如果扩容,会导致entry的长度增加,在极端情况下,会导致prevlen的长度从1个字节变成5个字节,且导致链式反应,每个entry都会需要进行prevlen的扩容。

QuickList 快表 (Redis3.2之后)

image.png
  1. 基于ziplist和双向链表2种数据结构融合而成的数据结构
  2. 最外层是双向链表,Node节点相互连接,Node内的数据指针指向ziplist
  3. quicklist内单节点最大存储为8kb
  4. 实现了基于lzf算法的压缩api,将中间不常用的节点进行压缩来节省内存

Dict 字典/哈希表

image.png
  1. 当发生hash冲突是,通过外链法解决冲突

  2. 内部含有2个hash表,默认使用hash[0],当达到需要扩容的阶段时,进行rehash,copy到hash[1]中。

  3. rehash是渐进式rehash,每次操作数据时,将对应数据copy到hash[1]中,并清除hash[0]中的数据;直到所有数据均copy到hash[1]中,若有些数据一直未被访问,则采用定时进行迁移。

  4. 触发扩容条件:
    哈希表的负载因子计算:负载因子 = 哈希表已保存节点数量 / 哈希表大小
    load_factor = ht[0].used / ht[0].size

    1、服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1。

    2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5。

  5. 触发缩容条件:
    负载因子小于0.1

IntSet 整数集

Redis在存储集合时,如果集合内只包含整数且数目较少时,会采用IntSet来存储。

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
  • encoding:表示编码方式,取值有3个:INTSET_ENC_INT16, INTSET_ENC_INT32, INTSET_ENC_INT64
  • length: 存储的整数的个数
  • contents: 实际指向的存储数据的内存区域,虽然定义为int8_t,但实际以encoding为准
整数集合的升级

在int16类型的集合内插入一个int32的数据:

  1. 根据新元素的数据类型,对内存空间进行扩容,分配为按照int32来分配的内存空间
  2. 将底层原始数组的数据类型转换为新的长度,并迁移到新分配的内存空间中
  3. 改变encoding的值,并length+1

当最大元素删除后,是否需要降级?
不会,为了减少开销

ZipList 跳表

数据结构

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
skiplist和平衡树的对比
  1. skiplist更适用于范围查找,只需要在底层链表中进行遍历查找;而平衡树需要重新进行中序查找。
  2. skiplist和平衡树的查询复杂度都是O(logn),但是实现起来更简单。
  3. 在节点插入变更时,skiplist更为简单一些,只需要插入节点并修改相邻指针,而平衡树在节点更新时会需要更多的操作,阻塞时间也会更长。

Redis的对象和编码格式的关系

String支持3种编码格式 -> RAW / INT / EMBSTR
  • RAW

    1. 长度大于44字节的字符串
    2. 新增时会生成2个对象,redisObject和SDS,指针指向SDS,所以需要2块内存空间。
  • INT

    1. 可以保存为long表示的整数值
    2. 新增时是1块内存空间
  • EMBSTR

    1. 长度小于44字节的字符串
    2. 新增时分配1块内存空间,redisObject和SDS是连续的。但是当字符串需要修改时,只能销毁重新分配,所以EMBSTR是只读的。
    3. 因为在修改时只能销毁并重新分配,所以修改一定会变更为RAW类型,无论是否到达44字节。

ps: redis对于浮点数类型也是作为字符串保存的,在需要的时候再转换为浮点数类型

List -> quickList

从目前的版本(6.0)来看,List仅支持quickList(之前的版本有linked和ziplist这2种编码)。

Hash -> ziplist/hashtable(dict)
image.png

当同时满足以下2个条件时,使用ziplist:

  1. 列表保存的元素小于512个(ps:可以通过配置修改)
  2. 每个元素长度小于64字节
Set -> intset/hashtable(dict)
image.png
image.png

当同时满足以下2个条件时,使用intset

  1. 集合中所有元素都是整数
  2. 集合对象中所有元素不超过512个(ps:可以通过配置set-max-intset-entries修改)
zSet -> zipList/dict&skipList

tips:对于skipList的编码格式,其实是同时采用了dict和skiplist的数据结构来存储
说明:有序集合本身使用dict或skiplist其中一种都可以实现,如果单独使用dict来实现,那么查找可以达到O(1)的复杂度,但是每次范围查询排序需要进行额外操作;如果单独使用skiplist,虽然可以使用范围操作,但是查找复杂度却是O(logn),所以redis采用了2种数据结构混合。但虽然同时使用了2种数据结构,但数据其实只有1份,通过指针指向到对应地址。

image.png

当同时满足以下2个条件,使用ziplist编码:

  1. 保存的元素数量小于128个(可配置)
  2. 保存的元素长度都小于64个(可配置)

参考文章:https://www.pdai.tech/md/db/nosql-redis/db-redis-x-redis-ds.html

你可能感兴趣的:(Redis数据结构和编码)