redis 多线程实现原理

从redis6.0开始,redis中开始新增了支持多线程。

Redis基于Reactor模式开发了网络事件处理器。

当有客户端连接请求时,主线程接收并解析请求,然后执行命令处理请求,最后把结果返回给客户端。这个流程都是主线程在处理,所以在 redis6.0 以前都是单线程的。

对于 redis 性能来讲,其性能不在 cpu,而在于内存和网络。redis 的存在就是为了做缓存,因此系统搭建采用 redis 的话,一般内存是足够的,若不够,可以加大内存或者优化数据结构能方式进行优化。因此,网络优化对于 redis来讲才是大头。

本次 redis 采用多线程,主要解决的就是网络 IO 部分, 处理网络数据的读写和协议的解析, 对于命令的执行依然是主线程在执行。

在 redis6.0 中默认是不开启多线程,若要开启多线程,需要用户进行修改redis.conf 配置进行开启。

io-threads-do-reads yes
io-threads 线程数量

redis启用多线程流程

redis 启动过程中会调用 initThreadedIO 生成线程,每个线程执行函数为 IOThreadMain

main
-> InitServerLast()
-> initThreadedIO()
-> pthread_create() 创建线程,每个线程执行方法为 IOThreadMain
-> IOThreadMain

每个线程的处理流程如下:

void *IOThreadMain(void myid) {
/
The ID is the thread number (from 0 to server.iothreads_num-1), and is
* used by the thread to just manipulate a single sub-array of clients. */
long id = (unsigned long)myid;
char thdname[16];

snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);
redis_set_thread_title(thdname);

while(1) {
    /* 若io_threads_pending[id]不为0,说明有任务需要处理  */
    for (int j = 0; j < 1000000; j++) {
        if (io_threads_pending[id] != 0) break;
    }

    /* Give the main thread a chance to stop this thread. */
    if (io_threads_pending[id] == 0) {
        pthread_mutex_lock(&io_threads_mutex[id]);
        pthread_mutex_unlock(&io_threads_mutex[id]);
        continue;
    }


    /* Process: note that the main thread will never touch our list
     * before we drop the pending count to 0. */
    listIter li;
    listNode *ln;
    //取出队里中的节点,进行处理,
    listRewind(io_threads_list[id],&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        if (io_threads_op == IO_THREADS_OP_WRITE) {
            //给客户端回应消息
            writeToClient(c,0);
        } else if (io_threads_op == IO_THREADS_OP_READ) {
            //处理客户端发送的命令请求
            readQueryFromClient(c->conn);
        } else {
            serverPanic("io_threads_op value is unknown");
        }
    }
    // 队列中的任务处理完后,清空,然后继续循环等待有任务到来
    listEmpty(io_threads_list[id]);
    io_threads_pending[id] = 0;

    if (tio_debug) printf("[%ld] Done\n", id);
}

}

每个线程都在等待各自的队列中是否有任务可以处理,若有,则进行处理。

主线程处理客户端连接请求

main
-> initServer
-> acceptTcpHandler
-> anetTcpAccept -> anetGenericAccept -> accept 获取一个套接字
-> acceptCommonHandler
-> createClient 创建一个client
-> connSetReadHandler
-> (conn->type->set_read_handler) 也即是 connSocketSetReadHandler
-> aeCreateFileEvent 往epoll中添加一个事件,事件回调函数为 (conn->type->ae_handler) 也即是 connSocketEventHandler ,
此时 conn->read_handler 函数为 readQueryFromClient

通过上述流程看到,主线程调用 accept 接收客户端连接后,把新接收的套接字注册到 epoll 中,等待接收客户端发送命令。

当客户端发送命令,主线程触发epoll事件,调用 connSocketEventHandler,调用流程如下:

connSocketEventHandler
-> callHandler
-> handler(conn) 也即是调用 conn->read_handler 也即是 readQueryFromClient

主线程调用 readQueryFromClient 用来接收 client 发来的消息。

readQueryFromClient 调用流程如下:

readQueryFromClient
-> postponeClientRead©
-> listAddNodeHead( server.clients_pending_read,c )

主线程会调用postponeClientRead会把 client 对象加入到 server.clients_pending_read 队列中。

void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
int nread, readlen;
size_t qblen;

/* Check if we want to read from the client later when exiting from
 * the event loop. This is the case if threaded I/O is enabled. */
/* 主线程会在postponeClientRead中客户端添加到server.clients_pending_read 中,并返回1,
   到此, readQueryFromClient结束,后续代码不再执行,   
*/
if (postponeClientRead(c)) return; //若是多线程,第一次把c加入到server.clients_pending_read中,退出,后面的不在执行

...

}

//把请求来的客户端加入到全局队列clients_pending_read中
int postponeClientRead(client *c) {
if (io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
{
c->flags |= CLIENT_PENDING_READ;
listAddNodeHead(server.clients_pending_read,c);
return 1;
} else {
return 0;
}
}

主线程会在启动后会一直在 eventLoop循环中,在该循环中每次处理事件前都会调用 beforeSleep 函数,在该函数中对调用 handleClientsWithPendingReadsUsingThreads

main
-> aeMain
-> beforesleep
-> handleClientsWithPendingReadsUsingThreads

handleClientsWithPendingReadsUsingThreads 作用就是把存放在clients_pending_read 队列中的客户端分发给线程池每个线程的队列中,线程从各自队列中取出任务,调用 readQueryFromClient 进行处理请求。

int handleClientsWithPendingReadsUsingThreads(void) {
if (!io_threads_active || !server.io_threads_do_reads) return 0;
int processed = listLength(server.clients_pending_read);
if (processed == 0) return 0;

/* Distribute the clients across N different lists. */
listIter li;
listNode *ln;
listRewind(server.clients_pending_read,&li);
int item_id = 0;
//把请求队列中客户端分发给线程池中的各个线程的队列中,每个线程都有一个list
while((ln = listNext(&li))) {
    client *c = listNodeValue(ln);
    int target_id = item_id % server.io_threads_num;
    listAddNodeTail(io_threads_list[target_id],c);
    item_id++;
}

/* Give the start condition to the waiting threads, by setting the
 * start condition atomic var. */
io_threads_op = IO_THREADS_OP_READ;
//io_threads_pending 记录对应io_threads_list 中每个list中节点的个数
for (int j = 1; j < server.io_threads_num; j++) {
    int count = listLength(io_threads_list[j]);
    io_threads_pending[j] = count;
}

/* Also use the main thread to process a slice of clients. */
//主线程也处理一些客户端请求
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
    client *c = listNodeValue(ln);
    readQueryFromClient(c->conn);
}
listEmpty(io_threads_list[0]);

/* Wait for all the other threads to end their work. */
//等待线程池中所有线程把请求处理完成
while(1) {
    unsigned long pending = 0;
    for (int j = 1; j < server.io_threads_num; j++)
        pending += io_threads_pending[j];
    if (pending == 0) break;
}
if (tio_debug) printf("I/O READ All threads finshed\n");

/* Run the list of clients again to process the new buffers. */
//由于多线程处理完了client的请求并进行了解析,主线程进行业务处理
while(listLength(server.clients_pending_read)) {
    ln = listFirst(server.clients_pending_read);
    client *c = listNodeValue(ln);
    c->flags &= ~CLIENT_PENDING_READ;
    listDelNode(server.clients_pending_read,ln);
    //client 解析完命令会设置 CLIENT_PENDING_COMMAND 标志,说明命令解析完成,主线程可进行处理命令
    if (c->flags & CLIENT_PENDING_COMMAND) {
        c->flags &= ~CLIENT_PENDING_COMMAND;
        if (processCommandAndResetClient(c) == C_ERR) {
            /* If the client is no longer valid, we avoid
             * processing the client later. So we just go
             * to the next. */
            continue;
        }
    }
    processInputBuffer(c);
}
return processed;

}

由于上述主线程把请求队列中的请求分发给每个线程,线程从自己的队列中取出请求,然后调用 readQueryFromClient 处理请求。主线程等待线程池中所有的线程均完成请求和解析后,主线程则调用 processInputBuffer -> processCommandAndResetClient 处理命令。

redis 响应请求

主线程处理完命令后,需要向客户端进行回应。而回应是通过 addReplyXX 系列进行回应的。

addReply (addReplySds、addReplyProto、AddReplyFromClient等)
-> prepareClientToWrite
-> clientInstallWriteHandler 把client中设置标记CLIENT_PENDING_WRITE,同时将client加入全局写队列clients_pending_write
-> listAddNodeHead(server.clients_pending_write,c)

主线程会把 client 对象加入到 server.clients_pending_write 响应队列中。

随后主线程会 把 server.clients_pending_write 队列中的对象以轮训的方式分发给线程池中各个线程的 io_threads_list 队里中,各个线程从各自的队里中取出消息调用 writeToClient 进行向client进行回应。

IOThreadMain
-> writeToClient

从整体上来看,主线程处理逻辑如下

main
-> aeMain
-> beforesleep
-> handleClientsWithPendingReadsUsingThreads 主线程把clients_pending_read队列请求分发到线程池,让线程处理请求并解析,同时主线程等待线程池处理完解析后,进行接管处理命令,处理后把回应加入到clients_pending_read响应队里

-> handleClientsWithPendingWritesUsingThreads 主线程把clients_pending_read队里中的响应分发到线程池,让线程去处理响应
-> aeProcessEvents 处理事件

线程池的处理逻辑如下

initThreadedIO
-> IOThreadMain
-> writeToClient 处理响应
-> readQueryFromClient 接收请求并解析

因此可以看到,redis6.0 本次采用的多线程,其功能就是接收请求参数、解析请求参数、响应 client 。

redis 多线程实现总结

redis 服务器启动时,创建一个线程池,线程个数最多128个。每个线程有一个队列 (list *io_threads_list[128])。

当主线程触发有客户端请求事件时,把client对象中标志设置为CLIENT_PENDING_READ (说明需要读取请求),然后把触发的客户端加入到clients_pending_read 队里中,随后把该对列中client以轮训的方式分发给线程池中各自线程的队列中,主线程进入循环等待线程池处理请求完毕。

各自线程从自己的队里中取出分配的client对象,然后进行接收客户端发来的请求命令、解析命令,解析完后把client对象中标志设置为CLIENT_PENDING_COMMAND,说明命令已经解析完了。

主线程等待所有线程处理完后,根据 CLIENT_PENDING_COMMAND标志进行执行命令。

命令执行完后,通过调用一组 addReplyXX 函数进行对客户端回应,先把 client 对象中标志设置为 CLIENT_PENDING_WRITE,然后把 client对象加入到全局队列 clients_pending_write 中。

主线程把 clients_pending_write 对列中的client对象的以轮训的方式分发给线程池中各自线程的队列中,然后进入循环等待线程池处理完毕。

线程池中的线程遍历各自的队列,把响应发送个客户端。

主线程等待线程池中的线程处理全部处理完后,做善后处理。

以上操作过程中操作队列中并没有用到锁,原因如下:

在该线程模型中属于生产者和消费者模型,主线程负责往线程池中发送消息,线程池中的线程进行消费,一般来讲,主线程进行向队列生产消息时要加锁,然后释放锁;线程加锁从队列中取消息,释放锁,处理消息。

但是该模型中并没有使用到锁,原因是每个线程都有一个记录各自队里中任务数量的计数(unsigned long io_threads_pending[128]),各个线程不停的循环判断计数是不是大于0,若大于 0 说明有任务,需要从队里中取消息进行处理,此时线程并没出操作队列,所以主线程存放消息时是不需要加锁的。

当线程池中的线程从各自队列中进行消费时,主线程在循环判断线程池是否处理完毕,此时主线程并没有操作线程池中的队列,因此线程消费时也不需要加锁。总之就是主线程操作时线程池不操作,线程池操作时主线程不操作,避免了加锁。

整个过程如下图

在这里插入图片描述

原文链接
redis 多线程实现原理

欢迎关注公众号 Linux码农,获取更多干货

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值