【Redis数据对象与结构】string与其底层结构

【Redis数据对象与结构】string与其底层结构

【Redis数据对象与结构】系列的主线如下,本文主要讲解string数据对象及其底层结构在redis中的实现。

img

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的类型。类型为数据对象五大类型中的一种。

img

对象的编码:对应类型的实现方式,使用object encoding xxx可获得xxx的类型。也就是这个对象使用了什么数据结构作为对象的底层实现。

img

简单字符串(SDS)

redis中实现字符串类型的一种编码实现方式。redis中的SDS的结构如下

struct sdshdr {
    //记录buf数组中已使用字节的数量
    //等于SDS所保存字符串的长度
    int len;
    //记录buf数组中未使用字节的数量
    int free;
    //字节数组,用于保存字符串
    char buf[];
};

img

  • 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嫌结构体中的lenfree字段也占两个int大小,心疼,所以要对SDS进行去肥增瘦。具体怎么做的大概如下:

  • 将字符串根据长度进行分类。
    • 短字符串,lenfree长度1字节就够了,长字符串用2字节,更长的用8字节。
  • 如何区分这些类型?引入一个字段flags,来表示对应的字符串是短的还是长的还是超长的
  • 但是这样又多一个字段了,咋办,没关系,这个字段只有1字节,一来一回就省下了。

所以,在redis 3.2后,SDS有以下5种存储类型,当这五种类型在扩容时,因为长度改变导致存储类型改变了,就直接开辟新的内存即可。

imgimg

最后看一下SDS对外提供的常用API,SDS暴露对外的是指向buf数组的指针。

img

String

字符串对象的内存模型大概为(图中是raw编码的内存模型,其他类型差别不大):

img

字符串对象还是redis数据对象中唯一一种会被其他类型嵌套的对象。

字符串是大家最常用的redis对象,基本的命令为:

  • setgetsetnx等设置单个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。

img

此处就有一个疑点:

编码为embstr时,使用SDS来实现,编码为raw时,也是使用SDS来实现。老周树人了。所以embstr与raw的区别在哪,好处在哪?

embstr与raw都是使用redisObject,然后redisObject中的ptr指针指向SDS,区别在于,raw编码的字符串对象会调用两次内存来分别创建redisObject和SDS结构,而embstr中的内存模型如下,redisObject和SDS结构是连续存储的,一次内存分配即可。这样可以更好地利用局部性原理。

img

此处又有一个疑点了,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大小。

img

再等等?那sdshdr5呢?这样一看,sdshdr5没有用武之地了?

对的,在讲SDS的时候,源码没贴全:

img

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值