引言
这段时间,笔者在读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
此RedisObject
的refount
值是:2147483647
,相信大家对这个值都非常的敏感,2^31-1
也就是int
的最大值,等于Max.Integer
,很明显,目前Redis的逻辑已经有了些许变化了。我们要知道《Redis设计与实现》是基于2.9
和3.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;
};
谈到对象,为了丰富下内容,还是说明下其中字段含义吧
-
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. */
-
enconding
:表示实际的数据编码可以理解为底层的数据结构,会随着存储对象的大小、数量改变编码方式,以便更快的实现基本操作;有很多相信大家都了解过比如:linkedlist
、skiplist
、ziplist
等等,不过目前最版的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 */
-
lru
简单来说就是记录了最后一次被命令程序访问的时间,当我们使用
OBJECT IDLETIME key
就返回当前时间与lru
的差值,而且此命令也不会刷新时间,其它的目前不涉及。 -
refcount
:对象被引用的次数,后面会详细讨论到。 -
*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 存了一些较小的这种特殊符号带整数的共享对象 minstring
、maxstring
这个两个字符串,用于在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...]