【Redis数据对象与结构】string与其底层结构
【Redis数据对象与结构】系列的主线如下,本文主要讲解string数据对象及其底层结构在redis中的实现。
redis中基本的数据对象有字符串类型(String)、列表类型(List)、字典类型(Hash)、集合类型(Set)、有序列表类型(SortedSet),在5.0后,redis又增加了一个新的数据对象——流类型(Stream)不是steam。接下来分别介绍每个数据对象的使用、底层编码及基本命令。
redis中的数据对象结构如下,我们现在只需要记住,这个对象的大小为 16字节。
typedef struct redisObject {
unsigned type:4; // 对象的类型,4bit
unsigned encoding:4; // 对象的编码,4bit
unsigned lru:LRU_BITS; // 先不管,24bit
int refcount; // 先不管,32bit
void *ptr; // 对象的值的指针,64bit
} robj;
对象的类型:每个对象都有其类型,使用type xxx
可获得xxx
的类型。类型为数据对象五大类型中的一种。
对象的编码:对应类型的实现方式,使用object encoding xxx
可获得xxx
的类型。也就是这个对象使用了什么数据结构作为对象的底层实现。
简单字符串(SDS)
redis中实现字符串类型的一种编码实现方式。redis中的SDS的结构如下
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
- SDS基于C语言的字符串,保留了以
\0
为结尾的惯例,这可以重用C字符串库里的一部分函数。但是真正读取字符串的值的时候,是以len
为准的。 - SDS中对于末尾
\0
的添加,对于len
的计算与更新,对SDS的使用者来说是完全透明的。
SDS是二进制安全的吗?为什么?
看到有free
字段,就如同go slice中的cap一样,肯定是存在动态预分配的。策略都是类似的:
- 当更新后的
len
小于1MB,那么扩容一倍。 - 当更新后的
len
大于1MB,那么扩容1MB。 - 如果更新后的存储类型改变了,就直接开辟新的内存,并将原字符串的内容移动到新位置。
顺便了解一下go slice的扩容机制。
- 当大于扩容后的时,如果小于1024时,新的容量是扩容前容量的2倍。
- 当大于扩容后的时,如果大于1024时,新的容量是扩容前容量的1.25倍,即以0.25增加。
- 然后根据新的容量以及数据结构的类型大小,进行内存对齐,算出真实的新容量。
等等?存储类型?不都说了是使用SDS了,为什么扩容还有什么存储类型的改变?
原来是redis嫌结构体中的len
和free
字段也占两个int大小,心疼,所以要对SDS进行去肥增瘦。具体怎么做的大概如下:
- 将字符串根据长度进行分类。
- 短字符串,
len
和free
长度1字节就够了,长字符串用2字节,更长的用8字节。
- 短字符串,
- 如何区分这些类型?引入一个字段
flags
,来表示对应的字符串是短的还是长的还是超长的。 - 但是这样又多一个字段了,咋办,没关系,这个字段只有1字节,一来一回就省下了。
所以,在redis 3.2后,SDS有以下5种存储类型,当这五种类型在扩容时,因为长度改变导致存储类型改变了,就直接开辟新的内存即可。
最后看一下SDS对外提供的常用API,SDS暴露对外的是指向buf
数组的指针。
String
字符串对象的内存模型大概为(图中是raw编码的内存模型,其他类型差别不大):
字符串对象还是redis数据对象中唯一一种会被其他类型嵌套的对象。
字符串是大家最常用的redis对象,基本的命令为:
set
、get
、setnx
等设置单个kv和超时时间- mset、mget、msetnx等设置多个kv和超时时间
- append、setrange等用于对单个kv做改动
- incr、decr、incrby、decrby、incrbyfloat等用于原子计数
- setbit、getbit、bitcount、bitpos等字符串位操作
字符串对象的编码可以是:int、raw或embstr
- 当一个字符串保存的是整数值且可以用long类型表示时,其底层编码为int。
- 当一个字符串的长度小于等于39字节时,其底层编码为embstr,使用SDS来保存该值。
- 当一个字符串的长度大于39字节,其底层编码为raw,也是使用SDS来保存该字符串值。
上述的分界线32字节在不同版本不同,3.2之前是39,3.2版本是44。
此处就有一个疑点:
编码为embstr时,使用SDS来实现,编码为raw时,也是使用SDS来实现。老周树人了。所以embstr与raw的区别在哪,好处在哪?
embstr与raw都是使用redisObject,然后redisObject中的ptr指针指向SDS,区别在于,raw编码的字符串对象会调用两次内存来分别创建redisObject和SDS结构,而embstr中的内存模型如下,redisObject和SDS结构是连续存储的,一次内存分配即可。这样可以更好地利用局部性原理。
此处又有一个疑点了,3.2版本之后,embstr与raw的分界线是44。这个数字很可疑呀,我们刚刚才说过,SDS有5个版本,最小的sdshdr5
的最大容量稍微计算一下,也才31个字节的长度,当超过这个长度就要使用sdshdr8了,使用sdshdr8
的时候,len
字段有1B,说明sdshdr8
可容纳128个字节的长度,那为啥搞这个不零不整的44作为分界线?
如果在小于31字节用sdshdr5
的话,大于31字节后就要改变类型,重新分配,而因为embstr的redisObj与SDS结构体是连续的,重新分配的开销更大。
所以,embstr的底层结构是sdshdr8
,为什么使用44作为分界线?因为redis将redisObj与SDS结构体使用malloc方法一次分配,当使用sdshdr8
时,embstr最小占用空间为3字节,加上redisObj的16字节,一共19字节,再选一个合适的大小,比如64字节,也就是redis一次性申请64字节的空间给embstr,64-19-1=44,这样44就出现了。为什么再-1?因为尾部有一个\0
。这很redis,很节省。
等等?为什么是64字节?我的理解是:可能是cache大小。
再等等?那sdshdr5
呢?这样一看,sdshdr5
没有用武之地了?
对的,在讲SDS的时候,源码没贴全: