Redis数据类型之String详解
String的实现
String类型在日常工作中大家用到的是最多的,但是我们的使用只是最表层皮毛。Redis内部做了极多的优化调整,大幅度节省内存使用、增加查询效率。我认为作为开发人员是有必要了解并学习其设计理念与实现方案。
sds源码阅读
- Redis 3.2之前,只有一个sds类型
3.2之前的版本现在用的太少了,接下来所有内容均为3.2及以上版本
struct sdshdr {
int len;//字符串已使用长度(实际长度)
int free;//剩余可使用长度(len+free = 实际分配的内存空间)
char buf[];//字符串数组
};
- Redis 3.2及之后,sds扩展了5种类型分别存放不同长度的字符串。在创建字符串时,sdsReqType 方法会根据字符串的长度选择不同的类型
/* 根据当前字符串长度,获取当前字符串需要使用哪种sds类型存储 */
static inline char sdsReqType(size_t string_size) {
if (string_size < 32) //2^5 -1
return SDS_TYPE_5;
if (string_size < 0xff) //2^8 -1
return SDS_TYPE_8;
if (string_size < 0xffff) // 2^16 -1
return SDS_TYPE_16;
if (string_size < 0xffffffff) // 2^32 -1
return SDS_TYPE_32;
return SDS_TYPE_64;
}
/* sdshdr5 逻辑上用于存储小于32位的字符串
* 但是在sdsnewlen方法(创建字符串的方法)中存在如下判断if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
* 并且博主验证好像value值长度不为0且小于32也不会使用sdshdr5 而是使用sdshdr8(如有错误希望大佬们多多指正)
* 但是redis的Key长度 如果小于32位是会使用sdshdr5 这个类型的
*/
struct __attribute__ ((__packed__)) sdshdr5 而是使用 {
unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 实际使用的长度(当前字符串长度)
uint8_t alloc; // 分配的长度,不包括标头和空终止符
unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint8_t len; // 实际使用的长度(当前字符串长度)
uint8_t alloc; // 分配的长度,不包括标头和空终止符
unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint8_t len; // 实际使用的长度(当前字符串长度)
uint8_t alloc; // 分配的长度,不包括标头和空终止符
unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint8_t len; // 实际使用的长度(当前字符串长度)
uint8_t alloc; // 分配的长度,不包括标头和空终止符
unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
char buf[];// 用于存储字符串的char数组
};
sds设计优势
- Redis是通过C语言实现,C语言中String是不保存长度的,而是通过 \0 来标记字符串结束。通过len保存长度而不直接使用C语言的实现让Redis对其它语言的兼容性得到提升
但是为了兼容C语言的函数调用字符串数组依然使用了\0结尾
; - len 字段的存在,让字符串长度获取 (STRLEN)的时间复杂度降低至 O(1)
不需要遍历数组
; - alloc 字段的存在,让字符串的扩容变得更加简单和高效
Redis字符串的扩容为成倍扩容,但是当字符串在长度超过1024 * 1024 (1MB)之后每次扩容只会多分配 1MB的空间
- 成倍扩容 通过“空间预分配” 和“惰性空间释放”,防止多次重分配内存。
redis的String对象直接通过 set 等方法创建时,不会创建冗余空间,因为大多数情况下创建的String类型不需要进行追加截取等操作
redisObject对象
redisObject 是Redis类型系统的核心, 数据库中的每个键、值, 以及Redis本身处理的参数, 都表示为这种数据类型.
redisObject源码阅读
// redisObject对象 : string , list ,set ,hash ,zset ...
// 总空间(8 bit = 1 byte): 4 bit + 4 bit + 24 bit + 4 byte + 8 byte = 16 byte
typedef struct redisObject {
unsigned type:4; // 对象的数据类型 4 bit
unsigned encoding:4; // 对象的编码类型 4 bit,分别为OBJ_STRING、OBJ_ENCODING_INT或OBJ_ENCODING_EMBSTR
unsigned lru:LRU_BITS; // 最近一次的访问时间 24 bit ,用于LRU算法
int refcount; // 引用计数 4 byte
void *ptr; // 指向底层数据实现的指针 8 byte
} robj;
String的对象编码
int类型(REDIS_ENCODING_INT)
大家日常的Key Value 设值,表面上Redis给的都是String类型,特别是像我们在JAVA应用中使用 Jedis 的set、get命令对Value的 入参/出参 都是String类型,但是大家都知道内存中整数类型的存储空间使用是更小的,redis底层实现也有考虑到针对int类型的存储优化。
/* 尝试对字符串对象进行编码以节省空间 */
robj *tryObjectEncoding(robj *o) {
//我会用...省略掉许多其他的代码
...
//获取字符串长度
len = sdslen(s);
//字符串长度范围是否在整数值的表示范围内, 0 - 2^64,最多不超过20位 string2l方法是转换整数的方法
if (len <= 20 && string2l(s,len,&value)) {
//没有设置内存淘汰策略,且数字范围在缓存整数的范围内
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS)
{
decrRefCount(o); //不需要用额外得对象来存储
incrRefCount(shared.integers[value]);
return shared.integers[value]; // 共享对象
} else {
// 如果前一步不能使用共享小对象来表示,那么将原来的robj编码成encoding = OBJ_ENCODING_INT,这时ptr字段直接存成这个long型的值。
// 因此在64位机器上有64位宽度,正好能存储一个64位的整数值。 所以此刻ptr不需要存指针而是直接存整数值即可
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr); // 释放空间
o->encoding = OBJ_ENCODING_INT;
// 用整形编码
o->ptr = (void*) value;
return o;
} else if (o->encoding == OBJ_ENCODING_EMBSTR) {
decrRefCount(o);
return createStringObjectFromLongLongForValue(value);
}
}
}
...
return o;
}
int类型的存在直接使用 redisObject 的 ptr 来存储值,可以 减少一次IO ,同时也 节省了内存空间
- 通过object encoding命令 可以Redis查询指定Key的内部编码格式
embstr类型(REDIS_ENCODING_EMBSTR)
也是String类型,Redis 在保存长度小于 44 字节的字符串时会采用 embstr 编码方式,主要是因为现代处理器在读写内存时,为了提高DDR内存的读写效率,通常会连续读写一串内存 。同时每次读写都要填满一个 Cache Line ,所以每次内存读写都需要读写 64字节 (现在的一个 Cache Line 大小,很早以前是32字节)
/* 尝试对字符串对象进行编码以节省空间 */
robj *tryObjectEncoding(robj *o) {
//我会用...省略掉许多其他的代码
...
//获取字符串长度
len = sdslen(s);
// 数据长度 小于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 的话, 用 embstr 进行编码
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
...
return o;
}
已知处理器每次读取内存为 64 byte ,redisObject占用 16 byte ,sdshdr 基础信息(len、alloc、flags)占用 3 byte ,为兼容C语言 buf[] 数组用 \0 结尾占用 1 byte,64 - 16 - 3 - 1 = 44。所以当字符串长度 <= 44 时,直接申请一块连续的内存空间,存放 redisObject 与 sdshdr 。创建时少分配一次空间,删除时少释放一次空间。
- 通过object encoding命令 可以Redis查询指定Key的内部编码格式
raw类型(REDIS_ENCODING_RAW)
用于存储长度超过44位的字符串
- 通过object encoding命令 可以Redis查询指定Key的内部编码格式
Redis 命令参考手册:Redis常用命令传送门