从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 说明有任务,需要从队里中取消息进行处理,此时线程并没出操作队列,所以主线程存放消息时是不需要加锁的。
当线程池中的线程从各自队列中进行消费时,主线程在循环判断线程池是否处理完毕,此时主线程并没有操作线程池中的队列,因此线程消费时也不需要加锁。总之就是主线程操作时线程池不操作,线程池操作时主线程不操作,避免了加锁。
整个过程如下图