字符串常见命令
// 添加一个key为name,value为tom 的字符串
127.0.0.1:6379> set name "tom"
OK
// 获取key的值
127.0.0.1:6379> get name
"tom"
// 判断key是否存在
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> exists age
(integer) 0
// 批量写入多个key
127.0.0.1:6379> mset name "tom" age 20 sex man
OK
// 批量读取多个key
127.0.0.1:6379> mget name age sex
1) "tom"
2) "20"
3) "man"
// 设置失效时间
127.0.0.1:6379> setex name 5 "Tony"
OK
// 5秒后
127.0.0.1:6379> get name
(nil)
// 如果key不存在,设置value,返回1
127.0.0.1:6379> setnx name "tom"
(integer) 1
// key存在了,无法设置返回0
127.0.0.1:6379> setnx name "Tony"
(integer) 0
复制代码
数据结构
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
// 已经使用的数组长度
uint8_t len;
// 不包含对象头和空字符,数组总长度(减去len后就是之前版本的free属性)
uint8_t alloc;
// flag 一个字节 其中第三位存储的类型,高5位来记录小于32个字节的长度
unsigned char flags;
// 字符数组
char buf[];
};
复制代码
这里说下flag字段,看Redis为了节省空间如何“丧心病狂”的节约内存使用。看下面两个问题
- 如果不管存的是短字符、还是长字符都占用相同大小的头部?
- 短字符来说,长度为1个字节的字符串,而头部的len和alloc字段就占用两个字节有必要吗?
对于第一个问题,sdshdr5结构体中,没有len和alloc字段。那么怎么标识长度呢?别急,再看第二个问题,怎么知道字符长度是几个字节呢?(1个字节?、2个字节?4个字节?8个字节?还是说少于1个字节)。
所以sdshdr5结构体的flag的低3位来区分类型。高5位来存字符的长度,保存长度小于32短字符串。
sdshdr8,sdshdr16,sdshdr32,sdshdr64都是用len和alloc字段,flag字段只是采用低3位来记录类型。高5位就是闲置的。
内存分配
扩容
下面代码有部分删减,只截取了重要部分,完整代码请参考redis-5.0.1
sds sdsnewlen(const void *init, size_t initlen) {
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. */
sh = s_malloc(hdrlen+initlen+1);
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;
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 = initlen;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
复制代码
#define SDS_MAX_PREALLOC (1024*1024)
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
复制代码
-
SDS的alloc小于SDS_MAX_PREALLOC时,即1M大小,那么新数组是原数组长度的一倍,并且会预分配扩容后的容量,举个例子,现在数组长度是8字节,已经使用了6个字节,现在需要添加8个字节,那么新的字符数组长度就是 14,然后预分配一倍的长度,最后的14+14+1=29个字节,因为动态数据最后一个字节都是'\0'的空字符,所以+1是空字符长度。
-
SDS的alloc大于1M时,扩容后会预分配1M的内存,例如,现在数组长度是2M大小,需要添加3M的字符串,那么扩容后容量就是2+3+1=6M,再加上1个字节。
这种预分配好处就是避免连续重新分配内存。
惰性释放
“惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是更新len属性,将未使用的字节的数量记录起来,并等待将来使用。
个人拙见,如有不对,勿喷,还请指出。