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

文章详细介绍了Redis中的String数据对象,包括其底层结构SDS的实现,讨论了SDS的二进制安全性、动态预分配策略以及不同长度字符串的内存优化。此外,还提到了Redis对象的编码方式如int、raw和embstr,以及它们之间的区别和内存分配策略。

【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

RedisString 类型的底层实现并不是直接使用 C 语言中的传统字符串(即以空字符 `\0` 结尾的字符数组),而是采用了 Redis 自行设计的简单动态字符串(SDS,Simple Dynamic String结构[^1]。 ### SDS 的结构 SDS 结构由三个主要部分组成:当前字符串的长度(len)、当前字符串的空闲空间(free)以及实际存储字符的字节数组(buf)。这种结构设计使得 SDS 能够在字符串修改时避免频繁的内存重分配操作,从而提高性能[^3]。 以下是 SDS 的基本结构示例: ```c struct sdshdr { int len; // 当前字符串长度 int free; // 空闲空间大小 char buf[]; // 字符数组 }; ``` ### 编码方式 RedisString 类型根据字符串的长度和内容使用不同的编码方式来优化存储效率。常见的编码方式包括: 1. **int**:如果字符串可以表示为一个长整型(long)整数,则 Redis 使用 `int` 编码,直接存储该整数而不需要 SDS 结构。 2. **embstr**:当字符串长度小于等于 44 字节时,Redis 使用 `embstr` 编码。这种编码方式一次性分配 64 字节的空间,其中前 19 字节存储 SDS 的结构体,接下来的 44 字节用于存储字符串内容,剩余空间作为空闲空间。 3. **raw**:当字符串长度大于 44 字节时,Redis 使用 `raw` 编码。此时 SDS 的内存其依赖的 `redisObject` 的内存不再连续[^3]。 ### 普通字符串的区别 SDS 相较于传统的 C 语言字符串具有显著的优势: - **内存管理**:SDS 在修改字符串时能够动态扩展内存,减少了频繁的内存分配操作,提高了效率。而 C 语言字符串在修改时可能需要多次内存分配。 - **安全性**:SDS 在读取或修改字符串时会检查长度,从而避免越界错误。而 C 语言字符串不具备这种安全性,容易发生越界问题。 - **存储能力**:SDS 支持动态扩展,可以存储任意长度的字符串。而 C 语言字符串的大小在创建时固定,无法动态扩展。 - **数据类型**:除了存储常规字符串外,SDS 还支持存储整数类型(当整数可以表示为 long 类型时),此时直接使用 `int` 编码存储整数,提高了效率。C 语言字符串只能存储字符序列,无法直接存储整数。 - **操作接口**:SDS 提供了丰富的操作接口,如 `SET`、`GET`、`INCR` 等,方便对字符串进行各种操作。而 C 语言字符串的操作接口相对较少,通常只能进行简单的读取、修改和比较等操作[^3]。 ### 应用场景 在 Redis 中,所有的键(key)都是由字符串对象实现的,而这些字符串对象的底层实现就是 SDS。同时,值(value)中包含的字符串对象也由 SDS 实现[^4]。此外,每个键值对都会有一个 `dictEntry` 对象,类似于 Java 中 `Map` 的 `Entry`[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值