1. redis单线程

redis6.0之前是单线程的,主要指的是网络事件处理模型reactor单线程.
Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返 回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。 但如果严格来讲从 Redis4.0 之后并不是单线程,除了主线程外,它也有后台线程 在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。
要点
- 内核空间: 多路复用IO模型,同步阻塞io ,具体实现为epoll;
- 用户空间: 单reactor单线程
- aeMain() 内部是一个死循环,会在epoll_wait处短暂休眠
- epoll_wait返回的是当前可读、可写的socket列表
- beforeSleep是进入休眠前执行的逻辑,核心是回写数据到socket
- 核心逻辑都是由IO事件触发,要么可读,要么可写,否则执行timer定时任务
- 第一次的IO可读事件,是监听socket(如监听6379的socket),当有握手请求时,会执行accept调用,得到一个连接socket,注册可读回调createClient,往后客户端和redis的数据都通过这个socket进行
- 一个完整的命令,可能会通过多次readQueryFromClient才能从socket读完,这意味这多次可读IO事件
- 命令执行的结果会写,也是这样,大概率会通过多次可写回调才能写完
- 当命令被执行完后,对应的连接会被追加到 clients_pending_write,beforeSleep会尝试回写到socket,写不完会注册可写事件,下次继续写
- 整个过程IO全部都是同步非阻塞,没有浪费等待时间
- 注册事件的函数叫aeCreateFileEvent
同时这个模型有几个缺陷:
- 只能用一个cpu核(忽略后台线程)
- 如果value比较大,redis的QPS会下降得很厉害,有时一个大key就可以拖垮
- QPS难以更上一层楼
redis主线程的时间消耗主要在两个方面:
- 逻辑计算的消耗
- 同步IO读写,拷贝数据导致的消耗
当value比较大时,瓶颈会先出现在同步IO上(假设带宽和内存足够),这部分消耗在于两部分:
- 从socket中读取请求数据,会从内核态将数据拷贝到用户态 (read调用)
- 将数据回写到socket,会将数据从用户态拷贝到内核态 (write调用)
2. redis多线程
beforesleep中,先让IO线程读数据,然后再让IO线程写数据。 读写时,多线程能并发执行,利用多核。
- 将读任务均匀分发到各个IO线程的任务链表io_threads_list[i],将io_threads_pending[i] 设置为对应的任务数,此时IO线程将从死循环中被激活,开始执行任务,执行完毕后,会将 io_threads_pending[i]清零。 函数名为: handleClientsWithPendingReadsUsingThreads
- 将写任务均匀分发到各个IO线程的任务链表io_threads_list[i],将io_threads_pending[i] 设置为对应的任务数,此时IO线程将从死循环中被激活,开始执行任务,执行完毕后,会将 io_threads_pending[i]清零。 函数名为: handleClientsWithPendingWritesUsingThreads
- beforeSleep中主线程也会执行其中一个任务(图中忽略了),执行完后自旋等待IO线程处理完。
- 读任务要么在beforeSleep中被执行,要么在IO线程被执行,不会再在读回调中执行
- 写任务会分散到 beforeSleep、IO线程、写回调中执行
- 主线程和IO线程交互是无锁的,通过标志位设置进行,不会同时写任务链表
3.线程模型
- 多个socket, socket客户端
- IO多路复用程序,epoll_wait程序
- 文件事件分派器,获取到活动fds后分配给不同的处理器
- 事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)。