• Redis源码与设计剖析 -- 8.对象系统


    Redis 对象系统



    1. 介绍

    在前面的章节中,我们介绍了Redis中用到的所有底层数据结构,包括:SDS、双端链表、字典、跳表、压缩列表、整数集合、快速列表.

    Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构.

    使用对象系统的优点:

    • Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令.
    • 可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率.

    除此之外,对象系统还实现了一些比较实用的机制:

    内存回收机制:

    基于引用计数技术,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放.

    对象共享机制

    Redis同样通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存.

    过期键自动删除

    Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除.

    2. 对象的结构

    我们在Redis数据库中新创建一个键值对的时候,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象). 其中键对象固定是一个字符串对象,而值对象则是由redisObject结构保存的数据,该结构中和保存数据有关的三个属性分别是:type属性、encoding属性和ptr属性. 而lru和refcount分别是用于对象删除和对象引用的字段,我们后面再解释.

    Redis对象相关的源码在server.h和object.c文件中.

    typedef struct redisObject {
        // 对象的数据类型,占4bits,共5种类型
        unsigned type:4;
        // 对象的编码,占4bits,共10种类型
        unsigned encoding:4;
    
        // 实用LRU算法计算相对server.lruclock的LRU时间
        unsigned lru:LRU_BITS;
    
        // 引用计数
        int refcount;
    
        // 指向底层数据实现的指针
        void *ptr;
    } robj;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.1 对象类型

    对象的type属性记录了对象的类型,Redis一共有5个对象类型,见下表:

    类型常量对象的名称
    REDIS_STRING字符串对象
    REDIS_LIST列表对象
    REDIS_HASH哈希对象
    REDIS_SET集合对象
    REDIS_ZSET有序集合对象

    当我们对一个数据库键执行TYPE KEY命令时,命令会返回该数据库键对应的值对象的类型,下表列出了TYPE KEY命令在面对不同类型的值对象所产生的输出:

    对象对象 type 属性的值TYPE 命令的输出
    字符串对象REDIS_STRING“string”
    列表对象REDIS_LIST“list”
    哈希对象REDIS_HASH“hash”
    集合对象REDIS_SET“set”
    有序集合对象REDIS_ZSET“zset”

    type属性会被设置为对应类型的宏:

    // 字符串对象
    #define OBJ_STRING 0
    // 列表对象
    #define OBJ_LIST 1
    // 集合对象
    #define OBJ_SET 2
    // 有序集合对象
    #define OBJ_ZSET 3
    // 哈希对象
    #define OBJ_HASH 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.2 对象编码和底层类型

    一种对象在保存不同大小数据的时候,可能使用不同的编码类型,而编码类型由encoding属性来记录. 使用命令OBJECT ENCODING KEY可以得到该数据库键对应值所使用的编码.

    下表记录了每种对象会使用的编码类型:

    encodingptr
    OBJ_ENCODING_INT整数值实现的字符串对象
    OBJ_ENCODING_EMBSTRembstr 编码的简单动态字符串实现的字符串对象
    OBJ_ENCODING_RAW简单动态字符串实现的字符串对象
    OBJ_ENCODING_QUICKLIST快速列表实现的列表对象
    OBJ_ENCODING_HT字典实现的集合对象
    OBJ_ENCODING_INTSET整数集合实现的集合对象
    OBJ_ENCODING_ZIPLIST压缩列表实现的哈希对象
    OBJ_ENCODING_HT字典实现的哈希对象
    OBJ_ENCODING_SKIPLIST跳跃表和字典实现的有序集合对象
    OBJ_ENCODING_ZIPLIST压缩列表实现的有序集合对象

    下面是enconding属性会用到的宏:

    #define OBJ_ENCODING_RAW 0     /* Raw representation */
    #define OBJ_ENCODING_INT 1     /* Encoded as integer */
    #define OBJ_ENCODING_HT 2      /* Encoded as hash table */
    #define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
    #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
    #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
    #define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
    #define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
    #define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
    #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
    // redis 5.0添加,用于消息队列的数据类型
    #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.3 指向底层类型的指针

    ptr是一个万能指针,它会指向对象的底层实现数据类型. 我们后面会逐一介绍这些类型.

    3. 对象系统的重要操作

    Redis为对象系统定义了丰富的API,以下函数定义在server.h头文件中,实现在server.c源文件中:

    /* Redis object implementation */
    void decrRefCount(robj *o);
    void decrRefCountVoid(void *o);
    void incrRefCount(robj *o);
    robj *resetRefCount(robj *obj);
    void freeStringObject(robj *o);
    void freeListObject(robj *o);
    void freeSetObject(robj *o);
    void freeZsetObject(robj *o);
    void freeHashObject(robj *o);
    
    robj *createObject(int type, void *ptr);
    robj *createStringObject(const char *ptr, size_t len);
    robj *createRawStringObject(const char *ptr, size_t len);
    robj *createEmbeddedStringObject(const char *ptr, size_t len);
    robj *dupStringObject(robj *o);
    int isObjectRepresentableAsLongLong(robj *o, long long *llongval);
    robj *tryObjectEncoding(robj *o);
    robj *getDecodedObject(robj *o);
    size_t stringObjectLen(robj *o);
    robj *createStringObjectFromLongLong(long long value);
    robj *createStringObjectFromLongDouble(long double value, int humanfriendly);
    robj *createQuicklistObject(void);
    robj *createZiplistObject(void);
    robj *createSetObject(void);
    robj *createIntsetObject(void);
    robj *createHashObject(void);
    robj *createZsetObject(void);
    robj *createZsetZiplistObject(void);
    
    int getLongFromObjectOrReply(client *c, robj *o, long *target, const char *msg);
    int checkType(client *c, robj *o, int type);
    int getLongLongFromObjectOrReply(client *c, robj *o, long long *target, const char *msg);
    int getDoubleFromObjectOrReply(client *c, robj *o, double *target, const char *msg);
    int getLongLongFromObject(robj *o, long long *target);
    int getLongDoubleFromObject(robj *o, long double *target);
    int getLongDoubleFromObjectOrReply(client *c, robj *o, long double *target, const char *msg);
    char *strEncoding(int encoding);
    int compareStringObjects(robj *a, robj *b);
    int collateStringObjects(robj *a, robj *b);
    int equalStringObjects(robj *a, robj *b);
    unsigned long long estimateObjectIdleTime(robj *o);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    我们挑选几个进行研究.

    3.1 创建字符串对象

    // 创建一个默认的对象
    robj *createObject(int type, void *ptr) {
    	// 分配空间
        robj *o = zmalloc(sizeof(*o));
        // 设置对象类型
        o->type = type;
        // 设置默认的编码方式
        o->encoding = OBJ_ENCODING_RAW;
        // 设置对象
        o->ptr = ptr;
        // 引用计数为1
        o->refcount = 1;
    
        /* Set the LRU to the current lruclock (minutes resolution). */
        // 计算设置当前LRU时间
        o->lru = LRU_CLOCK();
        return o;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    创建一个编码为 OBJ_ENCODING_RAW 的字符串对象

    // 创建一个字符串对象,编码默认为 OBJ_ENCODING_RAW,指向的数据为一个sds
    robj *createRawStringObject(const char *ptr, size_t len) {
        return createObject(OBJ_STRING,sdsnewlen(ptr,len));
    }
    
    • 1
    • 2
    • 3
    • 4

    创建一个编码为 OBJ_ENCODING_EMBSTR 的字符串对象

    // 创建一个embstr编码的字符串对象
    robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    	// 分配空间
        robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
        // o+1刚好就是struct sdshdr8的地址
        struct sdshdr8 *sh = (void*)(o+1);
    
    	// 类型为字符串对象
        o->type = OBJ_STRING;
        // 设置编码类型
        o->encoding = OBJ_ENCODING_EMBSTR;
        // 指向分配的sds对象,分配的len+1的空间首地址
        o->ptr = sh+1;
        // 设置引用计数
        o->refcount = 1;
        // 计算设置当前LRU时间
        o->lru = LRU_CLOCK();
    
    	// 设置字符串长度
        sh->len = len;
        // 设置最大容量
        sh->alloc = len;
        // 设置sds的类型
        sh->flags = SDS_TYPE_8;
        // 如果传了字符串参数
        if (ptr) {
       		// 将传进来的ptr保存到对象中
            memcpy(sh->buf,ptr,len);
            // 结束符标志
            sh->buf[len] = '\0';
        } else {
        	// 否则将对象的空间初始化为0
            memset(sh->buf,0,len+1);
        }
        return o;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    创建一个编码为 OBJ_ENCODING_INT 的字符串对象

    // 创建字符串对象,根据整数值
    robj *createStringObjectFromLongLong(long long value) {
        robj *o;
    
        // redis中[0, 10000)内的整数是共享的
        // 如果value属于redis共享整数的范围
        if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
        	// 引用计数加1
            incrRefCount(shared.integers[value]);
            // 返回一个编码类型为OBJ_ENCODING_INT的字符串对象
            o = shared.integers[value];
    
        // 如果不在共享整数的范围
        } else {
        	// value在long类型所表示的范围内
            if (value >= LONG_MIN && value <= LONG_MAX) {
            	// 创建对象
                o = createObject(OBJ_STRING, NULL);
                // 编码类型为OBJ_ENCODING_INT
                o->encoding = OBJ_ENCODING_INT;
                // 指向这个value值
                o->ptr = (void*)((long)value);
            } else {
                // value不在long类型所表示的范围内,将long long类型的整数转换为字符串
                // 编码类型为OBJ_ENCODING_RAW
                o = createObject(OBJ_STRING,sdsfromlonglong(value));
            }
        }
        return o;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    根据不同的长度使用不同的编码

    // sdshdr8的大小为3个字节,加上1个结束符共4个字节
    // redisObject的大小为16个字节
    // redis使用jemalloc内存分配器,且jemalloc会分配8,16,32,64等字节的内存
    // 一个embstr固定的大小为16+3+1 = 20个字节,因此一个最大的embstr字符串为64-20 = 44字节
    #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
    
    // 创建字符串对象,根据长度使用不同的编码类型
    // createRawStringObject和createEmbeddedStringObject的区别是:
    // createRawStringObject是当字符串长度大于44字节时,robj结构和sdshdr结构在内存上是分开的
    // createEmbeddedStringObject是当字符串长度小于等于44字节时,robj结构和sdshdr结构在内存上是连续的
    robj *createStringObject(const char *ptr, size_t len) {
        if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
            return createEmbeddedStringObject(ptr,len);
        else
            return createRawStringObject(ptr,len);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.2 复制字符串对象

    // 返回 复制的o对象的副本的地址,且创建的对象非共享
    robj *dupStringObject(robj *o) {
        robj *d;
    
    	// 一定是OBJ_STRING类型
        serverAssert(o->type == OBJ_STRING);
    
    	// 根据不同的编码类型
        switch(o->encoding) {
        case OBJ_ENCODING_RAW:
        	// 创建的对象非共享
            return createRawStringObject(o->ptr,sdslen(o->ptr));        
        case OBJ_ENCODING_EMBSTR:
            return createEmbeddedStringObject(o->ptr,sdslen(o->ptr));
        case OBJ_ENCODING_INT:
        	// 即使是共享整数范围内的整数,创建的对象也是非共享的
            d = createObject(OBJ_STRING, NULL);
            d->encoding = OBJ_ENCODING_INT;
            d->ptr = o->ptr;
            return d;
        default:
            serverPanic("Wrong encoding.");
            break;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    3.3 销毁字符串对象

    // 销毁字符串对象ptr指向的对象
    void freeStringObject(robj *o) {
        if (o->encoding == OBJ_ENCODING_RAW) {
            sdsfree(o->ptr);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参考资料:

    《Redis设计与实现》

    Redis源码剖析和注释

  • 相关阅读:
    专题一:双指针【优选算法】
    如何使用CompletableFuture
    新公司第一次上架新APP需要提前准备哪些材料?
    鸿蒙Harmony应用开发—ArkTS声明式开发(触摸交互控制:触摸测试控制)
    【RabbitMQ】RabbitMQ 的 6 种工作模式
    关于页面渲染的一些优化方案分享(懒加载、虚拟列表)
    C++ 字母异位词分组
    C# 找出两个Rectangle或是矩形的相互重合与非重合部分?
    Kafka Leader和Follower故障处理细节
    RocksDB 在 vivo 消息推送系统中的实践
  • 原文地址:https://blog.csdn.net/qq_49723651/article/details/125862427