文章目录
- 前言
- 面试开始
- 1、说说Redis基本类型有哪些
- 2、String类型常用于什么场景
- 3、String类型采用什么数据结构
- 4、继续深入讲讲SDS实现细节
- 5、你说说SDS和C语言字符串的区别
- 5.1、SDS获取长度时间复杂度更低
- 5.2、SDS杜绝缓冲区溢出
- 5.3、SDS减少字符串内存重分配次数
- 5.4、SDS二进制安全
- 5.5、兼容部分C字符串函数
- 参考资料:`《Redis设计与实现》`
前言
该文章已面试场景进行叙述,场景纯属编造,请勿当真!
大家好,我是徐志斌。
现在找工作真的好难,今天我早早的来到了面试现场,这家公司的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语言代码如下(我随手拿起笔写出下面代码):
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
,那么会分配1MB
的free
空间,例如:修改后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原理学习的非常扎实啊,不过其他类型底层实现原理呢?
本以为面试到此为止了,没想到这才刚刚开始…