结构体

RedisObject 是封装键值对的基本单元,每个对象都可以关联不同的底层数据结构体,这样设计的好处是可以根据具体场景,灵活使用合适的数据结构,此外,通过统一的对象结构体,Redis 还实现了通过引用计数共享对象、LRU/LFU 淘汰数据的功能。

redisObject 的基本结构定义在https://github.com/redis/redis/blob/7.2.4/src/server.h#L899

struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS;
    int refcount;
    void *ptr;
};

redisObject->type

type 字段就是经常使用的数据库中键值对的“类型”,如常说的“字符串键”,“哈希键”等,源码中具体的枚举值和含义:

  • OBJ_STRING:值为0,表示字符串类型对象;
  • OBJ_LIST:值为1,表示列表类型对象;
  • OBJ_SET:值为2,表示集合类型对象;
  • OBJ_ZSET:值为3,表示有序集合类型对象;
  • OBJ_HASH:值为4,表示哈希类型对象

redisObject->encoding

encoding 为编码方式,表示当前对象具体使用哪种底层的数据结构,可能的枚举值和含义:

  • OBJ_ENCODING_RAW:表示当前对象指向的内容是一个普通的 SDS

  • OBJ_ENCODING_INT:表示当前对象指针指向的是一个整数值

Redis 服务器启动时候,会默认创建从 0~OBJ_SHARED_INTEGERS=10000 个整数编码的对象,这些对象可以通过引用计数的方式实现复用共享。

  • OBJ_ENCODING_EMBSTR:表示使用的是嵌入式的SDS字符串

嵌入式的 SDS 字符串指的是 SDS 数据在内存中和 redisObject 紧密排列在一段连续的内存当中,普通 raw 编码的 redisObject 指向的 SDS 需要分别经过2次内存分配,embstr 编码的 redisObject 创建时候直接分配一块连续的内存空间,可以快速通过指针的前进后退定位到相邻的 SDS 内容。embstr 编码的 SDS 通常是只读状态的,一但对其内容进行修改,redis 会自动将其编码格式从 embstr 转换为 RAW。

  • OBJ_ENCODING_HT:表示当前对象指向的是一个字典

  • OBJ_ENCODING_ZIPMAP:旧有编码格式,不再使用

  • OBJ_ENCODING_LINKEDLIST:旧的编码格式,指使用链表底层编码,redis 7 以后不再使用

  • OBJ_ENCODING_ZIPLIST:表示使用压缩列表数据结构,redis 7 不再使用

  • OBJ_ENCODING_INTSET:表示指向一个整数集合数据结构

  • OBJ_ENCODING_SKIPLIST:表示指向一个跳跃表结构

  • OBJ_ENCODING_QUICKLIST:表示指向了一个快速列表数据结构

  • OBJ_ENCODING_LISTPACK:表示指向的是一个紧凑列表

type 和 encoding 对应关系

每种 type 会在底层灵活使用多种不同的 encoding 编码方式,通用的一个规律是:当需要保存的元素占用空间比较小或者子元素的数量比较少的时候,为了不浪费内存空间,redis 会自动选择更加节省内存的编码方式(如:ziplist 或者 listpack)来存放数据,当要存放的数据量比较大的时候,这时候读写效率的优先级就高于空间占用了,相应的,Redis 又会使用对读写比较友好的编码格式来存储数据了。

目前最新的 Redis 7.2.4 版本(https://github.com/redis/redis/tree/7.2.4)中不同 redisObject 和可能会用到的底层数据结构的对应关系如图:

redis-object-type-encoding

具体的,object->typeobject->encoding 的对应关系为:

  1. type=OBJ_STRING

字符串类型的 redis 对象使用的底层编码方式可能出现三种情况:

  • encoding=OBJ_ENCODING_INT:指向的内容是较小的整数时候使用
  • encoding=OBJ_ENCODING_EMBSTR:需要保存长度较短的字符串(不超过OBJ_ENCODING_EMBSTR_SIZE_LIMIT=44字节),且没有被修改时候
  • encoding=OBJ_ENCODING_RAW:其他情况都使用普通 SDS 存储
  1. type=OBJ_LIST
  • encoding=OBJ_ENCODING_LINKEDLIST:较早期版本中可能会使用双端链表实现,现在已经废弃不再使用;

  • encoding=OBJ_ENCODING_ZIPLIST:redis <=6.2 版本中,列表中元素较少的时候使用压缩列表来实现

  • encoding=OBJ_ENCODING_LISTPACK:redis >=7.0 版本开始,列表中元素较小时候使用紧凑列表来实现

  • encoding=OBJ_ENCODING_QUICKLIST:列表中元素较多时候使用快速列表来实现

  1. type=OBJ_SET
  • encoding=OBJ_ENCODING_INTSET:集合中的元素都是较小的整数,并且集合元素数量较少时候使用整数集合实现

  • encoding=OBJ_ENCODING_LISTPACK:从 Redis >= 7.2 版本开始,集合中元素数量较小的时候使用紧凑列表来存储集合键的数据

  • encoding=OBJ_ENCODING_HT:集合中元素较多的时候使用字典来储存数据

  1. type=OBJ_ZSET
  • encoding=OBJ_ENCODING_ZIPLIST:在 Redis <= 6.2 以下的版本,有序集合中元素较少时候使用压缩列表来实现

  • encoding=OBJ_ENCODING_LISTPACK:在 Redis >= 7.0 以上的版本中,有序集合中元素较少的时候使用紧凑列表来存储集合中的元素

  • encoding=OBJ_ENCODING_SKIPLIST:有序集合中元素较多的时候使用跳跃表来存储元素数据,但 redisObject->ptr 并不是直接指向跳跃表结构体,而是指向一个单独的 zset 结构体:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

zset 结构体中的 dict 用来存储有序集合键中成员 member 和分值 score 的对应关系,跳跃表 zsl 中按照分值 score 的顺序存放了各个成员 member 的值。

之所以单独设计一个 zset 结构体来挂载跳跃表结构,是为了提高有序集合键的查询效率,如果单独使用跳跃表来查询数据,时间复杂度为平均O(logN),最坏可能达到 O(N) 级别,通过单独保存的字典结构,可以快速从哈希表中获取到成员对应的分值数据,查询复杂度降为了 O(1) 级别,当然,单独记录哈希表又会占用更多的内存空间,这其实体现了 redis “以空间换时间”的思想。

  1. type=OBJ_HASH
  • encoding=OBJ_ENCODING_ZIPMAP:较早期的版本中使用,现在已经废弃

  • encoding=OBJ_ENCODING_ZIPLIST:在 Redis <= 6.2 以下的版本中,哈希键中的元素较少的时候,使用压缩列表存储数据

  • encoding=OBJ_ENCODING_LISTPACK:从 Redis >= 7.0 版本开始,哈希键中元素较少的时候,使用紧凑列表存储数据

  • encoding=OBJ_ENCODING_HT:哈希键中元素较多时候,使用字典数据结构来存放数据

redisObject->lru

redis 对象中的 lru 字段是固定占用 24bit 长度内存,分别记录了对象最近使用的时间戳或者最近访问的频次数据。

  • 当 redis 被设置启用了 maxmemory并且内存淘汰策略设置成 LRU 相关策略的时候,lru 字段记录的是一个24bit的最近访问的毫秒时间戳;

  • 当 redis 设置的内存淘汰策略是 LFU 相关策略,lru 字段前16bit记录了以分钟为最小精度的最近一次访问的时间戳,后8bit记录了根据特定算法计算出来的当前对象访问的频次值

lru 字段在 redis 内存超额时候,按照特定 LRU 或者 LFU 策略对数据库中键值对进行淘汰时候会使用或者更新。

redisObject->refcount

refcount 是当前 redis 对象的引用次数,redis 通过 refcount 字段来实现对象的回收和共享。

内存回收

当 redisObject 不再被使用时候,redis 对 redisObject->refcount 每次执行减1操作,当 refcount=0 时候会自动释放内存:

void decrRefCount(robj *o) {
    if (o->refcount == 1) {
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
    }
}

对象共享

上文提到,redis 服务在启动阶段,会创建足够多的 encoding=REDIS_ENCODING_INT 编码的的对象,这样,后续如果有整数数据需要被封装成对象时候,可以直接重复指向这些已经定义好的“共享对象”。

共享对象的 refcount 在创建阶段被设置为了 OBJ_SHARED_REFCOUNT,并且后续在执行内存回收时候,不会变更共享对象的引用计数的值。