Redis Shared Object 分析

不久前遇到个 redis 的 Shared Object 问题,由 Shared Object 引发的 idletime 和 refcount 监控异常。于是看了下 redis 的相关实现,总价一下 Shared Object 的一些特点。

先说结论:

  • 10000 以内的数字在单个 Redis 实例中是共享的。包括 incr 等操作得到的数字
  • Shared Object 的 lru 也是共享的。因此对一个 key 的访问,也会影响其他拥有同一个共享对象的 lru。(即 object idletime $key 看到的数值)

Redis Object

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to server.lruclock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits decreas time). */
    int refcount;
    void *ptr;
} robj;

这是一个完整的 Redis Object 定义,与大多数弱类型语言的实现类似。其中:

  • type: 数据类型,包括 STRING、LIST、SET、ZSET、HASH 等,也就是 redis 所支持的数据类型。可以通过 TYPE $key 来查看。
  • encoding:redis 数据的内部存储方式。比如 int、string 在 redis 中都是 STRING,但实际上可以选择使用 int 的方式存储,以节省内存。可以通过 OBJECT ENCODING $key 来查看。
  • lru:时间戳,根据时间精度,生成访问时的时间。
  • refcount:引用计数,当引用计数为 0 时,释放对象。可以通过 OBJECT REFCOUNT $key 来查看。

可以看出,Shared Object 就是通过 refcount 来实现的。当新增一个共享时,增加其 refcount 即可。

另外,因为 lru 会在访问时被更新,因此对任意一个 Shared Object 的访问,都等价于访问了所有共享了此对象的 key。

Shared Integers

Redis 在 server.h 中定义了一个 sharedObjectsStruct。redis 所支持的命令、错误等常量都定义在这里。其中有个 integers 的定义,初始化的代码如下:

#define OBJ_SHARED_INTEGERS 10000

for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
    shared.integers[j] =
        makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
    shared.integers[j]->encoding = OBJ_ENCODING_INT;
}

可以看出,小于 10000 的数字被统一放到了一个数组中。这样做有两个显而易见的好处:

  1. 使用 redis 作为计数器的场景下,10000 以下数字的共享能够节省大量内存
  2. 用数组作为索引,基本没有查找的代价,因此共享后的索引成本很低

可以看下是怎么使用 Shared Integers 的:

robj *createStringObjectFromLongLong(long long value) {
    robj *o;
    if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    } else {
        if (value >= LONG_MIN && value <= LONG_MAX) {
            o = createObject(OBJ_STRING, NULL);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*)((long)value);
        } else {
            o = createObject(OBJ_STRING,sdsfromlonglong(value));
        }
    }
    return o;
}

这里也可以看出:

  • 小于 10000 的数字都会使用 shared.integers
  • long 可以表示的数字,内部都使用 OBJ_ENCODING_INT 类型,用以节省空间
  • 超出 long 范围的数字,则使用 OBJ_ENCODING_RAW 的类型

P.S.

php + mysql + redis 还真是创业公司三大神器.....