Redis数据结构内部编码

試著忘記壹切 提交于 2020-02-27 06:00:02

整体说明

Redis的每一个键值都是使用一个redisObject结构体保存的:

typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;

type字段表示数据类型:

/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

encoding表示内部编码方式:

#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */

数据结构与编码方式对应:

字符串类型

redis使用sdshdr类型的变量来存储字符串,redisObject的ptr字段指向sdshdr的地址,sdshdr的定义如下:

struct sdshdr {
  int len;   //buf已占用的空间长度
  int free; //buf中剩余的空间长度
  char but[];  //数据 真实存储c字符串
}

比如执行set key "hello"时,内存结构如下:

当存储字符串时,实际占用空间就如上图所示,字符串值存储在sdshdr结构中。

如果值的内容可以用一个64位符号整数表示,redis将会把值转换成long,内存结构如下:

可以看出如果值是long,那么将不会放到sdshdr结构中。

另外,redis会事先建立10000个key,分别存储0到9999这些数字,如果值是0-9999,那么可以直接使用这10000个数字而不需在创建redisObject了。

当配置了maxmemory设置了redis的最大空间大小时,redis不会使用共享对象,因为对于每一个键值都要上会用一个redisObject来记录LRU信息。

当键值内容不超过39个字节时,reids会采用EMBSTR编码,这个编码的好处是把sdshdr结构与redisObject分配到连续的内存空间:

这么做的好处是不管分配存储还是释放内存,所需要的操作都从2次减少到1次。

当对EMBSTR编码的字符串执行修改操作时(如APPEND操作),Redis会将其转换为RAW编码。

散列类型

散列类型内部使用HT或者ZIPLIST编码。

两种数据结构的切换配置在:

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

当散列类型字段个数少于hash-max-ziplist-entries个,或者每个字段名和字段值的长度都小于hash-max-ziplist-value个字节,Redis会用ziplist编码存储数据,否则就使用ht存储数据,转换过程是透明的。

Redis的ZIPLIST,是一种紧凑的编码格式,它牺牲了部分读取性能换区极高的空间利用率,所以当entries在较少的个数并且value在较小的长度时使用。

具体实现不在此记录了,可以搜索相关文章。

列表类型

列表类型采用LINKEDLIST和ZIPLIST两种编码。

LINKEDLIST采用双向链表,链表中的元素都是redisObject存储的,此处与字符串优化方式相同。

两种数据结构的切换配置在:

list-max-ziplist-entries 512
list-max-ziplist-value 64

转换方式和Hash一样。

其实linkedlist已经趋向于淘汰了,新版本的redis提供了QUICKLIST编码方式:

将长列表分成若干个以链表形式组织的ziplist,达到减少空间占用的同时还可以提升ziplist编码性能的效果,具体实现可以在网上搜索,此处不记录了。

集合类型

内部实现方式是HT和INTSET。

当元素都是整型且元素的个数小于配置文件中set-max-intset-entries=512个时,Redis会使用INTSET,否则使用HT。

typedef struct intset {
    uint32_t encoding;  // 编码类型 int16_t、int32_t、int64_t
    uint32_t length;    // 长度 最大长度:2^32
    int8_t contents[];  // 柔性数组
} intset;

其中centents存储的就是集合中的元素,根据encoding不同,每个元素占用的大小也不同。默认是INT16(2字节),当新增加的整数无法用2字节存储时,Redis会将该集合的encoding升级到INT32(4字节),以此类推到INT64(8字节)。INTSET以有序的方式存储元素,所以可用二分法查找,但添加或删除元素都需要调整元素的内存位置,所以元素太多时性能不好,故定义了entries的个数限制。

INTSET转成HT后,不会在自动转回INTSET。

有序集合

内部实现方式是SKIPLIST和ZIPLIST。

两种数据结构的切换配置在:

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

当使用ZIPLIST时,

每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

当使用SKIPLIST时,有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表。

关于SKIPLIST,可以在网上搜索redis zset skiplist数据结构,在此处不做详细记录了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!