简介
nignx和redis都是高性能服务的典型代表,其线程模型都是异步网络I/O机制实现的。redis的性能可以达到每秒10w+qps。
reids的数据结构有:字符串结构sds、跳跃表skiplist、压缩列表ziplist、字典dict、快速列表quicklist、消息队列stream等。
常用的算法:Hash常用算法times33、物理位置查找算法geohash、统计算法HyperLogLog等。
sds结构
1.redis中的字符串称为动态字符,可以存储字符串和整型数据(单个或多个char表示一个整型)。3.2之前采用的是下图的设计方式:
(1)头部投len和free可以轻松得到字符串长度;
(2) 内容存放到动态数组中,向上层暴露,上层可以自由处理;
(3) 由于len存在,结尾不用“\0”终止符,保证了二进制的安全;
2.Redis 5.0中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:
typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
上面结构体中4个字段具体含义:
len:buf中已使用字节数;
alloc: 除去头和终止符的长度即buf的总长度;
flags: 结构体的类型标识,只用了低3位。高5位保留;
buf: 动态数组;
看到结构体通过__packed__修饰,来进行1字节对齐。如果不进行修饰的话,按默认的4字节对齐。按照一字节对齐所需内存是9个字节存储头信息,按照3字节对齐需要是4 X 3 = 12字节。所以这样的好处是节省了内存,并且通过(char*)sh+hdrlen(hdrlen是结构体长度)得到buf地址,然后通过buf[-1]就可以简单定位到需要的类型标识符。
获取buf地址:
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
sds的常用操作
数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。下面将着重介绍创建、释放、拼接字符串的相关API。
创建字符串
创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。
1)创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。
2)长度计算时有“+1”操作,是为了算上结束符“\0”。
3)返回值是指向sds结构buf字段的指针。
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;//SDS_TYPE_5 强制转换为SDS_TYPE_8
int hdrlen = sdsHdrSize(type); //计算不同类型所需要的头部长度
unsigned char *fp; /* flags pointer. */ //指向flags 指针
sh = s_malloc(hdrlen+initlen+1);
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1); //+1为了保存结束符"\0"
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen; //s指向buf指针
fp = ((unsigned char*)s)-1; //s是动态数组buf的指针,-1即指向flags
switch(type) {
......//根据不同类型初始化不同结构
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0'; //添加结束符
return s;
}
返回的s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。
释放字符串
通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存;
/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
为了优化性能(减少申请内存的开销),SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
拼接字符串
调用sdscatlen进行扩容,其中可能涉及SDS的扩容。sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。
/* 将指针t的内容和指针s的内容拼接在一起,该操作是二进制安全的*/
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len); //直接拼接,保证了二进制安全
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* type5的结构不支持扩容,所以这里需要强制转成type8*/
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
if (oldtype==type) { /*无须更改类型,通过realloc扩大动态数组即可,注意这里指向buf的指针s被更新了*/
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {/*扩容后数据类型和头部长度发生了变化,此时不再进行realloc操作,而是直接重新开辟内存,拼接完内容后,释放旧指针*/
newsh = s_malloc(hdrlen+newlen+1); //将原buf内容移动到新位置
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);/释放旧指针
s = (char*)newsh+hdrlen; //偏移sds结构的起始地址,得到字符串起始地址
s[-1] = type;//为falgs赋值
sdssetlen(s, len); //为len属性赋值
}
sdssetalloc(s, newlen); //为alloc属性赋值
return s;
}
具体流程如下: