在redis6之后,引入了多线程,主要是因为硬件的发展,IO设备的吞吐能力在大大增强,很适合同时多任务大批量数据读写。
在了解redis的多线程之前,先来大概看下C语言多线程与metux锁的使用:
void test_thread_mutex();
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main(int argc, char **argv) {
test_thread_mutex();
}
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 获取互斥锁
sleep(10);
printf(" new thread! Thread ID: %lu\n", (unsigned long)pthread_self());
pthread_mutex_unlock(&mutex); // 释放互斥锁
return NULL;
}
void test_thread_mutex(){
pthread_t tid;
pthread_create(&tid, NULL, thread_function, NULL);//创建新线程并在新线程运行function
printf("Main thread start...\n");
pthread_mutex_lock(&mutex); // 获取互斥锁
printf("Main thread processing\n");
pthread_mutex_unlock(&mutex); // 释放互斥锁
pthread_join(tid, NULL);
}
代码很简单,如果没了解过,直接看redis多线程的源码有点阻碍。
启动多线程可以根据主机核数配置线程数:
官方的说明,对于四核主机,可以配置2-3个线程,对于八核主机可以配置6个线程。具体咱也没测过。最多可以配置#define IO_THREADS_MAX_NUM 128
个线程。
redis默认是不对读任务进行多任务处理的,如果需要启动读任务多线程处理需要在配置文件配置:
io-threads-do-reads yes
在main()方法中,进入InitServerLast(),完成bioInit()和initThreadIO():
初始化线程池完成后,缓存多个Io thread,thread2-4被bio线程占用,看名字可能是跟文件关闭、aof持久化等相关的。
查看其中一个子线程,可以看到该线程被阻塞:
由多线程初始化可知,最开始,由于主线程抢占了各个子线程的锁,每个子线程都在阻塞状态,因此主线程需要在某处释放锁,启动各个子线程:
通过pthread_mutex_unlock(&io_threads_mutex[id])
在IDE里ctrl+G,全局查找释放锁的位置:一个是在子线程本身,一个是在startThreadIO();
继续跟踪发现,只有handleClientsWithPendingWritesUsingThreads()中调用了startThreadedIO(),handleClientsWithPendingWritesUsingThreads主要的作用是处理pending的写任务,这里主要先看何时启动子线程:
在aeMain()中不断循环遍历获取epoll事件过程中:
在redis源码之:事件驱动epoll中我们分析过,eventloop不断循环遍历获取事件,然后读事件会通过handler调用到readQueryFromClient(),之前在redis源码之:客户端命令执行Command是以单线程的视角解读,现在来看看多线程下的逻辑:
那在多线程调用readQueryFromClient()的时候,何时结束子线程的调用,在主线程继续命令执行?在readQueryFromClient()执行到processInputBuffer©:
因此可以得出结论,在redis中,可以多线程执行多个client读数据,但是最后还是按client到达的先后顺序执行每个client对应的命令。事务的一致性跟单线程一样的。
对于写数据的场景,主要看handleClientsWithPendingWritesUsingThreads()
,套路跟读数据的情况差不多,分多个线程写数据到客户端,不过写数据后续没有命令执行的步骤。这里就不分析了。
handleClientsWithPendingWritesUsingThreads中停止多线程、通过定时任务在serverCron中尝试关停多线程
在initServer()的时候,设置了timeEvent,并设置回调函数serverCron().,不断检查当前pending的写任务个数,判断是否暂停多线程。
跟上面写事件的跟踪流程差不多。以infoCommand为例,在执行完命令后,会调用addReplyVerbatim()-》addReplyProto()返回响应,里面会调用prepareClientToWrite-》clientInstallWriteHandler()将当前处理的client放到pending_write队列,这也是开启多线程的关键;并将响应信息通过_addReplyToBuffer()写入到client的buffer中,此时并没有写入到fd对应的socket的buffer。
然后再eventloop的下次循环中,在beforesleep,完成handleClientsWithPendingReadsUsingThreads()后,调用handleClientsWithPendingWritesUsingThreads(),通过多线程对多个client完成writeToClient()调用connWrite()->connSocketWrite(),返回响应到客户端。
在handleClientsWithPendingWritesUsingThreads中通过多线程并行处理多个写任务过程中,有些写任务由于每次写有字节限制,未完成全部数据写的client,会通过connSocketSetWriteHandler()注册写事件的handler(sendReplyToClient())在epoll中,写事件表示一个文件描述符(通常是套接字)已经准备好进行写操作。当一个套接字的写缓冲区有空间可以写入数据时,会触发写事件,从而调用sendReplyToClient(),完成剩余字节的写。