文章目录
redis多线程模型
redis为什么引入I/O多线程
Redis 的性能瓶颈在网络 IO 的处理上。Redis 是网络 IO 密集型,需要同时处理多条并发请求,读写 IO 的问题(请求大量数据,写日志业务等)。多线程处理网络 IO,单线程执行命令。
Redis 线程池作用读写 IO 阶段,即 read, decode 和 encode, send 阶段。主线程处理业务逻辑,之所以用单线程执行命令,是因为 Redis 采用高效的数据结构,其业务逻辑处理较快。
I/O多线程模型
主线程拥有两个全局队列clients_pending_read
和clients_pending_write
,每个 io 线程(主线程同时也是 io 线程)拥有一个专属队列 io_threads_list[id]
。主线程既作为生产者,产生任务;又作为消费者,获取任务执行。
首先,主线程将一次循环的所有就绪的读事件收集到自己的全局任务队列clients_pending_read
中,再把每个事件负载均衡地分配到每个 io 线程的专属任务队列中。一次事件循环中不会出现同名 fd,不同的 fd 分配到每个 io 线程各自的队列中,避免了多个 io 线程同时从全局队列中取数据,因此,不需要加锁操作。
接下来,io 线程从自己的专属队列中取出任务,(除主线程外)并发执行 read 和 decode 操作。主线程将解析后的任务做 compute 操作。最后,io 线程(包括主线程)并发执行 encode 和 send 操作。
redis的单线程是指,命令执行(logic)都是在单线程中运行的
接受数据read和发送数据write都是可以在io多线程(线程池)中去运行
在Redis中,生产者也可以作为消费者,反之亦然,没有明确界限。
源码解析
测试设置
redis 线程池默认作用在 encode, send 阶段,这是因为客户端从 redis 获取大量数据需要并发处理。若想作用在 read, decode 阶段,需要手动开启。在 redis.conf 文件中,可以设置:
# 开启io线程的数量
io-threads 4
# 优化:read deconde 过程。默认优化,encode send从 redis 获取大量数据
io-threads-do-reads yes
开启 io 多线程的前提是有多个并发连接。如何在单个连接的情况下,开启 io 多线程调试,需要修改 redis 源码:
// networking.c
int stopThreadedIOIfNeeded(void) {
// 单个连接的情况下,开启多线程调试,永远不关闭 io 多线程
return 0;
...
}
连接建立
主线程处理连接建立,listenfd
- 连接到达,触发读事件回调:acceptTcpHandler
- 接收连接:acceptTcpHandler
- 初始化新连接:createClient
// server.c
void initServer(void) {
...
// 1、连接到来,触发读事件回调
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
...
}
// networking.c
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
...
while(max--) {
// 2、接收连接:内部封装 accept
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
...
// 为 cfd 初始化新连接,内部调用 createClient
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
}
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
...
/* Create connection and client */
// 3、创建新的连接
if ((c = createClient(conn)) == NULL) {
...
}
...
}
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
/* passing NULL as conn it is possible to create a non connected client.
* This is useful since all the commands needs to be executed
* in the context of a client. When commands are executed in other
* contexts (for instance a Lua script) we need a non connected client. */
if (conn) {
connNonBlock(conn);
connEnableTcpNoDelay(conn);
if (server.tcpkeepalive)
connKeepAlive(conn,server.tcpkeepalive);
// 4.接收数据的读事件触发,回调readQueryFromClient函数
connSetReadHandler(conn, readQueryFromClient);
connSetPrivateData(conn, c);
}
}
数据传输
clientfd
-
读事件回调:readQueryFromClient
-
分割并处理数据包 processInputBuffer
-
- 分割数据包:processInlineBuffer 和 processMultibulkBuffer
- 处理数据包:processCommandAndResetClient
-
数据写到 buffer:addReply
-
数据写到 socket:writeToClient
-
写事件回调:sendReplyToClient
当读事件触发时,执行读事件