1、string类型使用场景
- 计数,如点赞数、限制请求次数、pv/uv等
- 作为key-value缓存基础数据
- 存储session,以实现分布式下的共享session
2、数据结构
redis为c语言编写的,但是c语言没有String类型,只有cha[]类型,而且char数组在初始化的时候指定完大小后就不能再改变了。基于此,redis维护了一个自己的数据结构——SDS(Simple Dynamic String)。
SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。
2.1、二进制安全
在C语言中,用\0
表示字符串的结束,如果字符串本身就有0
字符,那么这个字符串就会被截断,这就是非二进制安全的。若要通过某种机制保证字符串读写时不损害其内容,则是二进制安全的。
2.2 redis实现的string二进制安全
-
Redis3.2之前
3.2版本之前的SDS数据结构主要是通过三个字段来确定一个字符串的
struct sdshdr { int len; int free; char buf[]; }
其中len表示buf数组已经被使用的长度,free表示buf数组剩余的可用字节数,buf则是数据空间存储数据。len和free被称为头部,通过头部可以很方便的得到字符串的长度。
字符串数据存放在buf数组中,SDS对上层暴露的不是SDS结构体的指针,而是指向buf数组的指针,因此上层可以想读取C字符串一样读取SDS中字符串的内容,同时也兼容了C语言处理字符串的各种函数。
redis 3.2版本之前使用的buf数组优点在于数组是连续的,不需要额外通过指针找到字符串的位置,而是直接通过指针和偏移量就可以找到数据。但是这样设计也存在缺点:不同长度的字符串占用了相同大小的头部,显然是不太好的,一个int类型占4字节,在实际的应用中存放于redis的字符串长度往往没有这么长,每个字符串都用4个字节存储未免太浪费空间。
-
3.0之后redis 改进(version=6.2)
redis根据字符串的长度分成了5种数据类型sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中sdshdr5没有了头部(len 和 free),低三位表示type,高五位预留。其他四种数据结构多了一个flags字段。
如上图,sdshdr5结构中,flags占1个字符,其第三位表示type,高5位表示长度,能表示的长度区间为0~31,flags后面的就是字符串的内容。
当长度大于31的字符串,1个字节存不下,那么就要将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,如下图所示:
四个字段的含义如下:
- len,表示buf中已经占用的字节数;
- alloc,表示buf中已经分配字节数,记录的是buf分配的总长度,不同于free;
- flags表示当前结构体的类型,低三位做标识位,高5位预留;
- buf,为真正存储数据的空间
创建SDS的大致流程是,首先计算好不同类型的头部和初始长度,然后动态分配。不过需要注意下面几种情况:
- 创建空字符串时SDS_TYPE_5会被强制转换为SDS_TYPE_8(下面空间预分配预分配部分3.2版本之后代码中会提及);
- 长度计算时有“+1”操作,是为了算上结束符“0”
- 返回值是指向sds结构buf字段的指针
注意:sdshdr5的数据结构只负责存储小于32字节的字符串。一般情况下,小字符串的存储更普遍,所以redis进一步压缩了sdshdr5的数据结构,将sdshdr5的类型和长度放入了同一个属性中,也就是flags的低3位表示存储类型,高5位表示存储长度。创建空字符串时,sdshdr5会被sdshdr8替代。
注意: 源码的__attribute__((__packed__))
。结构体会按照所有变量中最宽的基本数据类型做字节对齐,但redis使用packed
修饰后,结构体则变为按1字节对齐。以sdshdr32为例,修饰前按4字节对齐大小为12 = 4(len) + 4(alloc) + 4(flags, flags原本为1byte,但此时会被填充为4byte)
;使用packed
修饰之后按1byte对齐,也就是9 = 4(len) + 4(alloc) + 1(flags)
,节省了3个byte。
除了节省空间外,使用packed
修饰也不需要对不同的结构类型进行处理,实现变得更为简洁。因为此时是按照1byte对齐,所以SDS创建成功后,无论是sdshdr8、sdshdr16 还是 sdshdr32,都能通过 (char*)sh+hdrlen
得到 buf 指针地址(其中 hdrlen 是结构体长度,通过 sizeof 计算得到)。修饰后,无论是 sdshdr8、sdshdr16 还是 sdshdr32,都能通过 buf[-1] 找到 flags。
3、扩容及内存分配
3.1、额外未使用空间分配策略
当SDS的API对SDS进行修改,并且需要对SDS进行空间扩展的时候,redis不仅会为SDS分配修改所必需要的空间,还会为SDS分配额外的未使用空间
额外分配的未使用空间分下面两种情况:
- 如果对SDS进行修改后,SDS的长度小于1MB,则分配和
len
属性同样大小的未使用空间 - 如果对SDS进行修改之后,SDS的长度大于1MB,则会分配1MB的未使用空间
通过预分配策略,redis减少了连续执行字符串增长操作所需的内存重分配次数,类似于以空间换时间
3.2、惰性空间释放
惰性空间释放用于优化SDS字符串缩短操作,当SDS字符串执行’缩短’操作时,程序并不会将废弃的内存进行回收,而是通过修改free
值,在将来字符串再次执行扩充操作时直接复写之前废弃掉的内存中。
这种操作避免了缩短字符串所需内存时的重分配策略,并为将来可能有的增长操作提供了优化。
3.3、扩容函数
3.0版本
sds sdsMakeRoomFor(sds s, size_t addlen) { // 参数:sds(原来的,未添加新字符串之前) 字符串 s 和 扩容长度 addlen(新字符串长度)
struct sdshdr *sh, *newsh; //定义两个 sdshdr 结构体指针
size_t free = sdsavail(s); // 获取 s 目前空闲空间长度
size_t len, newlen; // 定义两个长度变量,一个用于存储扩展前 sds 字符串长度,一个用于存储扩展后 sds 字符串长度
if (free >= addlen) return s; // 如果 s 目前的剩余空闲空间已经足够,无需再进行扩展,直接返回
len = sdslen(s); // 获取 s 目前已占用空间的长度
sh = (void*) (s-(sizeof(struct sdshdr))); //结构体指针赋值
newlen = (len+addlen); // 字符串数组 s 最少需要的长度
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC) // 如果新长度小于 SDS_MAX_PREALLOC,那么为它分配两倍于所需长度的空间
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
3.2版本及之后
sds sdsMakeRoomFor(sds s, size_t addlen) { // 参数:sds(原来的,未添加新字符串之前) 字符串 s 和 扩容长度 addlen(新字符串长度)
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK; // s[-1]为flags值,SDS_TYPE_MASK = 7,取 & 操作可以得到flags字段的低三位进而得到type值
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);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
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; // type 5 不支持空格,所以当初始化为空字符串时转为type 8
hdrlen = sdsHdrSize(type);
if (oldtype==type) { // 扩展后type为改变
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else { // 扩展后type改变
/* 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;
}
流程图如下: