redis 对象共享实现

引言

这段时间,笔者在读Redis的经典书籍《Redis设计与实现》,看到其中 8.9 节:对象共享有些疑惑,于是实操上手了一下,发现结果和书中描述的不一样。按照书中逻辑,在[0,10000)的范围内对int编码实现的字符串对象共享,使用refcount来记录对象共享次数。

tex 复制代码
127.0.0.1:6379> set A 1002
OK
127.0.0.1:6379> get A
"1002"
127.0.0.1:6379> object encoding A
"int"
127.0.0.1:6379> object refcount A
(integer) 2147483647

RedisObjectrefount值是:2147483647,相信大家对这个值都非常的敏感,2^31-1也就是int的最大值,等于Max.Integer,很明显,目前Redis的逻辑已经有了些许变化了。我们要知道《Redis设计与实现》是基于2.93.0开发版写出来的,而目前的Redis的版本已经迭代到7.2,在某些地方不同很正常,这激发我去了解目前实现逻辑的想法,网上简单搜了下,发现这个方面的确实不多,所以归纳总结了一下。

阅读此文:你将对 redis 的对象和对象共享有一个初步了解。但是本文不会深入讲解一些机制,希望读者可以粗略浏览一遍,选取自己感兴趣的地方阅读。

正文

刚开始学习Java或者背面经的时候,在java.lang.Integer中会了解一个对象共享机制:享元模式;简单来讲就是在初始化就创建了值为[-128,127]之间的Integer对象,后续如果继续创建值相同的对象,就直接返回引用;这种思维在设计时候经常使用,目的就是为了减少因为重复创建常用对象导致资源的浪费。

一、对象结构浅析

我们先简单了解下RedisObject对象的结构,分别是 3.0 和 7.2 的版本,这个什么大的变化,对lru的描述有修改。

c 复制代码
// redis 3.0
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;
c 复制代码
// redis 7.2
struct redisObject {
    unsigned type:4;
    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). */
    int refcount;
    void *ptr;
};

谈到对象,为了丰富下内容,还是说明下其中字段含义吧

  1. type:指明对象的类型,目前对象的类型有五种如下,对象创建后不会改变

    c 复制代码
    /* The actual Redis Object */
    #define OBJ_STRING 0    /* String object. */
    #define OBJ_LIST 1      /* List object. */
    #define OBJ_SET 2       /* Set object. */
    #define OBJ_ZSET 3      /* Sorted set object. */
    #define OBJ_HASH 4      /* Hash object. */
  2. enconding:表示实际的数据编码可以理解为底层的数据结构,会随着存储对象的大小、数量改变编码方式,以便更快的实现基本操作;有很多相信大家都了解过比如:linkedlistskiplistziplist等等,不过目前最版的ziplist已经被listpack完全替代了,主要是因为两个原因:一个是ziplist中的每个节点会记录前一个节点的长度,前一个节点长度的改变可能会导致当前节点长度改变,在极端情况下会引起连锁的反应;另一个是在空间占用小于512MB的时候,每次扩大都是两倍的扩。

    The reason for using listpack instead of ziplist is that ziplist may cause cascading updates when insert and delete in middle, which is the biggest problem. capacity is incremented by 1, 2 or 4, this can lead to a lot of memory waste, ziplsit and listpack are designed to save memory, so is it a loss or gain?

    来自开发者@sundb在[NEW] listpack migration - replace all usage of ziplist with listpack #8702中对使用listpack描述

    c 复制代码
    // redis 7.2
    /* Objects encoding. Some kind of objects like Strings and Hashes can be
     * internally represented in multiple ways. The 'encoding' field of the object
     * is set to one of this fields for this object. */
    #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  /* No longer used: old hash encoding. */
    #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
    #define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
    #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 listpacks */
    #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
    #define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
  3. lru

    简单来说就是记录了最后一次被命令程序访问的时间,当我们使用OBJECT IDLETIME key就返回当前时间与lru的差值,而且此命令也不会刷新时间,其它的目前不涉及。

  4. refcount:对象被引用的次数,后面会详细讨论到。

  5. *ptr 指向数据存储地址的指针。

二、refcount在不同版本下是如何增加的

直接讲主要差距,下面两个方法,这个方法会修改refcount的值,就是对象共享标识,书中和当前不一样原因之一在这里;书中是早期版本,所以在[0,10000)之间共享是会增加refcount的值;现在[0,10000)的共享对象的refcount值都是固定的OBJ_SHARED_REFCOUNT值为INT_MAX

c 复制代码
// redis 3.0 src/object.c
void incrRefCount(robj *o) {
    o->refcount++;
}
c 复制代码
// redis 7.2 src/object.c
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");
        }
    }
}

简单分析下,上面两个函数,第一个函数只是简单增加了refcount的值,而第二个会优先判断refcount是否等于OBJ_SHARED_REFCOUNT;然后什么也不做,对,如果是[0,10000)的共享对象,那么什么也不做。这里再提一下对应的decrRefCount函数,正常对象创建后的refcount的值为1;当对象使用decrRefCount,在这里会释放对象的空间。

c 复制代码
// redis 7.2 src/object.c
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--;
    }
}

三、[0,10000)的共享对象是如何创建的

问题:redis 是在什么时机创建共享对象?什么时候设置的refcount固定值?

我们先看OBJ_SHARED_REFCOUNT的定义,并且值为INT_MAX

c 复制代码
// redis 7.2 src/server.h
#define OBJ_SHARED_REFCOUNT INT_MAX     /* Global object never destroyed. */
#define OBJ_STATIC_REFCOUNT (INT_MAX-1) /* Object allocated in the stack. */
#define OBJ_FIRST_SPECIAL_REFCOUNT OBJ_STATIC_REFCOUNT

makeObjectShared函数注释中说明了:将对象的refcount改为OBJ_SHARED_REFCOUNT也就是最大值

c 复制代码
// redis 7.2 src/object.c
/* Set a special refcount in the object to make it "shared":
 * incrRefCount and decrRefCount() will test for this special refcount
 * and will not touch the object. This way it is free to access shared
 * objects such as small integers from different threads without any
 * mutex.
 *
 * A common pattern to create shared objects:
 *
 * robj *myobject = makeObjectShared(createObject(...));
 *
 */
robj *makeObjectShared(robj *o) {
    serverAssert(o->refcount == 1);
    o->refcount = OBJ_SHARED_REFCOUNT;
    return o;
}

然后我们看看redis是如何初始化[0,10000)的共享对象的,这里注释 Server initialization 明说了在服务启动调initServer用的时候,在createSharedObjects函数中初始化的所有共享对象,当然 redis 内部的共享对象不止这[0,10000),只是用户能使用到的共享对象只有[0,10000),其他都是 redis 自用。

c 复制代码
// redis 7.2 src/server.c
/* =========================== Server initialization ======================== */
void createSharedObjects(void) {
  int j;
  // ...
   for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
        shared.integers[j] =
            makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
        initObjectLRUOrLFU(shared.integers[j]);
        shared.integers[j]->encoding = OBJ_ENCODING_INT;
    }
  // ...
}

在 redis 其他共享对象有哪些呢?

  • 命令回复字符串;比如OK\r\n+PONG\r\n
  • 命令错误回复字符串;比如no such key
  • 命令字符串;比如DEL
  • 命令参数字符串
  • 其他常用:比如协议会经常发出类似 $3\r\n 或 *2\r\n 这样的信息,redis 存了一些较小的这种特殊符号带整数的共享对象
  • minstringmaxstring这个两个字符串,用于在ZRANGELEX中表示最小和最大字符串

其他详情的可以参看源码createSharedObjects函数。

四、如何使用共享对象

当我们尝试创建一个int类型的对象的时候,如果传入的值是一个数字,那么就会走到下面的逻辑

c 复制代码
/* Create a string object from a long long value according to the specified flag. */
#define LL2STROBJ_AUTO 0       /* automatically create the optimal string object */
#define LL2STROBJ_NO_SHARED 1  /* disallow shared objects */
#define LL2STROBJ_NO_INT_ENC 2 /* disallow integer encoded objects. */
robj *createStringObjectFromLongLongWithOptions(long long value, int flag) {
    robj *o;

    if (value >= 0 && value < OBJ_SHARED_INTEGERS && flag == LL2STROBJ_AUTO) {
        o = shared.integers[value];
    } else {
        if ((value >= LONG_MIN && value <= LONG_MAX) && flag != LL2STROBJ_NO_INT_ENC) {
            o = createObject(OBJ_STRING, NULL);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*)((long)value);
        } else {
            char buf[LONG_STR_SIZE];
            int len = ll2string(buf, sizeof(buf), value);
            o = createStringObject(buf, len);
        }
    }
    return o;
}

在第一个判断的地方,如果传入的value在范围之类并且flag == 0那么直接返回共享的变量;后面的逻辑就是如果没有超过存储的范围就用long去存这个数字,相反超过范围就直接创建字符对象。

后文

通过前面简单的了解,我们可以对Redis的共享变量有了简单的认识,不过我们并没有继续深入探究其它相关的一些逻辑。

参考

源码地址[github.com/redis/redis]

github issue 地址[github.com/redis/redis...]

相关推荐
阿乾之铭11 分钟前
Spring Boot中集成Redis与MySQL
spring boot·redis·mysql
哭哭啼3 小时前
Redis环境部署(主从模式、哨兵模式、集群模式)
数据库·redis·缓存
明志致远淡泊宁静5 小时前
记录一次服务器redis被入侵
运维·服务器·redis
WuMingf_5 小时前
redis
数据库·redis
精进攻城狮@7 小时前
Redis(value的数据类型)
数据库·redis
jwybobo20078 小时前
redis7.x源码分析:(3) dict字典
linux·redis
simpleGq8 小时前
Redis知识点整理 - 脑图
数据库·redis·缓存
李少兄10 小时前
解决Spring Boot整合Redis时的连接问题
spring boot·redis·后端
日里安10 小时前
8. 基于 Redis 实现限流
数据库·redis·缓存
sam-12319 小时前
k8s上部署redis高可用集群
redis·docker·k8s