[面试集锦]源码解读:Redis为什么这么快

[面试集锦]源码解读Redis为什么这么快

-- 楼兰

​ Redis为什么这么快?这就是所有中高端技术岗面试的必问项。你会怎么答?如果到现在这个内卷到极致的时代,你还是只会说基于内存工作啊、数据结构丰富啊、IO多路复用啊这些虚头巴脑的面试流说法,那你就注定很难脱颖而出了。因为这些说法各种面试题说烂了,而且各种AI工具也能快速生成类似的说法。

​ 有没有更硬核的装逼方式?有!对于程序员来说,有一种任何AI都替代不了的基本功:读源码。

​ 当然,源码不是瞎读,有目的,有方法的读源码,会比看小说还爽。这是什么感觉?跟楼兰老师来感受一下。


一、找问题:Redis是怎么存数据的?


​ 读源码的第一步,是要找到自己感兴趣的问题。

​ 任何技术的核心是数据,Redis这么个对性能追求到极致的产品,是怎么存储数据的呢?其实Redis就提供了一个OBJECT指令,可以直接查看数据的底层存储类型。

localhost:0>OBJECT help
1)  "OBJECT <subcommand> [<arg> [value] [opt] ...]. Subcommands are:"
2)  "ENCODING <key>"
3)  "    Return the kind of internal representation used in order to store the value"
4)  "    associated with a <key>."
5)  "FREQ <key>"
6)  "    Return the access frequency index of the <key>. The returned integer is"
7)  "    proportional to the logarithm of the recent access frequency of the key."
8)  "IDLETIME <key>"
9)  "    Return the idle time of the <key>, that is the approximated number of"
10) "    seconds elapsed since the last access to the key."
11) "REFCOUNT <key>"
12) "    Return the number of references of the value associated with the specified"
13) "    <key>."
14) "HELP"
15) "    Print this help."

​ 接下来,以Redis中最简单的set指令来看看数据是怎么存的。

​ 说明:这里用的Redis版本是8.0.2,后续解读源码也以这个版本为准。

localhost:0>set k1 11
"OK"
localhost:0>type k1
"string"
localhost:0>OBJECT ENCODING k1
"int"
localhost:0>set k2 v2
"OK"
localhost:0>type k2
"string"
localhost:0>OBJECT ENCODING k2
"embstr"
localhost:0>set k3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
"OK"
localhost:0>type k3
"string"
localhost:0>OBJECT ENCODING k3
"raw"

​ 看到了吗?就是一个最简单的string类型数据,Redis就整出了好几种数据类型来存。

​ 为什么搞这么麻烦?当然是从根本上为了让Redis提速。到底怎么提速的呢?负责任的做法,自然是去源码当中找答案。


二、string数据类型源码解读


1、Redis的底层数据结构

​ 拿到Redis的源码别瞎读,先得找到那些OBJECT ENCODING指令的结果是从哪里来的。

​ 我们知道,Redis是一个<K,V>型的数据库。key的结构比较简单,通常就是一个字符串。但是value的结构就多了。string、list、set、hash等等。所以,在Redis中,自然需要定义一个能够兼容各种value类型的统一结构。

这个统一的结构,就是在server.h中,定义的基础结构:redisObject

在这里插入图片描述

​ 正是因为Redis把所有value类型都封装成了一个统一的redisObject结构,所以,对任何数据类型,我们都可以使用type指令和OBJECT指令查看数据类型和底层存储类型。

​ 比如,encoding中存储数据的底层存储类型。有哪些类型呢?也都在server.h中定义了。

在这里插入图片描述

​ 如果你面试题背得比较多,其中一定有很多你眼熟的数据结构。

2、set指令的执行入口

​ 对于set指令,执行的入口在t_string.c文件当中。其中,你一眼就能看到set、setnx、setex等熟悉的指令。

在这里插入图片描述

​ 其实,从文件名你也能猜到,hash、list、set等其他数据结构的指令入口在哪里了。

​ 当然,我们这次关注的是setCommand方法。

在这里插入图片描述

​ 先来看第一个tryObjectEncoding方法。具体实现在object.c当中。关键部分是这样:

在这里插入图片描述

从这里可以看到:

1、对于数字长度超过20的大数字,Redis是不会用int保存的。

2、OBJ_SHARED_INTEGER = 1000。也就是对于1000以内的数字,直接指向缓存,而不用重新创建内存。

在这里插入图片描述

​ 从这里就能看到Redis中对于string数据的基础类型选择逻辑

  • int : 如果value可以转换成一个long类型的数字,那么就用int保存value。只有整数才会使用int,如果是浮点数,Redis内部其实是先将浮点数转化成字符串,然后保存
  • embstr : 如果value是一个字符串类型,并且长度小于44字节的字符串,那么Redis就会用embstr保存。代表embstr的底层数据结构是SDS(Simple Dynamic String 简单动态字符串)
  • raw :如果value是一个字符串类型,并且长度大于44字节,就会用raw保存。

3、string类型对应的int、embstr、raw有什么区别

int类型,直接存储到redisObject中。

​ 对于简单的数字类型,Redis会尽量将对应的robj中的ptr指向一个缓存数据对象。

在这里插入图片描述

​ 其目的,当然是为了能够尽量重用相同的缓存对象。这种思想其实也并不陌生,Java当中Integer类型在-127-128之间时,使用预设的内存对象,也是同样的思路。

embsr类型,将字符串和redisObject紧凑的放到一起

​ 如果字符串类型长度小于44,就会创建一个embstr的对象。这个对象创建的方法在object.c当中的createEmbeddedStringObject方法,关键部分是这样的:

在这里插入图片描述

​ 其实,embstr字面意思就是内嵌字符串。所谓内嵌,核心就是将新创建的SDS对象直接分配在对象自己的内存后面。这样,读取value时,就不需要再额外进行内存寻址了,当然极大的提升内存的读取效率。

在这里插入图片描述

raw类型分散存储,通过ptr指针保留内存地址

​ 从之前的分析可以看出,raw类型就相当于是一种兜底的类型了。特殊的数字类型和小字符串类型处理完了,就是raw类型了。raw类型的处理方式就是单独申请一个内存,存放SDS数据,然后将robj的ptr指针指向这个SDS。

在这里插入图片描述

​ 为什么这样做呢?其实从源码中可以看到,embstr类型会调用内存分配函数,分配一块连续的内存空间保存redisObject以及SDS,这样使用连续的内存空间,自然能够提高数据的读取速度,而且也可以避免内存碎片。但是如果value数据太大了,申请连续的内存空间就会带来更大的系统开销。而改为raw类型,单独申请内存,可以减小操作系统的负担。

4、最后来看看SDS是什么

​ 从源码中开到,Redis中对于embstr类型的数据,是用一种称为SDS的结构保存value的。而SDS代表的是一段不可修改的字符串。这意味着如果使用APPEND之类的指令尝试修改一个key的值,那么Redis也会创建一个新的SDS,而不再使用原来的SDS。

​ 对于这样的不可修改的数据类型,你会如何处理呢?来看看Redis的处理方式。

​ Redis对于SDS的处理,其实相当简单粗暴。直接预先定义好了几种固定长度的SDS,根据需要选择即可。在sds.h文件中,定义了几种预设的SDS类型:

在这里插入图片描述

​ 这种预设长度的想法就好像去蛋糕店买蛋糕,你不知道要买多大的蛋糕,那就简单设定几种固定的规格,你只需要选择,不要计算。极大的减少你的选择困难症。

​ 当然,要真正了解SDS结构的好处,这就需要跟C语言中的字符串做对比了。

1、快速获取字符串长度

​ 获取字符串长度是Redis中非常频繁的操作。C语言中获取一个字符串长度需要遍历数组,而SDS直接保存了字符串长度,时间复杂度极大提升。

2、避免缓冲区溢出

​ C语言中字符串长度固定,当需要修改字符串时,就需要重新分配内存。而SDS中预先分配了内存空间,可以减少内存重分配的次数。

3、二进制安全

​ C语言中字符串需要以固定的\0作为结束标志。因此无法存储包含\0字符的其他类型二进制数据。比如图片、音频等。而SDS是通过len长度判断字符串结束,因此buf中可以包含任意字节数据。所以理论上,Redis是可以保存图片、音频等通用的二级制数据的。

​ 当然,实际使用时,很少用Redis作为通用存储。但是在AI大模型时代,我们却可以使用Redis,存储向量,这也得益于Redis提前的设计。


三、最后总结


​ 实际上,大家都知道,数据结构对任何应用程序来说都是最重要的。在Redis的版本变革过程当中,也伴随着对数据结构的极致优化。在Redis7版本中,使用listpack全面替换了Redis6中的ziplist。而在Redis8版本中,除了把Redis7中以stack形式存在的扩展包,直接集成到了Redis8中之外,还增加了HGETEX,HSETEX等更多操作。这些都离不开对数据结构的极致要求。

​ 最后,关于面试题,其实市面上几乎所有的面试题解读视频,都是经不起一字一字推敲的。花里胡哨的面试视频,最终代替不了平时的思考和积累。就像《霍元甲》中那一句“这一拳二十年的功夫,你挡得住吗?”。最终带我们和这换乱的时代抗衡的,是功夫,而不是这一拳。

当然,故事还没有结束,更多精彩,我们下次再聊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roykingw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值