Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。
在 Redis 里面, C 字符串只会作为字符串字面量(string literal), 用在一些无须对字符串值进行修改的地方, 比如打印日志.
当 Redis 需要的不仅仅是一个字符串字面量, 而是一个可以被修改的字符串值时, Redis 就会使用 SDS 来表示字符串值
1.SDS的定义
struct sdshdr { // 记录 buf 数组中已使用字节的数量, 不包括 '\0' 的长度
// 等于 SDS 所保存字符串的长度
int len; // 记录 buf 数组中未使用字节的数量
int free; // 字节数组,用于保存字符串
char buf[]; };
buf [ ] 除了保存字符串的字符外, 还会在末尾保存一个空字符 '\0' , 空字符不计算在 len 属性之中.
遵循空字符结尾的好处是可以重用一部分C字符串的函数.
2.SDS与C字符串的区别
2.1 常数复杂度获取字符串长度
C字符串不记录自身的长度信息, 获取字符串长度时会遍历字节数组, 直到遇到空字符为止. 复杂度为 O(N)
SDS直接通过 len 属性获取字符串长度. 复杂度为O(1)
2.2 杜绝缓冲区溢出
C字符串不记录自身长度, 修改字符串时不会判断本身是否拥有足够的内存空间, 当内存空间不足时, 则会造成缓冲区的溢出.
SDS对字符串进行修改时,先检查内存空间是否满足修改的需要, 若不满足, 则自动扩展SDS的内存空间. 所以使用SDS既不需要手动修改内存空间的大小, 也不会出现缓冲区溢出的情况.
2.3 空间预分配
第一次创建字符串对象时, SDS不会分配冗余空间, 即 len = 0
当SDS的API修改SDS时, 则会为其分配冗余空间.
- 当修改后的SDS的 len 属性小于1MB时, 则为其分配和 len 同样大小的冗余空间, 即 free = len, 此时 buf [ ] 的实际长度 = len(实际长度) + free(冗余空间) + 1(空字符)
- 当修改后的SDS的 len 属性大于等于1MB时, 则为其分配1MB的冗余空间. buf [ ] 的实际长度 = len(实际长度) + free(1MB) + 1(空字符)
2.4 惰性空间释放
SDS的API缩短SDS的字符串时, 不会立即使用内存分配回收缩短后多出来的字节, 而是记录在 free 属性中, 并等待将来使用.
2.5 二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
SDS的API都是二进制安全的.所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读 取时就是什么样。
这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。
3. 字符串的不同编码方式
传送门 : Redis 数据编码方式详解
为什么会有不同的编码方式,为了解释这种现象,我们首先来了解一下 Redis 对象头结构体,所有的 Redis 对象都有下面的这个结构头:
struct RedisObject {
int4 type; // 4 bits
int4 encoding; // 4 bits
int24 lru; // 24 bits
int32 refcount; // 4 bytes
void *ptr; // 8 bytes,64-bit system
} robj;
不同的对象具有不同的类型 type(4bit),同一个类型的 type 会有不同的存储形式 encoding(4bit),为了记录对象的 LRU 信息,使用了 24 个 bit 来记录 LRU 信息。
每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对象内容 (body) 的具体存储位置。
这样一个 RedisObject 对象头需要占据 16 字节的存储空间。
3.1 embstr
从Redis 3.0版本开始字符串引入了EMBSTR编码方式,长度小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39)的字符串将以EMBSTR方式存储。
EMBSTR方式的意思是 embedded string ,字符串的空间将会和redisObject对象的空间一起分配,两者在同一个内存块中。
Redis中内存分配使用的是jemalloc,jemalloc分配内存的时候是按照 8,16,32,64 作为chunk的单位进行分配的。
为了保证采用这种编码方式的字符串能被jemalloc分配在同一个chunk中,该字符串长度不能超过64,
故字符串长度限制OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof('0') - sizeof(robj)为16 - sizeof(struct sdshdr)为8 = 39。
采用这个方式可以减少内存分配的次数,提高内存分配的效率,降低内存碎片率。
3.2 raw
从len字段可以判断并不不依赖于'0',故可以用与保存二进制对象。
从free字段可以判断其空间分配是采用预分配的方式,避免字符串修改时频繁分配释放内存。
3.3 int
INT编码方式以整数保存字符串数据,仅限能用long类型值表达的字符串。
当robj中的LRU值没有意义的时候(实例没有设置maxmemory限制或者maxmemory-policy设置的淘汰算法中不计算LRU值时),
0-10000之间的OBJ_ENCODING_INT编码的字符串对象将进行共享。