Redis 源码阅读&实践-GET命令背后的源码逻辑
大家好,我是弟弟!最近读了一遍 黄健宏大佬的 <<Redis 设计与实现>>,对Redis 3.0版本有了一些认识,该书作者有一个添加了注释的 redis 3.0版本源码。
网上说Redis代码写得很好,为了加深印象和学习redis大佬的代码写作艺术,了解工作中使用的redis 命令背后的源码逻辑,便有了从redis命令角度学习redis源码的想法。
(全文提到的redis服务器,都指在 mac os 上启动的一个默认配置的单机redis服务器)
Redis服务器启动过程回顾
在上一篇博客中了解到,redis服务器是一个事件驱动的单线程服务器,
比如:客户端的链接请求是一个文件的读事件,除了文件事件意外,还有时间事件。
其中redis大佬对 事件 进行了抽象, 不管事件底层选取的是select, epoll, kqueue
都抽象出如下信息
结构体 aeApiState
(在 kqueue.c 里包含一个 i/o对象kqfd,以及一个事件数组)函数 aeApiCreate
(在kqueue.c里, 调用 kqueue()获取fd, 并为事件数组分配空间)函数 aeApiAddEvent
(在kqueue.c里, 给kqfd设需要监听的fd,监听的事件类型(读/写))函数 aeApiPoll
(在kqueue.c里, 通过kqfd获取活跃事件并存放到 事件数组里)函数 aeApiDelEvent
(在kqueue.c里, 删除kqfd设需要监听的fd,监听的事件类型(读/写))函数 aeApiResize
(在kqueue.c里, 调整事件数组大小)函数 aeApiFree
(c语言,手动释放aeApiState对象)函数 aeApiName
(选取的底层i/o模型名字)
对事件进行抽象的好处就在于,调用方可以不用关心底层实现,按抽象出来的函数写一套代码就可以了,不用ctrl c+v产生大量冗余代码。没有大量冗余代码,也使得代码逻辑清晰明了。🐂🍺
对各种事件的处理函数,是放在了全局的redisServer对象里,通过活跃事件的fd、读/写事件类型 在redisServer对象中 关联了对应的事件处理函数
上一篇博客传送门
GET命令背后的源码逻辑
GET 使用场景
在我们愉快的发送 GET命令前,不禁想要问自己,在工作中什么情况下会用到这个 GET 命令,如果哪儿都用不到的话,我是不是可以不用学了!😃
当然一个很容易能想到的场景就是,缓存 key: id, value: id_info 这种数据的时候,再具体一点,就是通过uid查用户信息。
好了,服务器搞起来了,客户端也连上了,连发送命令的理由都想好了,那就来让我们愉快的发送GET命令吧!
GET命令背后的源码逻辑
请求命令的参数处理
SET命令比GET命令稍微复杂了一点点,我们先SET一个值来GET它看看。
- SET uid.1 我是uid1的用户信息 SET命令 发送!
- GET uid.1 GET命令发送!
从下面的截图我们可以看到, set命令成功
但是get命令打错了哈,不好意思。😅
观众: “没关系,原谅你再打一次”
在我们重打一次GET命令前,我们看到redisClient上提示 (error) ERR unknown command 'ge’
好学的我不禁思考起来,redis应该是有一个支持的命令列表吧,它才知道我们打错了命令。
观众:“emm,弟(这)弟(不)真(是)聪(废)明(话吗)”
那我们来简单看一下redis服务器收到客户端的GET命令后是一个什么样的处理流程
从上一篇我们知道处理客户端命令的函数是readQueryFromClient, 下面是缩水的源码。
可以看到的操作是, 从fd中读取请求数据到 redisClient->querybuf中,
这个querybuf是一个sds对象,不慌下面会说这个sds是啥
然后调用processInputBuffer函数进行处理,那我们接着往下看
/*
* 读取客户端的查询缓冲区内容
*/
//... 代表省略x行代码
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)
{
redisClient *c = (redisClient *)privdata;
int nread, readlen;
size_t qblen;
...
// 读入长度(默认为 16 MB)
readlen = REDIS_IOBUF_LEN;
...
qblen = sdslen(c->querybuf);
...
// 为查询缓冲区分配空间
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
// 读入内容到查询缓存
nread = read(fd, c->querybuf + qblen, readlen);
...
// 从查询缓存重读取内容,创建参数,并执行命令
// 函数会执行到缓存中的所有内容都被处理完为止
processInputBuffer(c);
...
}
从下面这个while循环里,我们凭感觉来感觉一下,
c->querybuf 可能存了1条以上的客户端发来的命令,然后呢,每次处理一条命令
这两个函数 processInlineBuffer,processMultibulkBuffer 做的事情就是从querybuf中提取出一条命令的各个参数并放到 c->argv参数数组里,c->argc里放参数个数
从名字上来看 processCommand 应该是实际执行命令的函数,那我们接着往下看
// 处理客户端输入的命令内容
void processInputBuffer(redisClient *c)
{
while (sdslen(c->querybuf))
{
...
if (c->reqtype == REDIS_REQ_INLINE)
{
if (processInlineBuffer(c) != REDIS_OK)
break;
}
else if (c->reqtype == REDIS_REQ_MULTIBULK)
{
if (processMultibulkBuffer(c) != REDIS_OK)
break;
}
...
if (c->argc != 0)
{
// 执行命令,并重置客户端
if (processCommand(c) == REDIS_OK)
resetClient(c);
}
...
}
}
redis命令列表
从下图可以看出,processCommand正如其命,只是处理命令而已,
可以看到一个 c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
c->argv[0]->ptr 就是我们发送的命令里,按空格分隔的第一个字符串,也就是我们发送的 get
👇下面这一顿操作,就是在查找命令
int processCommand(redisClient *c)
{
...
/* Now lookup the command and check ASAP about trivial error conditions
* such as wrong arity, bad command name and so forth. */
// 查找命令,并进行命令合法性检查,以及命令参数个数检查
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
if (!c->cmd)
{
// 没找到指定的命令
flagTransaction(c);
addReplyErrorFormat(c, "unknown command '%s'",
(char *)c-