【Redis】深入理解 Redis 常用数据类型源码及底层实现(3.详解Strin数据结构)

从这篇开始,我们结合源码依次解析下String、Hash、List、Set、ZSet这五大数据结构,先看下object.c中各个类型的数据结构的编码映射和定义:

String数据结构

三大物理编码介绍

type都是string,但是encoding不同

redisObject内部对应三大物理编码:

  • int:保存长整型(long)的64位(8个字节)的符号整数

    • 只有整数才会使用int,如果是浮点数,Redis内部会先将浮点数转换为字符串值,然后再保存

    • 最小值是-2^63(-9,223,372,036,854,775,808)

    • 最小值是2^63-1(9,223,372,036,854,775,807)

    • 默认值是0L

  • embstr:保存长度小于44字节 的字符串或者长度大于19 的整数(代表embstr格式的SDS(Simple Dynamic String 简单动态字符串))

    • embstr即embedded string,表示嵌入式的String
  • raw:保存长度大于44字节的字符串

SDS(Simple Dynamic String)简单动态字符串

Redis中字符串的实现SDS有多种结构(sds.h)

它们分别用于存储不同长度的字符串,从上图源码中可以看到,主要有4个参数:

  • len表示SDS字符串的长度,使我们在获取字符串长度的时候可以在O(1)的情况下拿到,而不是像C语言一样要遍历一遍字符串
  • alloc可以用来计算free(就是字符串已经分配的未使用空间),有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题
  • flags表示SDS的类型
  • buf表示字符串的字节数组(真正存数据的)
Redis为什么要重新设计一个SDS的数据结构?

C语言没有Java里面的String类型,只能是靠自己的char[]来实现,想要获取字符串的长度,需要从头开始遍历,直到遇到'\0'为止,所以Redis没有直接使用C语言传统的字符串标识,而是自己构建了一种名为简单动态字符串的抽象类型,并将SDS作为Redis默认字符串。

我们可以简单对比下C语言中的字符串和SDS之间的区别

C语言 SDS
字符串长度处理 需要从头开始遍历,直到遇到'\0'为止,时间复杂度O(N) 记录当前字符串的长度,直接读取即可,时间复杂度O(1)
内存重新分配 超出分配的内存空间后,会导致数组下标越界/内存分配溢出 1.空间预分配 (SDS修改后,len长度小于1M,那么将会额外分配len相同长度的未使用空间。如果修改后大于1M,那么将会分配1M的使用空间)2.惰性空间释放(有空间分配对应就会有空间释放,SDS缩短时并不会回收♻️多余的内存空间,而是使用free字段将多出来的空间记录下来,如果后续有变更操作,直接使用free中记录的空间,减少内存的分配操作)
二进制安全 二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如'\0'等(前面提到过遇到'\0'会结束读取,有可能会导致'\0'后面的数据读取不到) 根据len的长度来判断字符串是否结束,就解决了二进制安全的问题

源码分析

在执行set key value命令时,底层到底做了些什么?

我们打开Redis源码src目录下的t_string.c文件,里面有一个名为setCommand()的方法

setCommand()方法中有两个重要的方法:tryObjectEncoding()和setGeneticCommand()

tryObjectEncodingEx()方法中调用了tryObjectEncodingEx()方法

在tryObjectEncodingEx()方法中会调用sdslen()方法获取字符串的长度,接着进行判断,如果字符串长度小于等于20并且字符串转long型成功则作为long型存储,配置server.maxmemory并且当值在[0,OBJ_SHARED_INTEGERS)之间时会直接使用共享对象值(如下图,OBJ_SHARED_INTEGERS的值为10000)

INT编码格式

当字符串键值的内容可以一个64位有符号整型来表示时(比如 set k1 123),Redis就会将键值转化为long型来储存,此时对应的是OBJ_ENCODING_INT编码类型,内部的内存结构表示如下:

Redis启动时会预先建立 10000 个分别储存 0-9999 的redisObject 变量作为共享对象,这就意味着如果set字符串的键值在这个范围内,就可以直接指向共享对象,而不需要再创建新对象(此键值不占空间)

比如:

set k1 123

set k2 123

我们看下源码执行流程

在进入到robj *tryObjectEncodingEx()方法中

当字符串的长度小于等于20并且转换成long型成功就会进入到下图中红框框内的逻辑

从上面代码中可以看到配置maxmemory(server.maxmemory == 0表示操作系统最大值)并且值在10000以内,则直接使用共享对象值

c 复制代码
decrRefCount(o);
return shared.integers[value];

EMBSTR编码格式

可以看到当字符串的键值为长度小于等于44的字符串 时,Redis内部的编码方式为OBJ_ENCODING_EMBSTR,表示嵌入式的字符串,即字符串SDS结构体与其对应的redisObject对象分配在同一块连续的内存空间,就像是字符串SDS嵌入到redisObject对象之中一样(如下图)

其实这一点我们在源代码中也可以看出(sh+1:紧挨着)

RAW编码格式

可以看到当字符串的键值为长度大于44的超长字符串时,Redis就会将内部的编码方式改为OBJ_ENCODING_RAW的格式,OBJ_ENCODING_RAW与OBJ_ENCODING_EMBSTR的区别在于**OBJ_ENCODING_RAW** 的动态字符串SDS的内存与其依赖的redisObject的内存不再连续,如下图所示

值得注意的是:修改后的对象一定是raw(无论长度是否超过44),判断不出来就取最大的raw

转变逻辑图

总结

只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值,然后再保存。

embstr与raw类型底层的数据结构其实都是SDS(简单动态字符串,Redis内部定义sdshdr一种结构)

区别如下:

int Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。
embstr 当保存的是字符串数组且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间 ,空间中依次包含redisObject与sdshdr两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片。
raw 当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含redisObject结构,而另一块用于包含sdshdr结构。

三种编码方式图像对比( ̄∇ ̄)/

Redis的String类型强大的原因:

SDS简单动态 字符串数据结构 + 3大物理编码方式 + 合理的逻辑转换

Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择优化的内部编码格式,而这一切对用户完全透明。

相关推荐
IT大白鼠3 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙4 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除4 天前
redis--018
redis
两锭子4 天前
Redis基础
redis
烟沙九洲4 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索4 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞4 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路4 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog4 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis