一谈到Redis,马上能想到的就是:“快”,Redis之所以快,一方面是因为Redis的所有操作都在内存中完成,内存操作本身就很快,另一方面就要归功于它的数据结构了,高效的数据结构是Redis快的基石,当然,也不是所有的命令执行效率都很高,本文就来看看都有哪些时间复杂度较高的命名。
Redis除了本身的Hash、Set类型使用了哈希表之外,实际上Redis中所有的键值对也是用哈希表来保存的,哈希表大家应该已经非常熟悉了,虽然存在Hash冲突的问题,但可以通过rehash解决,所以寻找Key的效率很高,时间复杂度是O(1)。
并且Redis为了尽量避免一次rehash时间过长可能带来的阻塞问题,还采用了渐进式rehash的方式,简单点来说,就是每次只处理哈希表中一个索引位置上的元素,这样就把一次性大量的消耗,分摊变为了多次小量的消耗,避免了长耗时的操作。
SDS(simple dynamic string),这是Redis中String类型的底层数据结构。
/* 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[];
};
len:记录buf数组中已使用的字节的长度。
alloc:已分配的空间,不包含头和空终止符。
flags:前三位表示不同的sds结构体的类型,后五位预留。
buf:字节数组,用于保存字符串。
1、获取字符串长度的复杂度降低
因为在sds的头中包含了len属性,所以在获取一个字符串长度时它的复杂度是O(1),而C字符串则必须遍历直到遇到’\0’,所以它的复杂度是O(n)。
2、避免内存溢出
在C字符串中为一个老的字符串追加字符,如果预先没有分配好足够的内存空间,则会导致数据溢出的问题,而使用sds则不会,因为sds提供的api会在空间不足的情况下,自动进行扩容操作。
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;
}
3、空间预分配
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);
//#define SDS_MAX_PREALLOC (1024*1024)
//如果拼接后的字符串长度小于1M,则为新的字符串分配原长度2倍的空间
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
//如果超过1M,则多分配1M的空间
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
//如果类型没改变
if (oldtype==type) {
//重新分配已经分配好的内存
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
//动态分配一个新的内存空间
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
这就说明了,每次字符串的扩容,并不是扩充到刚好为当前字符串的长度,而是根据当前字符串的长度,要么扩充到原来的2倍,要么扩充1M。
这样做的目的是为了解决C字符串每次扩容都需要进行内存分配的问题,为了减少内存分配的次数,而预先分配一定的空间,使得原来扩充N次需要进行N次内存分配,优化到了扩充N次最多进行N次内存分配。
4、惰性删除
清空字符串操作,会将sds字符串长度修改为0,但是缓冲区并不会被回收,只是修改为可用空间,这样之后如果又要对字符串做拼接操作的话就不需要再重新分配内存空间。
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
最后对比一下,相比C本身的字符串,SDS有哪些好处。
压缩列表可以理解为是一种数组形式的数据结构,目的是为了提升内存的利用率,Redis数据类型中的List、Hash、Sorted Set底层实现时都会有用到压缩列表
压缩列表分为ziplist header、entries、end三个部分,其中头部由zlbytes、zltail、zllen组成,分别表示:列表长度、到达列表尾部的偏移量和列表中entry的数量,具体可见下图。
压缩列表这种数据结构,相对于Hash来说,减少了大量的指针存储,因此非常的节省内存使用,并且在检索第一个元素以及最后一个元素时可以利用zlbytes、zltail、zllen直接定位,效率也很高,不过,如果要检索其他元素,就没那么高效了,只能一个个遍历了,所以其时间复杂度是O(n)。
为了避免在元素较多的情况下由于时间复杂度较高导致查询效率变差的情况,Redis特地设置了两个配置项,分别如下:
hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
这两个只要有一个条件满足,Redis就会把压缩列表转为哈希表,从而避免查询效率变低的问题。
跳表是Sorted Set数据类型的底层数据结构,跳表是在有序链表的基础上,通过增加多级索引,从而实现元素的快速定位,最关键的是,跳表的性能与红黑树差不多,但实现原理则比红黑树简单的多,并且查询、插入、删除的时间复杂度都是O(logN)。
摘自百度百科
整数集合、双向链表都是比较简单的数据结构了,其中List类型除了使用到了压缩列表之外,还会使用双向链表来实现,整数集合则在Set类型时会使用到。
Redis中的数据类型 | 底层数据结构 |
---|---|
String | 简单动态字符串 |
Hash | 哈希表、压缩列表 |
List | 双向链表、压缩列表 |
Set | 整数集合、哈希表 |
Sort Set | 跳表、压缩列表 |