Redis源码分析之robj篇 在 Redis 中,一个 database 内的这个映射关系是用一个 dict 来维护的(ht[0])。dict 的 key 固定用一种数据结构来表达就够了,即动态字符串 sds。而 value 则比较复杂,为了在同一个 dict 内能够存储不同类型的 value,需要一个通用的数据结构,这个通用的数据结构就是 robj(全称 RedisObject )。
1 2 3 4 5 6 7 8 9 10 11 12 13 #define LRU_BITS 24 typedef struct redisObject { unsigned type: 4 ; unsigned encoding: 4 ; unsigned lru: LRU_BITS; int refcount; void *ptr; } robj;
首先解释下这5个字段的含义:
type 数据类型:string、list、hash…
1 2 3 4 5 6 7 8 9 #define OBJ_STRING 0 #define OBJ_LIST 1 #define OBJ_SET 2 #define OBJ_ZSET 3 #define OBJ_HASH 4 #define OBJ_MODULE 5 #define OBJ_STREAM 6
encoding 编码方式:比如 string 就有 embstr 和 raw 编码方式;
1 2 3 4 5 6 7 8 9 10 11 #define OBJ_ENCODING_RAW 0 #define OBJ_ENCODING_INT 1 #define OBJ_ENCODING_HT 2 #define OBJ_ENCODING_ZIPMAP 3 #define OBJ_ENCODING_LINKEDLIST 4 #define OBJ_ENCODING_ZIPLIST 5 #define OBJ_ENCODING_INTSET 6 #define OBJ_ENCODING_SKIPLIST 7 #define OBJ_ENCODING_EMBSTR 8 #define OBJ_ENCODING_QUICKLIST 9 #define OBJ_ENCODING_STREAM 10
lru: LRU(Least Recently Used)时间,在 redis 淘汰数据时会用;
refcount: 引用计数。它允许 robj 对象在某些情况下被共享,例如 redis 在启动时对于常见的响应指令(ok、error),错误信息,0-9999 的数字对象都进行了创建并复用;
ptr: 指针,指向真正的值。
接着分析下这个结构体的定义方式
这是 C 语言里的位域定义方法,表示该字段所占的比特数(bit)
最后我们分析可以知道,一个这样的 redisObject 需要占用 4bit + 4bit + 24bit + 4B +8B 即 16 字节空间,这对我们使用 redis 有没有什么启发呢?
如果我要存一个很短的字符串, 字符串可能不到16字节,但为了描述这个字符串的头结构就占了16字节,内存利用率是不是低了?
如果我要存一个数字,8字节就能搞定,还有必要让 ptr 指针去指向一个真实存储这个数字的空间吗?
另外,目前的机器基本都是 64 位架构,通常处理器的缓存行大小是 64 字节(64B),这意味着每次从内存加载数据到缓存时,最小的单位是 64 字节。即使你只需要其中的一部分数据,剩余的部分也会被一并加载到缓存中。
Redis 中的创建的很多字符串,都会采用 SDS8 的结构(SDS5 无法记录有效容量,对后续拓展不友好),如果 SDS8 的字符串 + redisObject 的头大小,恰好能满足 64B,意味着就不用再去内存中取数据了。
这就是 Redis String 类型 embStr 编码方式:
1 2 3 4 5 6 7 8 9 10 11 #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 robj *createStringObject (const char *ptr, size_t len) { if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr, len); else return createRawStringObject(ptr, len); }
1 2 3 4 5 6 struct __attribute__ ((__packed__ )) sdshdr8 { uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; };
指的一提的是,即使刚开始我们创建的是一个 embStr 编码的字符串,只要对其进行 append 操作,编码方式就会变为 raw,原因是变动会执行下面这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 void appendCommand (client *c) { size_t totlen; robj *o, *append; o = lookupKeyWrite(c->db, c->argv[1 ]); if (o == NULL ) { c->argv[2 ] = tryObjectEncoding(c->argv[2 ]); dbAdd(c->db, c->argv[1 ], c->argv[2 ]); incrRefCount(c->argv[2 ]); totlen = stringObjectLen(c->argv[2 ]); } else { if (checkType(c, o, OBJ_STRING)) return ; append = c->argv[2 ]; totlen = stringObjectLen(o) + sdslen(append->ptr); if (checkStringLength(c, totlen) != C_OK) return ; o = dbUnshareStringValue(c->db, c->argv[1 ], o); o->ptr = sdscatlen(o->ptr, append->ptr, sdslen(append->ptr)); totlen = sdslen(o->ptr); } signalModifiedKey(c->db, c->argv[1 ]); notifyKeyspaceEvent(NOTIFY_STRING, "append" , c->argv[1 ], c->db->id); server.dirty++; addReplyLongLong(c, totlen); } robj *dbUnshareStringValue (redisDb *db, robj *key, robj *o) { serverAssert(o->type == OBJ_STRING); if (o->refcount != 1 || o->encoding != OBJ_ENCODING_RAW) { robj *decoded = getDecodedObject(o); o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr)); decrRefCount(decoded); dbOverwrite(db, key, o); } return o; }
关于 SDS 的 44 字节和 append 操作后会变为 raw 编码其实在上一篇 Redis源码分析之sds篇 中已经提过了~
Robj 在创建字符串对象时,还会尝试把字符串转为 64 位可表示的 long,如果能转成功:
该值处于 0-9999 且不运行 LRU(如果运行 LRU,会影响对象的共享),会返回一个共享对象(前面提到 Redis 会共享 0-9999 的 Robj 对象);
其他情况下 ptr 指针字段直接存成这个 long 型的值。
只要该值可以被转为 64 位可表示的 long,最终的表示形式就是 type=OBJ_STRING, ecoding=OBJ_ENCODING_INT 的一个 string robj 对象。
Robj 不只是为值提供一个统一的表示方式 ,还允许同一类型的数据采用不同的内部表示 ,从而在某些情况下尽量节省内存,同时支持对象共享和引用计数 。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。