Redis系列之RedisObject

2,572 阅读4分钟

简介

redis 是 key-value 存储系统,其中 key 类型一般为字符串,而 value 类型则为 redis 对象(redis object)。

每次当我们在redis数据库新创建一个值键对时,至少会创建两个对象,一个用于作为建对象,一个用于作为值对象。

redis每个对象都由一个redisObject的结构表示,该结构中与保存数据相关的三个属性分别是:type,encoding,ptr

/*
 * Redis 对象
 */
typedef struct redisObject {
 
    // 类型
    unsigned type:4;
 
    // 对齐位
    unsigned notused:2;
 
    // 编码方式
    unsigned encoding:4;
 
    // LRU 时间(相对于 server.lruclock)
    unsigned lru:22;
 
    // 引用计数
    int refcount;
 
    // 指向对象的值
    void *ptr;
 
}

编码和底层实现

RedisObject中的ptr属性指向对象底层实现的数据结构,encoding属性决定了数据结构的定义。

redis含有以下几种encoding

类型编码对象
REDIS_STRINGREDIS_ENCODING_INT使用整数值实现的字符串对象
REDIS_STRINGREDIS_ENCODING_RAW使用简单动态字符串实现的字符串对象
REDIS_STRINGREDIS_ENCODING_EMBSTR使用embstr编码的简单动态字符串实现的字符串对象
REDIS_LISTREDIS_ENCODING_ZIPLIST使用压缩列表实现的列表对象
REDIS_LISTREDIS_ENCODING_LINKENDLIST使用双端链表实现的列表对象
REDIS_HASHREDIS_ENCODING_ZIPLIST使用压缩列表实现的哈希对象
REDIS_HASHREDIS_ENCODING_HT使用字典实现的哈希对象
REDIS_SETREDIS_ENCODING_INTSET使用整数集合实现的集合对象
REDIS_SETREDIS_ENCODING_HT使用字典实现的集合对象
REDIS_ZSETREDIS_ENCODING_ZIPLIST使用压缩链表实现的有序集合对象
REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳跃列表和字典实现的有序集合对象

回收方式

redis的对象采用的是引用计数法,进行标记引用使用,当引用为0时,会将该对象销毁。当需要增加或减少引用时,必须调用相应的函数。如果基于redis进行二次开发,程序员必须遵守该准则。

// 增加 Redis 对象引用
void incrRefCount(robj *o) {
    o->refcount++;
    }
    // 减少 Redis 对象引用。特别的,引用为零的时候会销毁对象
void decrRefCount(robj *o) {
    if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
    // 如果取消的是最后一个引用,则释放资源
    if (o->refcount == 1) {
    // 不同数据类型,销毁操作不同
    switch(o->type) {
        case REDIS_STRING: freeStringObject(o); break;
        case REDIS_LIST: freeListObject(o); break;
        case REDIS_SET: freeSetObject(o); break;
        case REDIS_ZSET: freeZsetObject(o); break;
        case REDIS_HASH: freeHashObject(o); break;
        default: redisPanic("Unknown object type"); break;
    }
    zfree(o);
  } else {
      o->refcount--;
  }
}

类型检测实现

对象的属性type,可以用在实现类型检测。

当客户端发送命令时,服务端会根据输入键的值对象的type类型是否为执行命令所需的类型,如果是则对键执行指定的命令。

否则,服务器将拒绝执行指定命令,并返回一个类型错误。

多态命令的实现

redis中存在两种多态命令:

  1. 类型多态命令:如EXPIRE、DEL、TYPE,不管键的值属于哪种类型,都要保证该执行执行正确
  2. 编码多态命令:如都是REDIS_LIST的TYPE,但是编码形式有可能是REDIS_ENCOIDNG_ZIPLIST也有可能是REDIS_ENCODING_LINKEDLIST,输入LLEN命令时,针对不同编码,需要选择该编码下的正确的命令实现。

对象共享

对象的属性refcount,除了用于实现引用计数外,还能用来实现整数对象共享的功能。会共享0~9999的字符串对象。

当键A跟键B的值都是一个整数100的对象时,redis不会创建两个新对象,而是创建一个整数100的对象,将refcount设置为2,表示有两个引用,该对象同时被键A和键B共享。数据库中保存相同值的对象越多,对象共享机制就能节约更多的内存。

为什么redis不共享非整数型对象

只有在共享对象跟目标对象完全相同的情况下,程序才会将共享对象作为键的值对象,而一个共享对象保存的数据越复杂,验证共享对象跟目标对象完全相同的过程复杂度就越高,消耗的cpu资源也越多。

  • 如果共享对象是保存整数型的字符串对象,验证复杂度为O(1)
  • 如果共享对象是保存字符串值的字符串对象,验证复杂度为O(n)
  • 如果共享对象时包含多个值的对象,比如哈希对象、列表对象,验证复杂度为O(n^2)

因此,尽管存储更复杂的对象能节约更多的内存,考虑到cpu时间的限制,redis只对保存整数值对象进行共享。

空转时长

RedisObject中属性lru记录了最后一次被程序访问的时间。

OBJECT IDELTIME命令可以打印出给定键的空转时长,其实就是通过当前时间减去键值对象lru记录的时间计算得出。

如果服务器开启了maxmemory选项且内存淘汰策略选择的是allkey-lru或者volatile-lru,当服务器占用的内存超过maxmemory选项所设置的上限时,空转时长较高的那部分键值将会优先被服务器释放,进而回收内存。

参考资料

  1. Redis设计与实现第三版
  2. Redis 源码日志 wiki.jikexueyuan.com/project/red…