1. Redis简介
Redis是一个高性能的内存键值存储系统,可以用作数据库、缓存和消息中间件。Redis支持多种数据结构,包括字符串、哈希表、列表、集合、有序集合,还提供一些高级功能,例如事务、消息订阅、Lua脚本处理等等。Redis以C语言编写,被广泛应用于Web应用程序、游戏、移动应用、消息传递、实时分析等场景。
2. Redis请求模型
Redis客户端向Redis服务端发送请求消息,服务端接收请求消息并执行对应的操作,然后返回响应消息给客户端。这里的请求和响应消息遵循Redis协议,是一种文本协议,采用行分割符号(\r\n)和参数个数来描述消息内容。以下是一个示例的Redis请求消息:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n
消息的第一行是*3表示这是一个有3个参数的消息,后面的$3\r\nSET\r\n表示第一个参数是字符串SET,$5\r\nmykey\r\n表示第二个参数是字符串mykey,$7\r\nmyvalue\r\n表示第三个参数是字符串myvalue。
Redis的请求模型采用简单易懂的方式来处理请求和响应消息,使得Redis具有很高的性能和可扩展性。下面我们来看一下Redis处理请求的流程。
3. Redis请求处理流程
Redis服务端支持单线程或多线程模式,这里我们以单线程模式为例来介绍请求处理的流程。单线程模式意味着Redis服务端采用一个线程来处理所有的请求和响应消息,因此需要采用异步I/O模型来提高性能。
3.1 监听端口
Redis服务端在启动后,需要监听一个端口,这样客户端才能通过网络连接到Redis服务端。Redis服务端采用的是TCP协议进行网络通信,因此需要使用socket来进行监听。Redis服务端的监听端口默认是6379,可以在启动命令中通过参数--port来修改端口号。
void listenToPort( int port ) {
// 创建socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 端口复用
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
// 绑定地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
// 监听端口
listen(fd, 128);
// 加入事件循环
aeCreateFileEvent(server.el, fd, AE_READABLE, acceptHandler, NULL);
}
setsockopt函数用于设置socket选项,这里我们开启了端口复用。setsockopt的第二个参数表示选项所属的协议族,第三个参数表示选项名称,第四个参数是选项值,第五个参数是选项值的长度。
bind函数用于将socket绑定到一个地址(IP地址和端口号)上,addr是一个sockaddr_in结构体,包含了地址的IP地址、端口号和协议族。
listen函数用于将socket设置为监听模式,并设置backlog参数表示最大连接数。当有客户端连接到服务端时,内核会创建一个新的socket来和客户端进行通信。
在上述代码中,我们还创建了一个文件事件处理器(aeCreateFileEvent),用于处理客户端连接请求。这个处理器的回调函数是acceptHandler,它会在有新的连接时被调用。
3.2 接收客户端请求
当有新的连接请求时,Redis服务端会接受客户端的连接,并将连接添加到一个事件循环中。事件循环会等待客户端发送请求消息,当消息到达时,事件循环会调用请求处理函数来处理请求。
void acceptHandler( aeEventLoop *el, int fd, void *privdata, int mask ) {
// 接收客户端连接
int cfd = accept(fd, (struct sockaddr*)&addr, &addrlen);
// 添加客户端事件
aeCreateFileEvent(server.el, cfd, AE_READABLE, readHandler, NULL);
}
void readHandler( aeEventLoop *el, int fd, void *privdata, int mask ) {
// 读取客户端请求
char buf[1024];
int nread = read(fd, buf, sizeof(buf));
// 处理请求
processRequest(fd, buf, nread);
}
在acceptHandler函数中,我们接受客户端连接,并创建一个文件事件用于处理客户端请求。文件事件的回调函数是readHandler,它会读取客户端发送的请求消息,并调用processRequest函数来处理消息。
3.3 解析请求消息
当readHandler函数读取到客户端发送的请求消息后,需要对消息进行解析,以便后续程序能够理解和处理消息。Redis服务端采用的是一种基于行分割的文本协议,因此只需要按照行分割符号(\r\n)将消息分割成多个参数,就可以获得每个参数的值。
void processRequest( int fd, char* buf, int nread ) {
int argc = 0;
char* argv[MAX_ARGUMENTS];
// 解析请求消息
char* p = buf;
char* q = buf;
while (q < buf + nread) {
if (*q == '\r' && *(q+1) == '\n') {
// 解析参数
argv[argc++] = p;
*q = '\0';
p = q + 2;
q += 2;
} else {
q++;
}
}
// 处理请求
processCommand(fd, argc, argv);
}
在这段代码中,我们首先定义了一个argv数组,用于存储请求消息中的参数。接着我们使用p和q两个指针来遍历请求消息,并按照行分割符号'\r\n'将消息分割成多个参数。每次找到分割符号时,我们就将分隔符设为字符串结束符'\0',将p设为当前位置的下一个位置,并将变量q指向下一个参数的开始位置。最后,我们调用processCommand函数来处理请求。
3.4 处理请求
Redis服务端支持多种命令,例如SET、GET、INCR等等,我们需要根据参数中第一个字符串来确定当前执行的命令。Redis服务端采用了一个命令表来记录每个命令对应的处理函数,以便快速查找和调用。当处理请求时,我们从命令表中查找对应的处理函数,并将参数列表传递给该函数来执行命令。
void processCommand( int fd, int argc, char** argv ) {
// 查找处理函数
commandTableEntry* entry = findCommand(argv[0]);
if (entry == NULL) {
sendError(fd, "unknown command");
return;
}
// 调用处理函数
entry->proc(fd, argc, argv);
}
在这段代码中,我们首先调用findCommand函数来查找对应的处理函数。如果查找失败,就返回一个错误消息,否则就调用处理函数来执行命令。在这个例子中,我们调用了entry->proc函数来执行命令,其中entry是一个命令表的条目,它包含了命令名和处理函数的指针。
3.5 执行命令
Redis服务端支持多种数据结构,例如字符串、哈希表、列表、集合、有序集合等等。不同的数据结构通常需要采用不同的算法来处理命令,因此执行命令的具体方式也会有所不同。我们以SET命令为例来说明命令的执行方式。
void setCommand( int fd, int argc, char** argv ) {
// 参数个数检查
if (argc != 3) {
sendError(fd, "wrong number of arguments");
return;
}
// 存储键值对
dictAdd(server.dict, sdsnew(argv[1]), sdsnew(argv[2]));
// 返回响应消息
sendSimpleString(fd, "OK");
}
在这个例子中,我们首先检查参数个数是否正确(SET命令需要2个参数),如果不正确就返回一个错误消息。接着,我们调用dictAdd函数来将键值对存储到Redis的字典数据结构中。最后,我们返回一个OK的响应消息给客户端。
4. 总结
Redis的请求处理流程比较简单,但也涉及到了多个步骤。整个流程可以分为监听端口、接收客户端请求、解析请求消息、处理请求和执行命令等几个步骤。在实际开发中,我们需要注意以下几个方面:
不同的请求可能需要采用不同的算法来处理,因此要根据请求的不同而处理。
请求消息格式较为简单,但仍需要注意消息的格式和参数个数。
Redis的性能和可扩展性依赖于单线程模式和异步I/O模型,因此需要注意异步处理和资源占用问题。
Redis采用的命令表和函数指针的方式可以快速查找并执行对应的命令处理函数,要熟悉命令表的实现和使用。