上周参加了字节跳动的面试,整场下来一共70分钟,面试官很是Nice,无奈本身太过紧张,不少准备好的知识点都没有可以准确传达意思。面试
面试中由于在简历上有提到Redis相关的内容,那么毫无疑问就会被问到了。先从经典的问题开始:Reids为何这么快?那天然会回答诸如单线程、IO多路复用等固定套路,而后这里由于一直有关注Redis的相关新闻,知道Redis 6.0年底发布了RC1版本,其中新特性包括多线程IO,那么天然想在面试中说起一下。面试官应该对这点比较感兴趣,因而就继续探讨了这个多线程IO的模型。算法
Q:Redis 6多线程是指什么?数据库
A:Redis这边将部分处理流程改成多线程,具体来讲是..后端
Q:是指查询是多线程吗?网络
A:应该说是处理请求的最后部分改成了多线程,由于这些部分涉及到数据的IO,是整个(Redis)模型中最耗时的部分,因此改为了多线程;这部分以前的好比用户请求进来、将请求放入一个队列中,仍是单线程的。(注意这部分回答是错误的,实际上Redis是将网络IO的部分作成了多线程,后文继续分析)数据结构
Q:若是我有一个SET操做的话,是单线程仍是多线程?多线程
A:多线程。(回答也是错的)并发
Q:那若是是,由于Redis都是内存操做,若是多线程操做一个数据结构的话会有问题吗?ide
A:Emm,目前我理解的模型上看确实会有问题,好比并发改同一个Key,那可能Redis有对应处理这些问题好比进行加锁处理。(确实不了解,回答也天然是错的)post
Q:好,下一个问题..
这里先总结一下:
由于Antirez在Redis Day介绍过,因此就了解到了有这么个新Feature,可是具体的实现由于没有看过源码,因此实际上对这个多线程模型的理解是有误差的。
若是对这些点没有十足的把握的话,面试中尝试本身思考和解决这样的问题实际上仍是会比较扣分,首先若是猜错了的话确定不行,其次即便是猜对了也很难有足够的知识储备去复述出完整的模型出来,也会让本身一边思考一边表达起来很费劲。
因而坑坑洼洼地坚持完了70分钟的面试,再总结一下作得不足的地方,由于是1.5Year经验,面试官主要考察:
现有的业务的一些设计细节的问题:要提早准备好你想介绍给面试官的业务系统,我的认为应该从业务中选出一两个难度比较大的点会比较合适。此次面试没有可以拿出对应的业务来介绍,是准备不到位。
数据库的基础知识:这块以为回答得还能够,不过有的时候由于准备的东西比较多,会常常想充分地展示和描述,有的时候可能会比较冗长,也是表达不够精确的问题。
计算机网络的基础知识:不是科班毕业,没有可以答完美,实际上问题并不难。
计算机系统的基础知识:同上。
一道算法题:字节跳动给的算法题仍是偏简单和经典的,建议多刷题和看Discussion总结。
因此就这样结束了第一次的社招面试,总体来讲几个方向的基础知识须要回去再多写多看就能够了,而后表达上尽可能控制时间和范围,深刻的内容若是面试官但愿和你继续探讨,天然会发问,若是没问,能够说起可是不该该直接展开讲。
面试结束后立刻知道这块的回答有问题,检查果真如此。因此也就借这个机会将Threaded IO对应的源码看了一遍,后续若是有机会的话,但愿能跟下一位面试官再来探讨这个模型。
本次新增的代码位于networking.c中,很显然多线程生效的位置就能猜出来是在网络请求上。做者但愿改进读写缓冲区的性能,而不是命令执行的性能主要缘由是:
读写缓冲区的在命令执行的生命周期中是占了比较大的比重
Redis更倾向于保持简单的设计,若是在命令执行部分改用多线程会不得不处理各类问题,例如并发写入、加锁等
那么将读写缓冲区改成多线程后整个模型大体以下:
首先,若是用户没有开启多线程IO,也就是io_threads_num == 1时直接按照单线程模型处理;若是超过线程数IO_THREADS_MAX_NUM上限则异常退出。
紧接着Redis使用listCreate()建立io_threads_num个线程,而且对主线程(id=0)之外的线程进行处理:
初始化线程的等待任务数为0
获取锁,使得线程不能进行操做
将线程tid与Redis中的线程id(for循环生成)进行映射
/* Initialize the data structures needed for threaded I/O. */
void initThreadedIO(void) {
io_threads_active = 0; /* We start with threads not active. */
/* Don't spawn any thread if the user selected a single thread:
* we'll handle I/O directly from the main thread. */
// 若是用户没有开启多线程IO直接返回 使用主线程处理
if (server.io_threads_num == 1) return;
// 线程数设置超过上限
if (server.io_threads_num > IO_THREADS_MAX_NUM) {
serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
"The maximum number is %d.", IO_THREADS_MAX_NUM);
exit(1);
}
/* Spawn and initialize the I/O threads. */
// 初始化io_threads_num个对应线程
for (int i = 0; i < server.io_threads_num; i++) {
/* Things we do for all the threads including the main thread. */
io_threads_list[i] = listCreate();
if (i == 0) continue; // Index 0为主线程
/* Things we do only for the additional threads. */
// 非主线程则须要如下处理
pthread_t tid;
// 为线程初始化对应的锁
pthread_mutex_init(&io_threads_mutex[i],NULL);
// 线程等待状态初始化为0
io_threads_pending[i] = 0;
// 初始化后将线程暂时锁住
pthread_mutex_lock(&io_threads_mutex[i]);
if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
exit(1);
}
// 将index和对应线程ID加以映射
io_threads[i] = tid;
}
}
Redis须要判断是否知足Threaded IO条件,执行if (postponeClientRead(c)) return;,执行后会将Client放到等待读取的队列中,并将Client的等待读取Flag置位:
int postponeClientRead(client *c) {
if (io_threads_active && // 线程是否在不断(spining)等待IO
server.io_threads_do_reads && // 是否多线程IO读取
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
{//client不能是主从,且未处于等待读取的状态
c->flags |= CLIENT_PENDING_READ; // 将Client设置为等待读取的状态Flag
listAddNodeHead(server.clients_pending_read,c); // 将这个Client加入到等待读取队列
return 1;
} else {
return 0;
}
}
这时server维护了一个clients_pending_read,包含全部处于读事件pending的客户端列表。
首先,Redis检查有多少等待读的client:
listLength(server.clients_pending_read)
若是长度不为0,进行While循环,将每一个等待的client分配给线程,当等待长度超过线程数时,每一个线程分配到的client可能会超过1个:
int item_id = 0;
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++;
}
而且修改每一个线程须要完成的数量(初始化时为0):
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
io_threads_pending[j] = count;
}
等待处理直到没有剩余任务:
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;
}
最后清空client_pending_read:
listRewind(server.clients_pending_read,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_READ;
if (c->flags & CLIENT_PENDING_COMMAND) {
c->flags &= ~ CLIENT_PENDING_COMMAND;
processCommandAndResetClient(c);
}
processInputBufferAndReplicate(c);
}
listEmpty(server.clients_pending_read);
在上面的过程当中,当任务分发完毕后,每一个线程按照正常流程将本身负责的Client的读取缓冲区的内容进行处理,和原来的单线程没有太大差别。
每轮处理中,须要将各个线程的锁开启,而且将相关标志置位:
void startThreadedIO(void) {
if (tio_debug) { printf("S"); fflush(stdout); }
if (tio_debug) printf("--- STARTING THREADED IO ---\n");
serverAssert(io_threads_active == 0);
for (int j = 1; j < server.io_threads_num; j++)
// 解开线程的锁定状态
pthread_mutex_unlock(&io_threads_mutex[j]);
// 如今能够开始多线程IO执行对应读/写任务
io_threads_active = 1;
}
一样结束时,首先须要检查是否有剩余待读的IO,若是没有,将线程锁定,标志关闭:
void stopThreadedIO(void) {
// 须要中止的时候可能还有等待读的Client 在中止前进行处理
handleClientsWithPendingReadsUsingThreads();
if (tio_debug) { printf("E"); fflush(stdout); }
if (tio_debug) printf("--- STOPPING THREADED IO [R%d] [W%d] ---\n",
(int) listLength(server.clients_pending_read),
(int) listLength(server.clients_pending_write));
serverAssert(io_threads_active == 1);
for (int j = 1; j < server.io_threads_num; j++)
// 本轮IO结束 将全部线程上锁
pthread_mutex_lock(&io_threads_mutex[j]);
// IO状态设置为关闭
io_threads_active = 0;
}
Redis的Threaded IO模型中,每次全部的线程都只能进行读或者写操做,经过io_threads_op控制,同时每一个线程中负责的client依次执行:
// 每一个thread有可能须要负责多个client
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");
}
}
每一个线程执行readQueryFromClient,将对应的请求放入一个队列中,单线程执行,最后相似地由多线程将结果写入客户端的buffer中。
Threaded IO将服务读Client的输入缓冲区和将执行结果写入输出缓冲区的过程改成了多线程的模型,同时保持同一时间所有线程均处于读或者写的状态。可是命令的具体执行还是以单线程(队列)的形式,由于Redis但愿保持简单的结构避免处理锁和竞争的问题,而且读写缓冲区的时间占命令执行生命周期的比重较大,处理这部分的IO模型会给性能带来显著的提高。
特别声明:本文素材来源于网络,仅做为分享学习之用,若有侵权,请联系删除!