杂记-Redis五大数据类型的底层数据结构

714 阅读5分钟

Redis底层数据结构分析

截止目前(2020-11-19),Redis 一共有以下几种上层数据类型:

  • Strings
  • Lists
  • Hashes
  • Sets
  • Sorted Sets
  • Bitmaps
  • HyperLogLogs
  • Streams
  • Geo

最上面五种可谓是最经常使用的数据结构,每种又可能由多种更底层的数据结构实现。

Redis Strings

一个字符串的值不能超过 proto-max-bulk-len 大小,默认是512M(512ll*1024*1024),在 t_string.c#checkStringLength 定义了检查方法。

redis字符串的编码格式由三种:

  • OBJ_ENCODING_INT
  • OBJ_ENCODING_RAW
  • OBJ_ENCODING_EMBSTR

其中后面两种都是底层采用的是一个叫做 sdshdrx 定义的结构体(x 为字节数),这种结构体分别是:

  • sdshdr5 这个很特殊
  • sdshdr8
  • sdshdr16
  • sdshdr32
  • sdshdr64
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

小于32位长的字符用sdshdr5,flags低三位存类型,高五位存长度。当创建空字符的时候,sdshdr5会转成sdshdr8。

采用的规则,可以查看一下 object.c 中的 tryObjectEncoding 方法

OBJ_ENCODING_INT

我们看一下这段代码:

if (len <= 20 && string2l(s,len,&value)) {
    /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
    if ((server.maxmemory == 0 ||
         !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
        value >= 0 &&
        value < OBJ_SHARED_INTEGERS) // 10000
    {
        decrRefCount(o);
        incrRefCount(shared.integers[value]);
        return shared.integers[value];
    } else {
        if (o->encoding == OBJ_ENCODING_RAW) {
            sdsfree(o->ptr);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
            decrRefCount(o);
            return createStringObjectFromLongLongForValue(value);
        }
    }
}

对于大于0并且小于10000的数字:

set str 9999

redis 在启动的时候,会预先创建10000个 redisObject 存储 0-99999 ,所以如果你设置的值在这个范围内,redis不需要再额外创建内存,只需要把引用加1就好。

对于其它的整型,则会创建一个redisObj,编码为int的redis对象并返回。

OBJ_ENCODING_EMBSTR

当字符串长度小于等于44个字节的时候,redis 将使用embstr 编码字符串,这个时候,生成的redisObj和sdshdr是连续内存。

OBJ_ENCODING_RAW

其它情况使用 raw 格式编码,注意这里要申请两次内存,因为sdshdr和redisObj不在连续的内存中。

Redis Lists

列表的底层数据结构采用 OBJ_ENCODING_QUICKLIST 编码。

quicklist 是redis3.2引入的数据结构,它由 List(链表)ziplist 结合而成。

A doubly linked list of ziplists

在quicklist.c 第一行注释解释了quicklist 到底是什么东西,顾名思义:quicklist 是一个双向链表,链表中的每个节点都是一个ziplist结构。

quicklist可以看成是用双向链表将若干小型的ziplist连接到一起组成的一种数据结构。当ziplist节点个数过多,quicklist退化为双向链表,一个极端的情况就是每个ziplist节点只包含一个entry,即只有一个元素。当ziplist元素个数过少时,quicklist可退化为ziplist,一种极端的情况就是quicklist中只有一个ziplist节点。

Redis Hashes

散列表底层数据结构采用 ziplistht(hashtable) 两种中的一种。

当需要存储的key-value都满足下面两个条件时,采用 ziplist 作为底层存储,否则需要转为 hashtable 存储,需要注意的是,ziplist 的存储顺序和写入顺序是一致的,而hashtable则不是。

  1. key-value 结构的所有键值对的字符串长度都小于 hash-max-ziplist-value,默认值是64,可以通过配置文件进行修改
  2. 散列表对象保存的键值对个数(一个键值对记为一个)小于 hash-max-ziplist-entries,默认值是512,可以通过配置文件修改

当数据结构从ziplist转为散列表时,如果后面满足ziplist的条件,也不会再转换回来。

Redis Sets

无序集合,集合成员唯一。

set 的底层基于 htinset 两种数据结构中的一种。

整数集合(inset)是一个有序的、存储整型数据的结构。

当集合数量超过 set-max-inset-entries(默认是512),即使往集合里面继续放整型元素,集合也会将编码转换位hashtable。

Redis Sorted Sets

Zset 底层数据结构采用 ziplist 或者 skiplist 两种中的一种。

redis 的配置文件中关于有序集合的两个配置:

  • zset-max-ziplist-entries 128:zset 采用压缩列表时,元素个数最大值位128
  • zset-max-ziplist-value 64:zset 采用压缩列表时,元素最大长度不能超过64

t_zset.c 的 zaddGenericCommand 函数中,如果是第一次添加元素,会判断下面两个条件:

  • zset-max-ziplist-entries 是否等于0
  • 插入元素大小是否大于 zset-max-ziplist-value

if (zobj == NULL) {
    if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
    if (server.zset_max_ziplist_entries == 0 ||
        server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
    {
        zobj = createZsetObject();
    } else {
        zobj = createZsetZiplistObject();
    }
    dbAdd(c->db,key,zobj);
}

如果满足其中一个,则zset使用 OBJ_ENCODING_SKIPLIST 编码,否则采用 OBJ_ENCODING_ZIPLIST 作为底层数据结构。

一般情况下,不会将zset-max-ziplist-entries配置成0,元素的字符串长度也不会太长,所以在创建有序集合时,默认使用压缩列表的底层实现。

zset新插入元素时,会判断以下两种条件:

  • zset中元素个数大于zset_max_ziplist_entries
  • 插入元素的字符串长度大于zset_max_ziplist_value

当满足任一条件时,Redis便会将zset的底层实现由压缩列表转为跳跃表。代码如下:

if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries || sdslen(ele) > server.zset_max_ziplist_value)
    zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);

zset在转为跳表之后,即使元素被逐渐删除,也不会重新转为压缩列表。