当前位置:首页 > 问答 > 正文

Redis源码深度挖掘,带你一步步搞懂它的底层实现细节

主要基于对Redis源码文件,如src/server.hsrc/t_string.csrc/adlist.csrc/dict.csrc/sds.h等的分析和理解)

要搞懂Redis的底层实现,我们得从它最基础的数据结构开始,很多人以为Redis的字符串就是C语言里简单的char*,其实不然,Redis自己实现了一个叫SDS(Simple Dynamic String,简单动态字符串) 的东西,在源码的src/sds.h文件中,你可以看到它的定义,它不仅仅是一个字符数组,而是在这个数组前面加了一个“头”,这个头里记录了这个字符串的长度(len)以及这个数组还剩多少空闲空间(free),这样做有两个巨大的好处:第一,获取字符串长度的时间是固定的,因为直接读len属性就行了,不用像C字符串那样一个个字节去数直到遇到\0,第二,避免了缓冲区溢出,C语言里拼接字符串如果没计算好空间很容易覆盖掉后面的数据,而SDS在拼接前会检查空间是否足够,不够的话会自动扩展,非常安全,这就是Redis字符串为什么这么快、这么安全的原因。

Redis源码深度挖掘,带你一步步搞懂它的底层实现细节

接下来看链表,Redis的列表(List)数据类型,当底层实现是链表时,用的就是它自己实现的这个链表结构,源码在src/adlist.c,这个链表是一个标准的双向链表,每个节点不仅有指向下一个节点的指针,还有指向上一个节点的指针,这样从前往后、从后往前遍历都很方便,这个链表结构还特意记录了这个链表的头节点、尾节点、链表长度等信息,这样做的好处是,比如我们要获取链表的长度,直接返回list结构体里的len属性就行了,时间是固定的,不需要去遍历整个链表,在链表两头进行插入和删除操作也非常快。

但Redis最核心、最精彩的数据结构是字典(Dict),也就是哈希表,几乎整个Redis就是一个大的字典结构,所有的键值对都存储在一个巨大的字典里,这个字典的源码在src/dict.c,非常复杂但也非常精妙,它解决了一个关键问题:如何快速根据一个键(username")找到它对应的值(张三")?答案就是哈希表,它通过一个哈希函数,把键转换成一个数字,这个数字对应到数组的一个下标,然后把值放在那个位置。

Redis源码深度挖掘,带你一步步搞懂它的底层实现细节

但不同的键可能会算出相同的哈希值,这就是“哈希冲突”,Redis怎么解决呢?它用的是“链地址法”,也就是说,哈希表的每个位置,不直接存一个值,而是存一个链表的头指针,所有哈希到同一个位置的键值对,都被串在这个链表里,当你要查找时,先通过哈希值定位到链表,然后再遍历这个小小的链表,找到你要的键。

随着数据越来越多,链表会变长,查找速度会变慢,这时候就需要“扩容”(Rehash),Redis的扩容策略很聪明,它不是一次性把所有数据都搬到新的、更大的哈希表里,那样会导致服务卡顿很久,它采用的是“渐进式Rehash”,它会同时准备两个哈希表,一个旧的,一个新的,在扩容时,慢慢地、分多次地把旧表里的数据一点点搬到新表里,在这期间,如果有新的数据写入,就直接写到新表;查找数据的话,会同时查旧表和新表,这样就把一次性的巨大开销,平摊到了后续的每次操作中,保证了服务的平滑。

我们看看最常用的SETGET命令在底层是怎么工作的,当你执行SET username zhangsan时,Redis会做这几件事:它会用我们前面提到的那个巨大的“键空间”字典(就是一个哈希表),以username这个SDS字符串为键,去查找,因为username还不存在,所以它会在这个字典里新增一个条目,键就是username这个SDS对象,值是什么呢?值也是一个Redis对象(在src/server.h里定义为redisObject结构体),这个对象里包含了zhangsan这个字符串(同样是SDS),以及这个值的类型(这里是字符串类型)、编码方式等信息,整个Redis数据库,本质上就是一个巨大的字典,字典里存放着许许多多的redisObject

当你执行GET username时,过程就反过来了:Redis同样用username作为键,去那个巨大的键空间字典里查找,通过哈希表快速定位,找到对应的redisObject,然后从里面取出值zhangsan,返回给客户端。

通过这样一步步拆解,你会发现,Redis的惊人性能并非魔法,而是源于这些精心设计、高效务实的数据结构,从安全的SDS,到高效的双向链表,再到核心的、采用渐进式Rehash的字典,每一层设计都是为了在内存中实现最快的数据存取,理解了这些,你才算真正窥见了Redis强大能力的冰山一角。

Redis源码深度挖掘,带你一步步搞懂它的底层实现细节