String四连问

1.String 是什么

字面意思,就是字符串,只不过底层编码方式有三种

  • OBJ_ENCODING_INT redis 【当整数数值超出64位有符号整数的表示范围时,Redis会将其存储为字符串类型的值 RAW格式

  • OBJ_ENCODING_EMBSTR

  • OBJ_ENCODING_RAM - 【SDS 实现的数据组织方式】

2.String 使用场景

字符串一般用于短消息(其实也不短了,最大单个value是512M[可修改proto-max-bulk-len改变阈值]), 或者用于文本协议的数据通信比如传Json 格式的业务数据。

3.常用操作

看图说话 多ky 批量操作SETNX/MGET 其他是单件值对操作

4.底层实现

4.1三种编码方式

  • OBJ_ENCODING_INT: value 是64位有符号整数时直接使用数值变量直接存储 value值。

  • OBJ_ENCODING_EMBSTR: 以成片连续小块内存存储value, 关键字EMB 即是嵌入的意思,类似C++ vector 预开辟6个元素与到栈上等着存数据。

  • OBJ_ENCODING_RAW: 链式存储数据【..._INT 值溢出或者变为浮点时,会直接转化为字符串RAW 存储】

三种格式大致转换关系见下图:

4.2 为什么要设计SDS数据结构

redis 使用的是C开发的,所以字符串系统自然也就是 C 的 char[]/char* 系列,但这套字符串系统过于底层且存在三大缺陷以致无法满足redis 的项目需求。所以才设计出SDS数据结构 (更确切的说是redis 高速的内存操作场景催生了此需求),C的字符系统在Redis项目存在的三大不足如下:

  • C 面向过程,单用连续内存块存储字符,无其他辅助属性作描述是每次获取真实字符长度都需要扫描整段内存地址中的数据才能知晓其真正数据长度,时间代价O(N),Redis 希望更快。

  • 对于字符串系统char[]/char* 变量赋值即确定装字符串的内存块大小,后续若追加内容则需重新分批。Redis 在高频场景下不能接受此代价 -- 频繁销毁/申请内存代价是非常大的(具体参考操作系统用户态-内核态切换 glibc内存分配策略/操作系统内存管理策略)

  • 字符都已'\0' 作结尾表示字符串标间,二进制不安全。

所以Redis 决定自己设计一个SDS数据结构解决上面的问题。以下是sdshdr8源码的摘抄 (同样设计的还有 sdshdr5(较新版本已废弃)/sdshdr16/sdshdr32/sdshdr64):

// from Redis 7.0.8

struct attribute ((__packed__)) sdshdr8 { 

    uint8_t len; /* used */

    uint8_t alloc; /* excluding the header and null terminator */

    unsigned char flags; /* 3 lsb of type, 5 unused bits */

    char buf[]; 
};

// before redis version 3.2
struct attribute ((__packed__)) sdshdr { 

    unsigned int len; /* used 4 bytes*/

    unsigned int free; /* 4 bytes */

    char buf[];  /* '\0' 1 bytes */

};
  • len :实际数据存储长度 -- 解决了上面统计字符串长度的问题 -- 解决了'\0' 二进制安全问题

  • alloc : 预分配内存大小 -- 方便一定量的字符串后续追加/动态扩容

  • flages :属性标记位 sdshdr5(较新版本已废弃)/sdshdr8/sdshdr16/sdshdr32/sdshdr64

#define SDS_TYPE_8  1

  • buf :数据块预分配内存段

alloc预分配规则:

预分配规则根据初始传入value 长度分为两种情况

Case A - strlen(valueA) < 1M : alloc -> 2*strlen(valueA) 【两倍长度大小空间 - 预留传入值1倍长度】

Case B - strlen(valueA) > 1M : len + 1M 【字符符长度 + 1M - 预留1M】

为了追求极致的性能,Redis 类似的设计 还会在后续出现很多次,预分配只是其中一种手段,另一种是超过阈值后的类型转换和引入缓存存储-淘汰 策略.


interview

好~键盘打工人上线啦,这次我们来到成都天府国际...串台了,(其实是redis-string interview 部分...)

1.Set一个已有的key对会发生什么?

  • 覆盖原有值,设置有TTL(过时时间 - 生存周期)会取消 (这里可以顺带将存储 指令执行中发生什么,这么多string KV是怎么组织的,怎么找到这对KV的,数据过长后怎么覆盖的,(比原理变更短不会作转换))

2.浮点在Redis String 中用什么表示(存储)?

  • 字符串,也就是存在char buffer 里,先存embstr 要是再出现修改会 转换为 RAW -- Redis String 只为 INT64 (有符号的) 做过优化,在范围内用INT64变量直接存,溢出或者被修改为其他类型会之际用 RAW存.

3.String可以有多大?

  • 512M,官网有说,可以在配置文件修改 (proto-max-bulk-len -- 记不清可以不说这个配置字段名...),更老的版本不可修改(直接写死的)。

4.Redis 字符串怎么实现?

先偷个图在回答问题:

  • Redis String 由 RedisObject(redis对象) 和 encodingStruct(编码结构) 组成,RedisObject 里有一个 void* ptr 指向 SDS 的开始地址,通过传入value值长度决定选取那种EncodingType 取高效合理存储数据比如这里String,当传入value 是(有符号)INT64 范围值时,redis 会用OBJ_ENCODING_INT 方式 取用一个int64变量存储数据,当传入数据不是int64的数据则会选择embstr 最为第一存储如果传入value长度本身已经超过embstr阈值44(3.2版本是分水岭,3.2前是39,3.2 之后是44)则会直接选择raw 格式,若第一次使用的编码格式是_INT/embstr再次改动则会变为 raw 编码格式 (redis 的编码格式转换不可逆)

5.你知道为什么EMBSTR 阈值是44吗?

  • 看上面偷的图,redis 使用的allocor (内存分配器) jemalloc (主流三大内存分配器:glibc[ptmalloc2] google[tcmalloc] facebook[jemalloc]) 配置默认内存块trunk大小是64 也就是所,redis 在获取内存块时jemalloc 能从小内存块linkPool 中分配的最小块就是64字节的;embstr 采用的成员属性位数(占bit)最小的uint8(sdshdr8),这是就会有:

sizeof(redisObject) = (4 + 4 +24)bit/8 + 4bytes + 8 bytes = 16bytes(64位机下)

sdshdr8 : 1 + 1 + 1 (flags) + 1 (char [] 编译器填充占位符 '\0' 因为编译器不允许存在 长度为0 的关键字或者说数据结构) (bytes)

64 - 16 - 4 = 44 (bytes)

  • 所以传入的value string strlen(value) <= 44 则会使用embstr,再次修改则会直接在value list 加入新值删去旧值,而不是直接原地覆盖.(为了块而设计的,因为删除可以在后台慢慢删,业务要紧)

6.你知道为什么EMBSTR 曾经的阈值是39吗?

  • 回答同上,3.2之前只有 sdshdr 一个sds结构并未有作容量细分也就是还没有 sdshdr5/8/16/32/64 (embstr 为了局部原理以及和jemalloc 对齐配置 64 - 为最小内存块) 是没有 flags 这个属性的 所以是 64 - 16 - (4 len + 4 free + 1‘\0')= 39.

7.SDS有什么用?

叠甲防杠: 这是为redis 业务设计的东西当然只能说是对redis

  • 接近面向对象的做法,引入成员属性使得获取长度不需要扫描数据段仅用 O(1),数据尾部截短仅需修改len 更快O(1)

  • 引入成员属性使得获取长度 摆脱原生C 字符串 用'\0' 二进制安全

  • 预留空间,追加更方便