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

刚开始接触Redis,想弄懂它源码背后的那些复杂又神秘的东西,慢慢拆解redis源码细节

想弄懂Redis源码,一开始千万别一头扎进server.c那几千行的主循环里,那会像走进一个没有地图的迷宫,咱们得换个思路,从一个最简单、最核心的问题开始:当你打一个命令,比如SET key value,敲下回车的那一刻,Redis内部到底发生了什么? 顺着这个线头,你就能慢慢把整个线团拆开。

你得知道Redis是个“单线程”的家伙(指处理命令的核心模块),这意味着,它在一个时间点只能认真做一件事,那它怎么同时应付成千上万个客户端的连接请求呢?秘密就在于一个叫做事件循环 的东西,这听起来有点唬人,但其实你可以把它想象成一个超级负责的“前台”或“调度员”。

这个“调度员”(在源码里主要是aeMain函数,在ae.c文件中)的工作非常简单,就是永不疲倦地重复问两个问题:1. 有没有哪个网络连接发来了新数据(客户端的命令)需要我读?2. 我有没有事情处理完了需要写回给客户端?它用一种高效的方式(比如Linux上的epoll)同时监听所有连接的“动静”,一旦有动静,它就立刻去处理,这个循环是Redis高效能的基石,理解了它,你就明白了Redis的“心脏”是怎么跳动的。

好,现在你的SET key value命令通过网络到达了Redis服务器,事件循环的“读事件”被触发,调度员说:“哦,连接5有数据来了。” 然后它会把读取数据和处理数据的任务交给一个“处理器函数”,读上来的数据是什么呢?是一串字符:*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n,这看起来像乱码,其实是Redis自定义的RESP协议(Redis Serialization Protocol),这是一种非常简单的协议,用表示数组个数,表示后面字符串的长度,\r\n是换行符,这样设计的好处是解析起来飞快,服务器不用去猜测你的命令到底是什么意思,按格式拆开就行。

解析命令的工作是在networking.c里的processInputBuffer等函数中完成的,解析器会把这串字符还原成你期望的样子:一个包含三个元素的数组 ["SET", "key", "value"],它就要去“执行”这个命令了。

刚开始接触Redis,想弄懂它源码背后的那些复杂又神秘的东西,慢慢拆解redis源码细节

Redis就像一个巨大的工具箱,里面装满了各种工具,比如处理字符串的、处理列表的、处理哈希表的,每一个命令(SET, GET, HSET, LPUSH等等)都对应一个特定的工具函数,源码里有一个巨大的命令表(在server.c文件里搜索redisCommandTable),它就像一个目录,把命令名(如"SET")和真正干活的函数(如setCommand)关联起来。

解析器根据命令名"SET"在命令表里找到了setCommand这个函数,然后就直接调用它。setCommand函数住在t_string.c文件里,因为字符串是Redis最基础的数据类型,这个函数会做一些检查,比如参数个数对不对,然后就会调用更底层的setGenericCommand函数,到这里,我们终于要接触到Redis最核心的部分了——键值对到底存哪儿了?

答案是一个叫做字典 的数据结构(源码在dict.c),你可以把它想象成一个超级升级版的HashMap,Redis服务器有一个顶级的、巨大的字典,叫做键空间(key space),你所有的key都放在这个字典里。setCommand函数最终会调用dbAdddbOverwrite(在db.c中)来操作这个键空间。

刚开始接触Redis,想弄懂它源码背后的那些复杂又神秘的东西,慢慢拆解redis源码细节

它会将你传入的key作为键,然后创建一个叫做redisObject(在server.h中定义)的东西作为值,这个redisObject非常非常重要,它是Redis中所有数据类型的“包装盒”,这个盒子里有几个关键信息:

  • type:标明盒子里装的是什么类型的数据,是字符串?列表?还是集合?这样GET命令就知道不能对一个列表类型的key使用。
  • encoding:标明这个数据类型在底层是用哪种具体数据结构实现的,比如一个字符串,可能用简单动态字符串(SDS)实现,也可能用整数实现,一个小一点的列表,可能用ziplist(压缩列表)实现,大了才会用linkedlist(双向链表),这种设计让Redis能在内存和速度之间做智能的权衡。
  • ptr:这是一个指针,真正指向存放数据的内存地址,比如指向一个SDS结构体。
  • refcount(引用计数)和lru(最近使用时间):用于内存管理和淘汰策略。

执行SET key value的本质,就是在那个巨大的键空间字典里,把key和一个新创建的、类型为字符串的redisObject关联起来,而这个redisObjectptr指向了存储"value"这个字符串的SDS内存位置。

命令执行成功后,setCommand函数会做一些后续工作,比如检查是否触发了持久化条件(如果配置了的话),然后就把成功的回复(比如"+OK\r\n")放入客户端的回复缓冲区,这时,事件循环的“调度员”下次巡视时,会发现这个连接有数据要写,就会触发“写事件”,把"OK"这个结果通过网络发回给你的客户端。

你看,就这么一个简单的SET命令,我们从网络监听、协议解析、命令查找、到最终写入核心数据结构,几乎走遍了Redis最核心的模块,下次你就可以顺着这个思路,去看看GET命令是怎么工作的,或者更复杂的HMSETZADD命令又是如何与字典、跳跃表这些数据结构打交道的,一点一点拆解,那些看似神秘复杂的东西,就会慢慢变得清晰起来。