关于java:Redis源码剖析之robjredisObject

32次阅读

共计 7105 个字符,预计需要花费 18 分钟才能阅读完成。

咱们在之前的文章中曾经理解过一部分 Redis 的数据结构了,尤其是 dict 中讲到,能够把 redis 看做一个 hashtable,存储了一堆的 key-value,明天就来看下 key-value 中 value 的次要存储构造 redisObject(后文统称 robj)。
robj 的具体代码见 object.c

字段详解

绝对与其余几个数据结构,robj 绝对简略,因为只蕴含了几个字段,含意都很明确。

typedef struct redisObject {
    unsigned type:4;       // 数据类型  integer  string  list  set
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). 
                            * redis 用 24 个位来保留 LRU 和 LFU 的信息,当应用 LRU 时保留上次
                            * 读写的工夫戳(秒), 应用 LFU 时保留上次工夫戳(16 位 min 级) 保留近似统计数 8 位 */
    int refcount;          // 援用计数 
    void *ptr;              // 指针指向具体存储的值,类型用 type 辨别
} robj;

外围就五个字段,咱们别离来介绍下。

type(4 位)

type 是示意当然 robj 里所存储的数据类型,目前 redis 中蕴含以下几种类型。

| 标识符 | 值 | 含意 |
|–|–|–|
| OBJ_STRING | 0 | 字符串(string) |
| OBJ_LIST | 1 | 列表(list) |
| OBJ_SET | 2 | 汇合(set) |
| OBJ_ZSET | 3 | 有序集(zset) |
| OBJ_HASH | 4 | 哈希表(hash) |
| OBJ_MODULE | 5 | 模块(module) |
| OBJ_STREAM | 6 | 流(stream) |

encoding(4 位)

编码方式,如果说每个类型只有一种形式,那么其实 type 和 encoding 两个字段只须要保留一个即可,但 redis 为了在各种状况下尽可能介绍内存,对每种类型的数据在不同状况下有不同的编码格局,所以这里须要用额定的字段标识进去。目前有以下几种编码(redis 6.2)。

| 标识符 | 值 | 含意 |
|–|–|–|
| OBJ_ENCODING_RAW | 0 | 最原始的标识形式,只有 string 才会用到 |
| OBJ_ENCODING_INT | 1 | 整数 |
| OBJ_ENCODING_HT | 2 | dict |
| OBJ_ENCODING_ZIPMAP | 3 | zipmap 目前曾经不再应用 |
| OBJ_ENCODING_LINKEDLIST | 4 | 就的链表,当初曾经不再应用了 |
| OBJ_ENCODING_ZIPLIST | 5 | ziplist |
| OBJ_ENCODING_INTSET | 6 | intset |
| OBJ_ENCODING_SKIPLIST | 7 | 跳表 skiplist |
| OBJ_ENCODING_EMBSTR | 8 | 嵌入式的 sds |
| OBJ_ENCODING_QUICKLIST | 9 | 快表 quicklist |
| OBJ_ENCODING_STREAM | 10 | 流 stream |

这里有个 OBJ_ENCODING_EMBSTR,这里着重介绍下。

robj *createEmbeddedStringObject(const char *ptr, size_t len) {robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {memset(sh->buf,0,len+1);
    }
    return o;
}

从下面代码就能够看出,它是 robj 和 sds 的一个联合,将 sds 间接放在 robj 里,这里限度最多能够寄存 44 字节长度的字符串。因为 robj 占 16 字节,sdshdr8 头占 3 字节,’\0’ 一个字节,限度字符串最长为 44 就能够保障在 64 个字节里寄存下所有内容(16+3+1+44==64)。

lru(24 位)

家喻户晓,redis 提供了过期数据主动淘汰的策略,如何晓得数据是否曾经过期?依照什么样的策略淘汰数据?这俩问题的答案都和 lru 这个字段无关。redis 给了 lru 这个字段 24 位,但千万别认为字段名叫 lru 就认为它只是 LRU 淘汰策略中才会应用的,其实 LFU 用的也是这个字段。 我预计是 redis 作者先写了 lru 策略,所以间接就叫 lru 了,起初再加 lfu 策略的时候间接复用这个字段了。
lru 字段在不同淘汰策略时有不同的含意。当应用 LRU 时,它就是一个 24 位的秒级 unix 工夫戳,代表这个数据在第多少秒被更新过。但应用 LFU 策略时,24 位会被分为两局部,16 位的分钟级工夫戳和 8 位的非凡计数器,这里就不再详解了,更具体能够关注我后续的博文。

refcount

援用计数,示意这个 robj 目前被多少个中央利用,refcount 的呈现为对象复用提供了根底。理解过垃圾回收的同学都晓得有中回收策略就是采纳计数器的形式,当 refcount 为 0 时,阐明该对象曾经没用了,就能够被回收掉了,redis 的作者也实现了这种援用回收的策略。

*ptr

这个就很简略了,后面几个字段是为当然 robj 提供 meta 信息,那这个字段就是数据具体所在地址。

robj 的编解码

redis 向来将内存空间节俭做到了极致,这里 redis 的作者又对字符串类型的 robj 做了非凡的编码解决,以达到节俭内存的目标,编码过程的代码及正文如下:

/* 将 string 类型的 robj 做非凡编码,以节俭存储空间  */
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

    /* Make sure this is a string object, the only type we encode
     * in this function. Other types use encoded memory efficient
     * representations but are handled by the commands implementing
     * the type. 
     * 这里只编码 string 对象,其余类型的的编码都由其对应的实现解决 */
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

    /* We try some specialized encoding only for objects that are
     * RAW or EMBSTR encoded, in other words objects that are still
     * in represented by an actually array of chars.
     * 非 sds string 间接返回原数据 */
    if (!sdsEncodedObject(o)) return o;

    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. 
     * 如果是共享的对象,不能编码,因为可能会影响到其余中央的应用 */
     if (o->refcount > 1) return o;

    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 20 chars is not
     * representable as a 32 nor 64 bit integer. 
     * 查看是否能够把字符串示意为一个长整型数。留神如果长度大于 20 个字符的字符串是
     * 不能被示意为 32 或者 64 位的整数的 */
    len = sdslen(s);
    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. 
         * 如果能够被编码为 long 型,且编码后的值小于 OBJ_SHARED_INTEGERS(10000),且未配
         * 置 LRU 替换淘汰策略, 就应用这个数的共享对象,相当于所有小于 10000 的数都是用的同一个 robj*/
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            /* 否则原来如果是 RAW 类型,间接转为 OBJ_ENCODING_INT 类型,而后用 long 来间接存储字符串 */    
            if (o->encoding == OBJ_ENCODING_RAW) {sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            /* 如果是 OBJ_ENCODING_EMBSTR,也会转化为 OBJ_ENCODING_INT,并用 long 存储字符串 */
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }
    // 对于那些无奈转为 long 的字符串,做如下解决

    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. 
     * 如果字符串太小,长度小于等于 44,间接转为 OBJ_ENCODING_EMBSTR*/
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    /* We can't encode the object...
     *
     * Do the last try, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. 
     * 
     * 如果后面没有编码胜利,这里做最初一次尝试,如果 sds 有超过 10% 的可用闲暇空间,* 且字符长度大于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(44)那尝试开释 sds 中多余
     * 的空间以节俭内存。**/
    trimStringObjectIfNeeded(o);

    /* 间接返回原始对象. */
    return o;
}
  1. 查看是否是字符串,如果不是间接返回。
  2. 查看是否是共享对象(refcount > 1),被共享的对象不做编码。
  3. 如果字符串长度小于等于 20,间接能够编码为一个 long 型的整数,这里小于 10000 的 long 对象都是共享的。
  4. 如果字符串长度小于等于 44,间接用 OBJ_ENCODING_EMBSTR 存储。
  5. 如果没有被编码,且字符串长度超过 44,且 sds 中的闲暇空间超过 10%,则革除闲暇空间,以节俭内存。

当然有编码就有解码,代码及如下,绝对比较简单:

/* Get a decoded version of an encoded object (returned as a new object).
 * If the object is already raw-encoded just increment the ref count.
 * 获取解码后的对象(返回的是有个新对象),如果这个对象是个原始类型,只是把援用加一。*/
robj *getDecodedObject(robj *o) {
    robj *dec;

    if (sdsEncodedObject(o)) {incrRefCount(o);
        return o;
    }
    if (o->type == OBJ_STRING && o->encoding == OBJ_ENCODING_INT) {char buf[32];

        ll2string(buf,32,(long)o->ptr);
        dec = createStringObject(buf,strlen(buf));
        return dec;
    } else {serverPanic("Unknown encoding type");
    }
}

援用计数和主动清理

上文曾经说到了,redis 为了节俭空间,会复用一些对象,没有援用的对象会被主动清理。作者用了援用计数的形式来实现 gc,代码也比较简单,如下:

void incrRefCount(robj *o) {if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {o->refcount++;} else {if (o->refcount == OBJ_SHARED_REFCOUNT) {/* Nothing to do: this refcount is immutable. */} else if (o->refcount == OBJ_STATIC_REFCOUNT) {serverPanic("You tried to retain an object allocated in the stack");
        }
    }
}
/* 缩小援用计数,如果没有援用了就开释内存空间 */
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--;
    }
}

总结

总结下,能够认为 robj 有这样几个作用。

  1. 为所有类型的 value 提供一个对立的封装。
  2. 为数据淘汰保留必要的信息。
  3. 实现数据复用,和主动 gc 性能。

本文是 Redis 源码分析系列博文,同时也有与之对应的 Redis 中文正文版,有想深刻学习 Redis 的同学,欢送 star 和关注。
Redis 中文注解版仓库:https://github.com/xindoo/Redis
Redis 源码分析专栏:https://zxs.io/s/1h
如果感觉本文对你有用,欢送 一键三连

正文完
 0