面试官:说说Redis的SDS底层实现原理

前言

该文章已面试场景进行叙述,场景纯属编造,请勿当真!

大家好,我是徐志斌。

现在找工作真的好难,今天我早早的来到了面试现场,这家公司的HR小姐姐也是很热情的接待了我。

正当在会议室中沉浸在和HR姐姐聊天的喜悦中时,沉重的脚步声慢慢走来。只见一个身穿格子衫大哥颤颤巍巍走进来,一看就是技术大佬,今天的面试怕是凶多吉少。

面试开始

1、说说Redis基本类型有哪些

:我以为能问多难得问题,这种问题我还不是手到擒来 面试官:口气真是不小,老夫从业20年也不敢简历写精通Redis

Redis数据库常用的基本类型有5种,分别是:

  • String
  • List
  • Hash
  • Set
  • ZSet

2、String类型常用于什么场景

Redis的String类型使用的场景很多,例如:

  • 数据缓存
  • 计数器
  • 分布式锁
  • 分布式限流
  • 存储Token等时效信息
  • .....

使用起来非常简单高效,我个人在开发过程中,经常会使用到String类型结构进行功能实现,比如说存储验证码用户身份信息等。

3、String类型采用什么数据结构

:怎么突然就问到底层实现了?你这个跨度真是够离谱的! 面试官:汗流浃背了吧,老弟!

Redis数据库虽然是用C语言开发的,但是String数据类型的底层并不是直接使用C语言的字符串结构(以空格结尾的字符数组),而是使用简单动态字符串(SDS),这个类型可以被修改。

AOF的缓冲区客户端状态中的输入缓冲区都是由SDS实现的。

4、继续深入讲讲SDS实现细节

:虎躯一震,心里慌了但又故作镇定,毕竟这块我曾经还是认真学习过

SDS结构是这样的,C语言代码如下(我随手拿起笔写出下面代码):

c 复制代码
struct sdshdr {
	// SDS保存字符串长度(buf数组已使用字节数量)
	int len;
	// buf数组未使用字节数量
	int free;
	// buf字节数组,用于存储SDS字符串
	char buf[];
}

图中也可以看到,buf数组最后一个字节存储的是'\0',但这并不算入SDS的len属性SDS函数自动为该字节分配额外空间,并且添加到数组尾部,对于用户来说是无感知的。

好处:SDS可直接重用部分C语言字符串函函数(直接使用printf函数,无需为SDS编写专门打印的函数),后续5.5章节会提到。

5、你说说SDS和C语言字符串的区别

:呸呸呸!真该死啊,提C语言干嘛,这不给自己挖坑?

这个就说来话长了!

我先说C字符串吧,C语言的字符串是通过长度为N + 1的字符数组来表示,数组尾部总是空字符'\0',但是不满足Redis对字符串的高要求:安全、效率、功能等(这就是为什么Redis使用SDS的原因),C字符串结构:

它俩之间的主要区别有以下5点:

5.1、SDS获取长度时间复杂度更低

C语言字符串不记录自身长度信息,所以想要获取长度必须遍历整个字符串,该操作的时间复杂度为O(N)

SDS不需要从头遍历获取长度,之前SDS图中也提到有len属性,这样可以直接获取SDS的长度,获取操作时间复杂度为O(1)。设置、更新SDS长度操作是由SDS API自动完成。

Redis使用SDS将获取字符串长度所需要的复杂度从O(N)------>O(1),这样就不会影响Redis的性能。Redis的Key底层就是通过SDS来实现的,所以对一个非常长的字符串多次进行STRLEN命令也不会影响系统性能!

5.2、SDS杜绝缓冲区溢出

C语言字符串不记录自身长度信息,很容易造成缓冲区溢出。 例如:程序内存中存在两个相邻的C字符串,如图: 此时将S1内容修改为Redis Cluster,并且修改前并没有为S1分配足够的空间,此时修改后的结果如下:

SDS空间分配策略就不会出现上述情况:当SDS API需要对SDS进行修改,API会先检查SDS空间是否满足修改要求。如果不满足:SDS API自动将SDS空间扩展至修改所需的大小,SDS API中的sdscat函数就是这样的。

正如之前所说,SDS不需要手动修改SDS空间大小,并且不会出现缓冲区溢出问题。

5.3、SDS减少字符串内存重分配次数

C语言字符串增长、缩短操作都需要进行一次内存重分配

  • 增长:执行拼接操作前需要先通过内存重分配来扩展底层数组长度,如果没扩展就会产生缓冲区溢出
  • 缩短:执行截断操作前需要先通过内存重分配来释放不再使用的空间,如果没释放就会产生内存泄漏

但是内存重分配算法复杂,并且可能需要执行系统调用,所以是非常耗时的操作,但是Redis对性能要求是非常苛刻的,如果每次修改字符串长度都需要进行内存重分配,就会对性能造成极大的影响。

SDS为了避免这种情况,并不像C字符串那样长度一定是字符串数 + 1,在SDS中buf数组可以包含未使用字节空间,这个字节空间数量就是SDS的free属性,通过未使用字节空间实现了空间预分配惰性空间释放两种优化策略!


我先说说空间预分配

听名字也很好理解啦,就是SDS API对一个SDS进行扩容时,不仅分配修改所需必要空间,还会额外分配未使用的空间。

  • 如果修改后SDS的len < 1MB,那么会分配和len等大的free空间,例如:修改后SDS的len为13字节,那么free也为13字节,buf[]长度为:13 + 13 + 1 = 27字节
  • 如果修改后SDS的len ≥ 1MB,那么会分配1MBfree空间,例如:修改后SDS的len为30MB,那么会分配1MB的未使用空间,此时SDS的buf[]长度为:30MB + 1MB + 1字节

通过空间预分配策略,可以减少连续执行字符串变化操作所需的内存重分配次数,因为下次扩展SDS空间之前,如果SDS API检查空间充足,就直接使用了,不需要内存重分配了。

通过该策略,即使SDS连续增长N次,内存重分配次数也最多为N次。


关于惰性空间释放:

也比较好理解,区别于空间预分配,它是针对于SDS字符串缩短操作的。当SDS进行缩短操作时,程序不会立即进行内存重分配回收多出来的字节空间,而是使用free属性将多出来的数量记录下来,等待以后使用。 此时在对SDS进行扩容操作时,free空间足够就不需要进行内存重分配操作了。当然SDS API也提供了真正释放未使用空间的函数,无需担心内存浪费

5.4、SDS二进制安全

C语言字符串不能包含空字符,程序会认为空字符就是字符串结尾,所以C字符串只能存文本信息,不能存二进制数据,例如:视频、音频、图片等。

比如说字符串"Redis Cluster",对于C的函数只能识别"Redis""Cluster",如下图:

但是SDS是二进制安全的,SDS API以二进制方式处理存放在buf[]中的数据,写入和读取时数据没有任何区别,SDS使用len去取代空字符判断字符串是否结束,所以相比于C字符串,SDS不仅能存文本,还能存任意格式的二进制数据

5.5、兼容部分C字符串函数

之前提到,SDS的末尾总是设置为'\0',就是为了能够使用C字符串函数库的部分函数(文章开头提到过这个特点),Redis就不需要专门为SDS编写这些函数了,减少了大量代码重复!


以上就是SDS和C语言字符串的区别所在,哎哟,累死我了(这还唬不住你?),面试官嘴角露出一丝微笑,我就知道这个回答他也是挺满意的(能不满意吗,面试时候要是这么答,那Offer必然手到擒来)

面试官:不错不错,SDS原理学习的非常扎实啊,不过其他类型底层实现原理呢?

本以为面试到此为止了,没想到这才刚刚开始.....

参考资料:《Redis设计与实现》
相关推荐
monkey_meng16 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss24 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己24 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
2401_8532757344 分钟前
ArrayList 源码分析
java·开发语言
爪哇学长1 小时前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万1 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc
MoFe11 小时前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端