//c语言声明字符串
char *s = "hello"
//本质是字符数组: {'h','e','l','l','o','\0'}
我们都知道,Redis的底层是用C语言写的,那么Redis为什么不直接使用C语言的字符串结构呢?
原因是因为C语言字符串存在很多问题:
- 获取字符串长度的需要通过运算
- 非二进制安全
- 不可修改
1.因为c语言字符串本质是字符数组,所以获取长度时需要遍历到 \0结束字符,才能获取字符串长度
2.且因为结束字符是\0,所以C语言不允许字符串中出现\0字符,所以是二进制不安全的
3.因为string通常指向字符串字面量,而字符串字面量存储位置是只读段,而不是堆或栈上,所以才有了string不可修改的约定。
Go语言string底层Go语言的string有三个特点:
- string是8bit字节的集合,通常是但并不一定非得是UTF-8编码的文本。
- string可以为空(长度为0),但不会是nil;
- string对象不可以修改。
src/runtime/string.go:stringStruct
type stringStruct struct {
str unsafe.Pointer //stringStruct.str:字符串的首地址;
len int //stringStruct.len:字符串的长度;
}
- stringStruct.str:字符串的首地址;
- stringStruct.len:字符串的长度;
所以字符串的结构和切片非常相像,只是缺少了切片的cap字段。事实上string和切片,准确的说是byte切片经常发生转换。因为本文的重点是讲述string底层结构,所以不展开[]byte和string的转换关系
Redis底层字符串结构SDSRedis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS。
Redis底层SDS源码(64位):
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:buf申请的总字节数,不包含结束标识
- flags:不同SDS的头类型,用来控制SDS的头大小
- buf:数据存储区,为了兼容C语言,尾部会有\0结束标志。但并不以其结束,而是以len长度结束,所以是二进制安全的
SDS的头类型大小
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
如上代码所示,redis的__attribute__ 结构体,其实分为5bit,8bit.....64bit位的多种类型。一般采用8bit位的1字节大小来存储头信息(长度,申请数,8bit就是255,若不够就用更大的类型)。
示例
我们假设在redis中操作
set name "虎哥"
底层就会自动创建 “name” 和 “虎哥”这两个字符串变量。所以说redis最常见的数据结构就是String
SDS的动态扩容
例如一个内容为“hi”的SDS:
假如我们要给SDS追加一段字符串“,Amy”,这里首先会申请新内存空间:
- 如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;
- 如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。
注意的是,这里alloc扩容到12,但因为是不包含结束符的,所以实际的空间里有13
总结
- 获取字符串长度的时间复杂度为O(1)
- 支持动态扩容
- 减少内存分配次数
- 二进制安全