【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-19,223,372,036,854,775,807

    • 默认值是0L

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

    • embstrembedded 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型成功就会进入到下图中红框框内的逻辑

从上面代码中可以看到配置maxmemoryserver.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_RAWOBJ_ENCODING_EMBSTR的区别在于**OBJ_ENCODING_RAW** 的动态字符串SDS的内存与其依赖的redisObject的内存不再连续,如下图所示

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

转变逻辑图

总结

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

embstrraw类型底层的数据结构其实都是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内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择优化的内部编码格式,而这一切对用户完全透明。

相关推荐
minihuabei1 小时前
linux centos 安装redis
linux·redis·centos
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
hlsd#4 小时前
go 集成go-redis 缓存操作
redis·缓存·golang
奶糖趣多多6 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt7 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
ketil2711 小时前
Redis - String 字符串
数据库·redis·缓存
王佑辉13 小时前
【redis】延迟双删策略
redis
生命几十年3万天13 小时前
redis时间优化
数据库·redis·缓存
Shenqi Lotus15 小时前
Redis-“自动分片、一定程度的高可用性”(sharding水平拆分、failover故障转移)特性(Sentinel、Cluster)
redis·sentinel·cluster·failover·sharding·自动分片·水平拆分
YMY哈18 小时前
Redis常见面试题(二)
redis