前言 Redis是用C语言开发的,但并没有直接使用C语言数组去表示字符串,而是使用简单动态字符串(Simple dynamic String,简称SDS)作为字符串的底层实现。
以下给出SDS相关的一些常见问题,通过源码分析和实际验证,思考这些问题的答案,了解实现原理和设计思路。
源码版本:
Redis 3.0.0
Redis 6.0.10
思考问题
源码分析 SDS的数据结构,字符串是如何表示的 REDIS 3.0中,SDS的数据结构:
1 2 3 4 5 struct sdshdr { int len; int free ; char buf[]; };
注意sizeof(struct sdshdr)的结果等于8,不是9;这里的buf为柔性数组成员。至于为什么用柔性数组成员可以参考这篇文章:https://www.cnblogs.com/davygeek/p/5748852.html
SDS结构体各成员的作用
len: 表示buf数组中已使用的字节数(不包括’\0’ ),即字符串长度。好处是获取字符串长度时间为O(1)
free: 表示buf数组中未使用的字节数。
buf: 用于保存字符串,遵循C风格字符串原则,以’\0’结尾。
创建给定C字符串的SDS场景,sdshdr结构体各成员初值是多少? 思考问题 :写入一个字符串键key, 值为长度为5的字符串”Redis”,它的SDS表示中len, free, buf成员值各是多少?
源码分析:
1、Redis调用sdsnew
创建一个包含给定C字符串的SDS,这里的initlen = strlen(“Redis”) = 5。
1 2 3 4 sds sdsnew (const char *init) { size_t initlen = (init == NULL ) ? 0 : strlen (init); return sdsnewlen(init, initlen); }
2、再调用sdsnewlen
,返回SDS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 sds sdsnewlen (const void *init, size_t initlen) { struct sdshdr *sh ; if (init) { sh = zmalloc(sizeof (struct sdshdr)+initlen+1 ); } else { sh = zcalloc(sizeof (struct sdshdr)+initlen+1 ); } if (sh == NULL ) return NULL ; sh->len = initlen; sh->free = 0 ; if (initlen && init) memcpy (sh->buf, init, initlen); sh->buf[initlen] = '\0' ; return (char *)sh->buf; }
可以看出,创建给定C字符串的SDS场景:
SDS实例图:
思考问题: 初始化SDS时free为啥给0?—— 平时使用字符串还是只读场景偏多,这样能节约空间。
GDB验证:
Redis是基于内存的数据库,可以通过GDB打印内存,查看这个key对应的SDS结构体,是否和源码分析结果一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (gdb) p server.db[0].dict.ht[0] $1 = {table = 0x7f3008d40900, size = 4, sizemask = 3, used = 1} (gdb) p *(struct redisObject *) (server.db[0].dict.ht[0].table[0].v) $2 = {type = 0, encoding = 8, lru = 11494144, refcount = 1, ptr = 0x7f3008d40838} # type = REDIS_STRING(0), encoding = REDIS_ENCODING_EMBSTR(8) (gdb) p (*(robj *)server.db[0].dict.ht[0].table[0].v).ptr $3 = (void *) 0x7f3008d40838 (gdb) p (sds) 0x7f3008d40838 $4 = (sds) 0x7f3008d40838 "redis" # 存储字符串键的值 (gdb) p *(struct sdshdr *)(0x7f3008d40838 - 0x8) # len 4字节, free 4字节,减去8字节正好是struct sdshdr的首地址 $5 = {len = 5, free = 0, buf = 0x7f3008d40838 "Redis"} # 和以上源码分析的SDS结构体内容一致
上述GDB调试操作的依据说明:
Redis将所有数据库保存在服务器状态server
变量中,默认创建16个数据库,默认目标数据库为0号(db[0])
dict为数据库键空间,保存所有键值对,底层实现为哈希表,其中ht[0]存储key-value,ht[1]用于rehash
table类型为dictEntry **
, 链地址法实现哈希表,table[0]不为NULL,说明这个key的hashcode % size的结果为0
ptr类型为void *。对于字符串键而言,ptr实际类型为char *,存储内容为”Redis”串,而不是struct sdshdr的首地址。
注: Redis并没有直接使用sds, list这些基本数据结构去实现数据库,而是在这些基本数据结构上构筑了一个对象系统 ,统一使用redisObject对象:
1 2 3 4 5 6 7 typedef struct redisObject { unsigned type:4 ; unsigned encoding:4 ; unsigned lru:24 ; int refcount; void *ptr; } robj;
ptr为void *泛型指针,指向底层实现的数据结构。void *是C语言实现泛型编程的常用手段。
type为对象类型。type属性设计目的很简单,因为仅通过ptr这个泛型指针无法获取这个对象真正的类型。对于字符串键,type值取0(REDIS_STRING)
encoding为编码类型。encoding属性设计目的在于,根据不同场景,为对象设置不同的底层数据结构实现来优化性能。此例中,encoding值取8(REDIS_ENCODING_EMBSTR),表示编码方式为embstr。
SDS相较于C风格字符串的优点
SDS的空间分配策略是如何杜绝缓冲区溢出问题的? 以SDS拼接函数sdscat
为例,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 sds sdscat (sds s, const char *t) { return sdscatlen(s, t, strlen (t)); } sds sdscatlen (sds s, const void *t, size_t len) { struct sdshdr *sh ; size_t curlen = sdslen(s); s = sdsMakeRoomFor(s,len); if (s == NULL ) return NULL ; sh = (void *) (s-(sizeof (struct sdshdr))); memcpy (s+curlen, t, len); sh->len = curlen+len; sh->free = sh->free -len; s[curlen+len] = '\0' ; return s; }
可以看出,sdscat
拼接字符串前,会先通过sdsMakeRoomFor
检查s的剩余空间是否足够,如果空间不足,会先调用realloc
扩展出足够空间后 ,再通过memcpy
拼接字符串。 所以杜绝了缓冲区溢出问题。
SDS是如何减少修改字符串时带来的内存重分配次数 对于C字符串,每次增长或缩短操作,都会导致一次内存重分配,性能较差。
SDS中引入free属性,通过未使用空间,优化字符串的增长或缩短操作,减少内存重分配次数。
字符串增长场景,SDS扩容策略 对于字符串增长场景,REDIS采用空间预分配的思想,即不仅分配修改后的SDS必需的空间,还会额外分配一定的未使用空间 。源码参考sdsMakeRoomFor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define SDS_MAX_PREALLOC (1024 * 1024) sds sdsMakeRoomFor (sds s, size_t addlen) { struct sdshdr *sh , *newsh ; size_t free = sdsavail(s); size_t len, newlen; if (free >= addlen) return s; len = sdslen(s); sh = (void *) (s-(sizeof (struct sdshdr))); newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) newlen *= 2 ; else newlen += SDS_MAX_PREALLOC; newsh = zrealloc(sh, sizeof (struct sdshdr)+newlen+1 ); if (newsh == NULL ) return NULL ; newsh->free = newlen - len; return newsh->buf; }
可以看出,额外分配的未使用空间大小由修改后的SDS长度决定:
如果对SDS修改后,SDS长度(即len属性的值)小于1MB,REDIS会额外分配len个字节的空间。例如,给定SDS串s(“hello”),调用sdscat(s, “world”)之后,len = 10,free = 10, buf数组大小变为 10 + 10 + 1 = 21字节。
如果对SDS修改后,SDS长度大于等于1MB, REDIS只额外分配1MB的空间,目的是避免内存出现太大的浪费。
相比C风格字符串,SDS的扩容策略将增长N次字符串需要的内存重分配次数从N次降低为最多N次 。
GDB验证结果如下,和分析源码得出的结论一致:
1 2 3 4 5 6 7 8 9 127.0.0.1:6379> set key hello [OOK (gdb) p *(struct sdshdr *)((*(robj *)server.db[0].dict.ht[0].table[3].v).ptr - 0x8) $1 = {len = 5, free = 0, buf = 0x7f571f1407f8 "hello"} 127.0.0.1:6379> APPEND key world (integer) 10 (gdb) p *(struct sdshdr *)((*(robj *)server.db[0].dict.ht[0].table[3].v).ptr - 0x8) $2 = {len = 10, free = 10, buf = 0x7f571f1407e8 "helloworld"}
字符串缩短场景,SDS空间释放策略 对于字符串缩短场景,REDIS采用惰性空间释放策略,即并不立即回收空闲内存,而是仅使用free属性记录空闲字节数,如果将来需对SDS做增长操作,可以直接使用这部分空闲内存,无需做内存重分配。
源码分析:sdsclear
用于清空SDS保存的字符串内容,采用惰性空闲释放策略,复杂度仅为O(1)
1 2 3 4 5 6 void sdsclear (sds s) { struct sdshdr *sh = (void *) (s-(sizeof (struct sdshdr))); sh->free += sh->len; sh->len = 0 ; sh->buf[0 ] = '\0' ; }
同时,REDIS提供API sdsRemoveFreeSpace
,通过realloc仅分配实际大小的内存,真正地回收空闲内存,解决惰性空间释放策略带来的内存浪费问题。
1 2 3 4 5 6 7 sds sdsRemoveFreeSpace (sds s) { struct sdshdr *sh ; sh = (void *) (s-(sizeof (struct sdshdr))); sh = zrealloc(sh, sizeof (struct sdshdr)+sh->len+1 ); sh->free = 0 ; return sh->buf; }
SDS最大长度是多少? Redis 3.0
struct sdshdr的len成员记录SDS长度,类型为int,4字节,理论上最大长度 2^32 / 2^10 / 2^10 = 4096MB
在set, append操作中通过硬编码写死字符串的最大长度为512MB,超过这个长度会报错,源码参考checkStringLength
。
1 2 3 4 5 6 7 static int checkStringLength (redisClient *c, long long size) { if (size > 512 *1024 *1024 ) { addReplyError(c,"string exceeds maximum allowed size (512MB)" ); return REDIS_ERR; } return REDIS_OK; }
综上, Redis 3.0中SDS最大长度为 512MB。
Redis 6.0.10
通过配置项proto-max-bulk-len
指定SDS长度,默认是512MB,用户可以自行配置这个值,这点和Redis 3.0有区别。
1 2 3 4 5 6 7 static int checkStringLength (client *c, long long size) { if (!(c->flags & CLIENT_MASTER) && size > server.proto_max_bulk_len) { addReplyError(c,"string exceeds maximum allowed size (proto-max-bulk-len)" ); return C_ERR; } return C_OK; }
综上, Redis 6.0中SDS最大长度默认为512MB,用户可以自行配置这个值。
字符串键的三种编码方式 Redis中字符串对象有三种编码,分别是int , embstr, raw。以下分别介绍这三种编码:
int编码 如果一个字符串对象保存的内容是整数值,且这个整数可以用long表示,Redis就把它的编码设置为int
举例:执行 set key “123”命令,会创建一个int编码的字符串对象
1 2 3 4 127.0.0.1:6379> set key 123 OK 127.0.0.1:6379> object encoding key "int"
内存中的redisObject
对象内容如下:
int类型编码的字符串,要求整数落在long的范围内。在64位环境上,long范围:-9223372036854775808~+9223372036854775807,不同的键值和编码结果参考下表:
键值
编码
+123
embstr
-123
int
–123
embstr
-9223372036854775808
int
-9223372036854775809
embstr
9223372036854775807
int
9223372036854775808
embstr
源码分析:
createStringObjectFromLongLong
根据传入的long long类型的整数值,创建一个字符串对象。如果入参在long范围之内,就创建int编码的字符串对象,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define REDIS_SHARED_INTEGERS 10000 robj *createStringObjectFromLongLong (long long value) { robj *o; if (value >= 0 && value < REDIS_SHARED_INTEGERS) { incrRefCount(shared.integers[value]); o = shared.integers[value]; } else { if (value >= LONG_MIN && value <= LONG_MAX) { o = createObject(REDIS_STRING, NULL ); o->encoding = REDIS_ENCODING_INT; o->ptr = (void *)((long )value); } else { o = createObject(REDIS_STRING,sdsfromlonglong(value)); } } return o; }
REDIS中的对象共享机制 Redis服务器初始化的时候,会预先创建1万个字符串对象 (0 ~ 9999), 当服务器需要使用值为 0 - 9999的字符串对象时,服务器会直接使用这些共享对象,而不是去新创建一个对象。这个用意在于节约内存。
举例:
创建字符串键A,B,值都写“1”, 那么这两个键共享同一个redisObject对象,且这个redisObject对象的ptr指向的内容为1
GDB验证结果:
1 2 3 4 5 6 7 8 (gdb) p *(robj *)server.db[0].dict.ht[0].table[0].v $15 = {type = 0, encoding = 1, lru = 11668863, refcount = 3, ptr = 0x1} (gdb) p *(robj *)server.db[0].dict.ht[0].table[2].v $16 = {type = 0, encoding = 1, lru = 11668863, refcount = 3, ptr = 0x1} (gdb) p &(*(robj *)server.db[0].dict.ht[0].table[0].v) $17 = (robj *) 0x7fa7c1457360 (gdb) p &(*(robj *)server.db[0].dict.ht[0].table[2].v) $18 = (robj *) 0x7fa7c1457360 # 和$17相同,都是0x7fa7c1457360,说明这是一个共享对象
此时,两个key和共享字符串对象的内存示意图:
raw编码 如果字符串对象保存的是一个字符串值,且这个字符串长度大于39个字节,REDIS就使用SDS存储这个字符串,并设置编码类型为raw。
举例:
1 2 3 4 5 6 127.0.0.1:6379> set key 1234567891234567891234567891234567891234 OK 127.0.0.1:6379> object encoding ket "raw" 127.0.0.1:6379> strlen key (integer) 40
raw编码的字符串示意图:
源码分析:
createStringObject
用于创建一个SDS表示的字符串对象。当字符串长度大于39字节时使用raw编码, 否则用embstr编码,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39 robj *createStringObject (char *ptr, size_t len) { if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); } robj *createRawStringObject (char *ptr, size_t len) { return createObject(REDIS_STRING,sdsnewlen(ptr,len)); } robj *createObject (int type, void *ptr) { robj *o = zmalloc(sizeof (*o)); o->type = type; o->encoding = REDIS_ENCODING_RAW; o->ptr = ptr; o->refcount = 1 ; o->lru = LRU_CLOCK(); return o; }
embstr编码 如果字符串对象保存的是一个字符串值,且这个字符串长度小于等于39个字节,REDIS就使用SDS存储这个字符串,并设置编码类型为embstr,embstr是专门用于保存短字符串的一种优化编码方式。
举例:
1 2 3 4 127.0.0.1:6379> set key hello OK 127.0.0.1:6379> object encoding key "embstr"
和raw编码类似,embstr编码也使用redisObject
和sdshdr
保存字符串,但差别在于:
raw编码需调用2 次malloc创建redisObject
和sdshdr
对象,且redisObject
和sdshdr
内存不连续
而embstr编码只需1 次malloc创建redisObject
和sdshdr
对象,且redisObject
和sdshdr
内存是连续的
GDB查看embstr编码字符串的内存:
1 2 3 4 5 6 (gdb) p &(*(robj *)server.db[0].dict.ht[0].table[3].v) $1 = (robj *) 0x7f43ebd3b9c0 (gdb) p *(robj *)server.db[0].dict.ht[0].table[3].v $2 = {type = 0, encoding = 8, lru = 11755503, refcount = 1, ptr = 0x7f43ebd3b9d8} (gdb) p *(struct sdshdr *)0x7f43ebd3b9d0 $3 = {len = 5, free = 0, buf = 0x7f43ebd3b9d8 "hello"}
观察结果发现robj和sdshdr内存确实是连续的,embstr编码的内存示意图:
源码分析:
createEmbeddedStringObject
用于创建一个embstr编码的字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 robj *createEmbeddedStringObject (char *ptr, size_t len) { robj *o = zmalloc(sizeof (robj)+sizeof (struct sdshdr)+len+1 ); struct sdshdr *sh = (void *)(o+1 ); o->type = REDIS_STRING; o->encoding = REDIS_ENCODING_EMBSTR; o->ptr = sh+1 ; o->refcount = 1 ; o->lru = LRU_CLOCK(); sh->len = len; sh->free = 0 ; if (ptr) { memcpy (sh->buf,ptr,len); sh->buf[len] = '\0' ; } else { memset (sh->buf,0 ,len+1 ); } return o; }
设计embstr编码的用意是什么 相比于raw编码,embstr编码存储短字符串的优点:
三种编码之间的转换规则 规则1: embstr对象执行修改命令后,总是会变成一个raw编码对象。
规则2: 对于int对象,如果在这个对象执行的操作导致其保存的值不在long范围内,这个对象编码总是变成raw
源码分析:
以APPEND命令为例,源码参考appendCommand
,此函数最终调用dbUnshareStringValue
,总是创建一个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 void appendCommand (redisClient *c) { size_t totlen; robj *o, *append; o = lookupKeyWrite(c->db,c->argv[1 ]); if (o == NULL ) { } else { append = c->argv[2 ]; totlen = stringObjectLen(o)+sdslen(append->ptr); if (checkStringLength(c,totlen) != REDIS_OK) return ; o = dbUnshareStringValue(c->db,c->argv[1 ],o); o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr)); totlen = sdslen(o->ptr); } } robj *dbUnshareStringValue (redisDb *db, robj *key, robj *o) { redisAssert(o->type == REDIS_STRING); if (o->refcount != 1 || o->encoding != REDIS_ENCODING_RAW) { robj *decoded = getDecodedObject(o); o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr)); decrRefCount(decoded); dbOverwrite(db,key,o); } return o; }
验证结果:
1 2 3 4 5 6 7 8 127.0.0.1:6379> set key "hello" OK 127.0.0.1:6379> object encoding key "embstr" 127.0.0.1:6379> append key " world!" (integer) 12 127.0.0.1:6379> object encoding key "raw"
REDIS3.2中的SDS实现 REDIS 3.2中,根据SDS的长度又细分为5类,对于不同长度的字符串,用不同的sdshdrX结构体存储,实现节约内存的目的。
以REDIS 6.0.10源码为例,sdshdr结构体定义如下:
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 typedef char *sds;struct __attribute__ ((__packed__ )) sdshdr5 { unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr8 { uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr16 { uint16_t len; uint16_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr32 { uint32_t len; uint32_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr64 { uint64_t len; uint64_t alloc; unsigned char flags; char buf[]; };
各结构体成员作用:
1 2 3 4 5 6 7 #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 #define SDS_TYPE_32 3 #define SDS_TYPE_64 4 #define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3
__attribute__ ((__packed__))
作用?
__attribute__ ((__packed__))
是GCC特有的语法,作用是取消结构体的字节对齐,采用内存紧凑模式排列。
这里给出加上或不加关键字时,各sdshdr结构体的大小:
结构体
加GCC关键字__attribute__ ((__packed__))
不加关键字
sdshdr5
1字节
1
sdshdr8
3
3
sdshdr16
5
6
sdshdr32
9
12
sdshdr64
17
24
问题思考 :取消结构体字节对齐的用意是什么,有什么优点?
1个好处是节约了内存,时间换空间。
另1个好处是使得通过内存直接访问结构体内部变量非常方便 ,比如通过buf[-1]这种骚操作可以直接访问到flags
成员,从而判断sds类型,实现非常简洁。源码参考如下:
1 2 sds s; char type = s[-1 ] & SDS_TYPE_MASK;
给定一个长度为n的sds,它的底层通过哪个sdshdr类型表示? 以REDIS 6.0.10源码为例,参考sdsReqType
的实现:
1 2 3 4 5 6 7 8 9 10 11 static inline char sdsReqType (size_t string_size) { if (string_size < 32 ) return SDS_TYPE_5; if (string_size < 0xff ) return SDS_TYPE_8; if (string_size < 0xffff ) return SDS_TYPE_16; if (string_size < 0xffffffff ) return SDS_TYPE_32; return SDS_TYPE_64; }
得出如下结论:
SDS长度小于32,用sdshdr5类型表示
SDS长度在 [32, 255),用sdshdr8类型表示
依次类推 ……
思考问题: set key hello
, 键key和值hello都是用sdshdr5表示的吗?
详细分析过程参考文章:https://segmentfault.com/a/1190000017450295
以下仅给出结论和验证结果:
结论:
对于长度小于32的字符串键和值,键通过sdshdr5表示,而值通过sdshdr8表示
对于值,使用createEmbeddedStringObject
总是创建一个sdshdr8类型的对象。
对于键,通过调用链setGenericCommand-->genericSetKey-->dbAdd
,最终调用sdsdup
,创建一个sdshdr5类型的对象。调用栈参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (gdb) bt #0 dbAdd (db=0x557b1c4d9690, key=0x557b1c4f5720, val=0x557b1c4f4730) at db.c:185 #1 0x0000557b1b95751d in genericSetKey (c=c@entry=0x557b1c4ecfa0, db=0x557b1c4d9690, key=key@entry=0x557b1c4f5720, val=val@entry=0x557b1c4f4730, keepttl=keepttl@entry=0, signal=signal@entry=1) at db.c:252 #2 0x0000557b1b9649e7 in setGenericCommand (c=c@entry=0x557b1c4ecfa0, flags=flags@entry=0, key=0x557b1c4f5720, val=0x557b1c4f4730, expire=expire@entry=0x0, unit=unit@entry=0, ok_reply=0x0, abort_reply=0x0) at t_string.c:87 #3 0x0000557b1b964c51 in setCommand (c=0x557b1c4ecfa0) at t_string.c:146 #4 0x0000557b1b93bd5e in call (c=0x557b1c4ecfa0, flags=15) at server.c:3368 #5 0x0000557b1b93c7a5 in processCommand (c=c@entry=0x557b1c4ecfa0) at server.c:3797 #6 0x0000557b1b94a7b0 in processCommandAndResetClient (c=c@entry=0x557b1c4ecfa0) at networking.c:1895 #7 0x0000557b1b94f09a in processInputBuffer (c=0x557b1c4ecfa0) at networking.c:1978 #8 0x0000557b1b9cbd48 in callHandler (handler=<optimized out>, conn=0x557b1c4fdd60) at connhelpers.h:79 #9 connSocketEventHandler (el=<optimized out>, fd=<optimized out>, clientData=0x557b1c4fdd60, mask=<optimized out>) at connection.c:296 #10 0x0000557b1b9356c7 in aeProcessEvents (eventLoop=eventLoop@entry=0x557b1c479aa0, flags=flags@entry=27) at ae.c:479 #11 0x0000557b1b935a0d in aeMain (eventLoop=0x557b1c479aa0) at ae.c:539 #12 0x0000557b1b932216 in main (argc=2, argv=0x7ffe8e366c18) at server.c:5498
GDB验证结果:
1 2 3 4 5 6 7 8 (gdb) x/tb ((*(robj *)server.db[0].dict.ht[0].table[1].v).ptr - 1) // value 0x557b1c4f4742: 00000001 // 注意value的最后三位为001,表示SDS_TYPE_8 $2 = {len = 5 '\005', alloc = 5 '\005', flags = 1 '\001', buf = 0x557b1c4f4743 "hello"} (gdb) p (sds)server.db[0].dict.ht[0].table[1].key $3 = (sds) 0x557b1c502f61 "key" (gdb) x/tb 0x557b1c502f61 - 0x1 0x557b1c502f60: 00011000 // 注意key的最后三位为000,表示SDS_TYPE_5
思考问题 :对于短字符串,为什么键底层类型为sdshdr5,值底层类型设置却成sdshdr8?
个人分析:实际应用场景中,通常键的更新次数远小于值的更新次数。所以对键采用最小的内存存储,以节省空间;对值用更大的内存存储,减少内存重分配的次数,提高性能。
REDIS字符串命令 REDIS 字符串命令参考官方网站:https://redis.io/commands#string
以下仅给出几个最常用的命令:
set key value
get key
append key value
参考资料 【1】《Redis设计与实现》第2章 简单动态字符串,第8章 对象
【2】如何阅读Redis源码
【3】【Redis源码分析】一个对SDSHDR5是否使用的疑问
【4】柔性数组