简介
数据结构的基本操作不外乎增、删、改、查,Redis也不例外;它拥有五种常见的数据结构String(字符串),Hash(哈希),List(列表),Set(集合),Sorted Set(有序集合)数据结构,通过这些数据结构它能完成上述基本数据操作。同时它还有和HyperLogLog结构以及pub/sub (一种消息通信模式)模式。
在面试时询问Redis运行速度为什么这么快时,回答肯定会包含优秀的数据结构这一条,现在我就来学习Redis的数据结构设计优秀在哪里。
总览
Redis的五种常见数据结构只是对外的数据结构,通过指令:object encoding key,可以查询其内部编码(先设置k,v键值对)。
五种数据结构对应的内部编码如下:
见到其数据结构,那么就有两个问题:Redis如何选择其内部数据结构,以及这么做的好处在哪里。
String
下面就分析下String结构: 设置一个非常长的key(超过44个字节),发现其数据结构为(raw):
可以发现Redis的String数据结构内部编码有以下三种:int ,embstr ,raw。它们在何时会被使用呢?当使用如下命令后,Redis做了哪些操作呢,根据源码其大致分为以下几步
set key "this is a key which is greater than 39 bytes www.baidu.com"
- 解析SET命令,获取键和值,进行网络处理。
- 命令分发并执行
- 键值对象创建
- 数据库操作
- 持久化操作
- 返回响应给客户端。
主要看第三步的源码如下:
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)//默认为44
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
可以发现key使用embstr和raw的默认限制是44,这个数字有点奇怪,现在看看Redis设计者为什么把这个值设置成44。看看两个创建对象的代码源码:
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
//连续内存块通过zmalloc方法进行分配
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
struct sdshdr8 *sh = (void*)(o+1);
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
o->ptr = sh+1;
o->refcount = 1;
o->lru = 0;
sh->len = len;
sh->alloc = len;
sh->flags = SDS_TYPE_8;
if (ptr == SDS_NOINIT)
sh->buf[len] = '\0';
else if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
robj为Redis在内部定义为redisObject结构体,其代码如下:
struct redisObject {
unsigned type:4;//对象类型
unsigned encoding:4;//内部编码类型
unsigned lru:LRU_BITS; //LRU计时时钟
int refcount;//引用计数器
void *ptr;//数据指针
};
type字段:表示当前对象使用的数据类型,Redis主要支持5种数据类型:string、hash、list、set、zset。可以使用type{key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
encoding字段:表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要,同一个对象采用不同的编码实现内存占用存在明显差异。
lru字段:记录对象最后一次被访问的时间,当配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru时,用于辅助LRU算法删除键数据。可以使用object idletime{key}命令在不更新lru字段情况下查看当前键的空闲时间。
refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount{key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。
*ptr字段:与对象的数据内容相关,如果是整数,直接存储数据;否则表示指向数据的指针。
回到正文,
当其分配内存时可以发现,Redis通过zmalloc一次性分配内存,总大小为:sizeof(robj) + sizeof(sdshdr8) + len + 1
其中:
robj占16字节(4位类型+4位编码+24位LRU+32位引用计数+64位指针)6。
sdshdr8头占3字节(uint8_t len, uint8_t alloc, char flags)3。
len为字符串长度,+1用于存储\0结束符(C语言中表示字符串结束)。
内存布局:
robj *o指向内存块起始地址。
sdshdr8 *sh紧随robj之后(o+1)。
o->ptr指向sh+1,即SDS的buf数组起始位置,确保数据与元数据连续。
Redis默认使用jemalloc分配内存,以64字节为最小单位。扣除robj(16字节)、sdshdr8头(3字节)和\0(1字节),剩余空间为64 - 16 - 3 - 1 = 44字节。
再看看创建raw数据结构的源码:
robj *createRawStringObject(const char *ptr, size_t len) {
return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = OBJ_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
o->lru = 0;
return o;
}
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
size_t usable;
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
其代码需要创建两个部分:
调用sdsnewlen(ptr, len),根据输入字符串长度创建SDS结构体,返回SDS指针。
调用createObject,传入类型OBJ_STRING和SDS指针,生成redisObject。
其中SDS结构体独立分配内存,结构包含len(字符串长度)、alloc(分配空间)、flags(类型标志)和buf(字符数组);单独分配redisObject内存块,通过ptr字段指向SDS结构体的地址。
由此分析源码可以得出字符串数据结构的优劣点:
两种编码方式的优劣之处:
embstr:是连续内存适用高性能场景可以避免内存碎片,提升小字符串操作效率。
raw:支持动态修改(如APPEND操作),而embstr编码因内存连续无法原地修改;适合大字符串存储,避免预分配过多内存。
剩下的数据结构继续学习,如果有不同意见可以在我的公众号或者本文章下留言。