zoukankan      html  css  js  c++  java
  • Redis源码剖析(八)对象系统

    对象的类型与编码

    在 Redis 中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。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属性记录了对象的类型,type属性的值有以下几种:

    类型 

    对象名称

    REDIS_STRING       

    字符串对象        

    REDIS_LIST

    列表对象

    REDIS_HASH

    哈希对象

    REDIS_SET

    集合对象

    REDIS_ZSET

    有序集合对象

    编码与底层实现

    对象的encoding属性记录了对象使用的编码,即底层使用的数据结构,encoding的值有以下几种:

    编码常量

    底层数据结构

     REDIS_ENCODING_INT

     long 类型的整数

     REDIS_ENCODING_EMBSTR

     embstr 编码的简单动态字符串

     REDIS_ENCODING_RAW

     简单动态字符串

     REDIS_ENCODING_HT

     字典

     REDIS_ENCODING_LINKEDLIST

     双端链表

     REDIS_ENCODING_ZIPLIST

     压缩列表

     REDIS_ENCODING_INTSET

     整数集合

     REDIS_ENCODING_SKIPLIST

     跳跃表

     


    字符串对象

    值对象编码

    字符串对象的编码可以是 int 、 raw 或者 embstr

    如果字符串对象保存的是整数值, 那么这个整数值保存在 ptr属性(将 void* 转换成 long ) ,对象编码为int

    如果字符串对象保存的是字符串值, 并且字符串长度大于 39 字节, 那么该字符串值使用简单动态字符串(SDS)来保存, 对象编码为 raw 。

    如果字符串对象保存的字符串值, 并且字符串长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码来保存字符串值。

    字符串对象编码的部分代码如下:

    robj *tryObjectEncoding(robj *o) {
        long value;
        sds s = o->ptr;
        size_t len;
    
        // 对字符串进行检查
        // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
        len = sdslen(s);
        if (len <= 21 && string2l(s,len,&value)) {
        ......
    o->ptr = (void*) value;
    .......
    }
    // 尝试将 RAW 编码的字符串编码为 EMBSTR 编码,#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39 if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {     ...... } // 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间 if (o->encoding == REDIS_ENCODING_RAW && sdsavail(s) > len/10) { o->ptr = sdsRemoveFreeSpace(o->ptr); } return o; }

    EMBSTR编码时,直接申请一段连续的空间,空间中依次包含 redisObject 和 sdshdr 两个结构:

    robj *createEmbeddedStringObject(char *ptr, size_t len) {
        robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
        ......
        return o;
    }

    编码的转换

     当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令。下面给出APPEND命令部分代码:

    void appendCommand(redisClient *c) {
        size_t totlen;
        robj *o, *append;
    
        // 取出键相应的值对象
        o = lookupKeyWrite(c->db,c->argv[1]);
    
        // 检查追加操作之后,字符串的长度是否符合 Redis 的限制
        append = c->argv[2];
        totlen = stringObjectLen(o)+sdslen(append->ptr);
        if (checkStringLength(c,totlen) != REDIS_OK)
                return;
    
        // 执行追加操作,此处会进行编码的转换
        o = dbUnshareStringValue(c->db,c->argv[1],o);
        o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
        totlen = sdslen(o->ptr);
    }

    dbUnshareStringValue函数实现:

    robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o) {
        redisAssert(o->type == REDIS_STRING);
        if (o->refcount != 1 || o->encoding != REDIS_ENCODING_RAW) {
            robj *decoded = getDecodedObject(o);
            //创建raw编码对象
            o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr));
            decrRefCount(decoded);
            dbOverwrite(db,key,o);
        }
        return o;
    }

     列表对象

    编码转换

    列表对象的编码可以是 ziplist 或者 linkedlist

    列表对象使用 ziplist 编码:

    1. 列表对象保存的所有字符串元素的长度都小于 64 字节
    2. 列表对象保存的元素数量小于 512 个

    不能满足这两个条件的列表对象需要使用 linkedlist 编码。以上两个条件的上限值可以通过配置文件中的list-max-ziplist-value 和 list-max-ziplist-entries 选项进行修改。

    ziplist编码的列表对象:

    linkedlist编码的列表对象:

    编码转换的部分代码如下:

    void listTypeTryConversion(robj *subject, robj *value) {if (sdsEncodedObject(value) &&
            // 看字符串是否过长
            sdslen(value->ptr) > server.list_max_ziplist_value)
                // 将编码转换为双端链表
                listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
    }
    
    // PUSH操作
    void listTypePush(robj *subject, robj *value, int where) {
    
        // 是否需要转换编码?
        listTypeTryConversion(subject,value);
    
        if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
            ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
                listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
    
        // ZIPLIST
        if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
            int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
            // 取出对象的值,因为 ZIPLIST 只能保存字符串或整数
            value = getDecodedObject(value);
            subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
            decrRefCount(value);
    
        // 双端链表
        } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
            if (where == REDIS_HEAD) {
                listAddNodeHead(subject->ptr,value);
            } else {
                listAddNodeTail(subject->ptr,value);
            }
            incrRefCount(value);
    
        // 未知编码
        } else {
            redisPanic("Unknown list encoding");
        }
    }

    阻塞行为

    待填坑


    哈希对象

    哈希对象的编码可以是 ziplist 或者 hashtable 。

    当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

    1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节
    2. 哈希对象保存的键值对数量小于 512 个

    不能满足这两个条件的哈希对象需要使用 hashtable 编码。以上两个条件的上限值可以通过配置文件中的 hash-max-ziplist-value 和  hash-max-ziplist-entries 选项进行修改。

    ziplist编码的哈希对象:

    另一方面, hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

    • 字典的每个都是一个字符串对象
    • 字典的每个都是一个字符串对象

     hashtable编码的哈希对象:


    集合对象

     集合对象的编码可以是 intset 或者 hashtable 

    当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

    1. 集合对象保存的所有元素都是整数值
    2. 集合对象保存的元素数量不超过 512 个

    不能满足这两个条件的集合对象需要使用 hashtable 编码。元素数量的上限值是可以通过配置文件 set-max-intset-entries 选项进行修改。

    intset编码的集合对象:

     

     hashtable编码的集合对象


    有序集合对象

    有序集合的编码可以是 ziplist 或者 skiplist 

    当使用ziplist编码作为有序集合对象底层实现时,每个集合元素使用两个压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序。

    ziplist编码的有序集合对象如下:

    skiplist 编码的有序集合对象使用 zset 结构作为底层实现:

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

    zset 结构中的 zsl 跳跃表按分值从小到大保存了所有集合元素: 跳跃表节点的 object 属性保存了元素的成员,  score 属性则保存了元素的分值ZRANK、 ZRANGE 等命令就是基于跳跃表 API 来实现的。

    除此之外, zset 结构中的 dict 字典为有序集合创建了一个从成员到分值的映射: 字典的保存了元素的成员, 而字典的则保存了元素的分值。 通过字典, 可以用 O(1) 复杂度查找给定成员的分值

    skiplist编码的有序集合对象如下:

    这两种数据结构都会通过指针来共享相同元素的成员和分值,不会因此而浪费额外的内存。


    内存回收

     Redis 使用引用计数技术实现的内存回收机制。引用计数信息由 redisObject 结构的 refcount 属性记录。

    以list的PUSH操作为例:

    void listTypePush(robj *subject, robj *value, int where) {
        ......
        // ZIPLIST
        if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
            ......
            decrRefCount(value);
    
        // 双端链表
        } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
            ......
            incrRefCount(value);
        }
    }
    incrRefCount的实现,当引用计数为0时会释放对象内存:
    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--;
        }
    }

    对象的空转时长

    redisObject 结构中的 lru 属性记录了对象最后一次被命令程序访问的时间。可以通过OBJECT IDLETIME 命令可以打印出给定键的空转时长。

    除了可以被 OBJECT IDLETIME 命令打印出来之外, 键的空转时长还有另外一项作用: 如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru , 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。


  • 相关阅读:
    h264 流、帧结构
    H264 帧结构分析、帧判断
    sigaction
    sigaction 用法实例
    sigaction函数的使用
    linux c 之signal 和sigaction区别
    linux 信号signal和sigaction理解
    Hamcrest使用
    Junit4中的新断言assertThat的使用方法
    Hamcrest Tutorial
  • 原文地址:https://www.cnblogs.com/lizhimin123/p/10167867.html
Copyright © 2011-2022 走看看