很多人认为 Redis 是单线程,这个描述是不准确的。准确来说 Redis 只有在处理「客户端请求」比如接收客户端请求
、解析请求
和进行数据读写
等操作时,是单线程的。但整个 Redis Server 并不是单线程的,还有后台线程,比如文件关闭
、AOF 同步写
和惰性删除
在辅助处理一些工作。
Redis 选择单线程处理请求,是因为 Redis 操作的是「内存」,加上设计了「高效」的数据结构,所以操作速度极快,利用 「IO 多路复用」机制,单线程依旧可以有非常高的性能。
但如果一个请求发生耗时,单线程的缺点就暴露出来了,后面的请求都要「排队」等待,所以 Redis 在启动时会启动一些「后台线程」来辅助工作,目的是把耗时的操作,放到后台处理,避免主线程操作耗时影响整体性能
关闭 fd、AOF 刷盘、释放 key 的内存,这些耗时操作,都可以放到后台线程中处理,对主逻辑没有任何影响
后台线程处理这些任务,就相当于一个消费者,生产者(主线程)把耗时任务丢到队列中(链表),消费者不停轮询这个队列,拿出任务就去执行对应的方法即可:
后台线程有3个,后台进程只有RDB和AOF rewrite时才会fork子进程。
Redis 后台任务使用 bio_job 结构体来描述,该结构体用了三个指针变量来表示任务参数
struct bio_job { time_t time; void *arg1, *arg2, *arg3; //传递给任务的参数 };
如果我们创建的任务,所需要的参数大于 3 个,最直接的方法就是,使用指针数组,因为指针数组本身就是一个个指针,可以通过index的顺序标记参数的含义类型,通过index就能快速获取不同的参数对应的指针这样就可以传递任意数量参数了。因为这里 Redis 的后台任务都比较简单,最多 3 个参数就足够满足需求,所以 job 直接写死了 3 个参数变量,这样做的好处是维护起来简单直接
在启动 Redis 实例时,可以在 shell 中,执行 redis-server 这个可执行文件,如下所示:
./redis-server /etc/redis/redis.conf
运行这个命令后,实际会调用 fork 系统调用函数,来新建一个进程。因为 shell 本身是一个进程,所以,这个通过 fork 新创建的进程就被称为是 shell 进程的子进程,而 shell 进程被称为父进程
紧接着,shell 进程会调用 execve (内核级系统调用)系统调用函数,将子进程执行的主体替换成 Redis 的可执行文件。而 Redis 可执行文件的入口函数就是 main 函数,这样一来,子进程就会开始执行 Redis server 的 main 函数了。
//filename 是要运行的程序的文件名,argv[]和 envp[]分别是要运行程序的参数和环境变量 int execve(const char *filename, char *const argv[], char *const envp[]))
execve 函数只是把子进程的执行内容替换成 Redis 可执行文件,子进程从 shell 父进程继承到的标准输入和标准输出保持不变,所以shell窗口会输出以下:
37807:M 19 Aug 2021 07:29:36.372 # Server initialized 37807:M 19 Aug 2021 07:29:36.372 * DB loaded from disk: 0.000 seconds 37807:M 19 Aug 2021 07:29:36.372 * Ready to accept connections
Redis 运行时通过 serverLog 函数打印的日志信息,就会默认输出到终端屏幕上了,也就是 shell 进程的标准输出。
Redis 进程创建开始运行后,从 main 函数开始执行。调用 initServerConfig 函数初始化 Redis server 的运行参数,调用 loadServerConfig 函数解析配置文件参数。当 main 函数调用这些函数时,这些函数仍然是由原来的进程执行的。所以,在这种情况下,Redis 仍然是单个进程在运行。完成参数解析后,会根据两个配置参数 daemonize(是否以守护进程方式) 和 supervised(是否使用 upstart 或 systemd 守护进程的管理程序),来设置变量 background 的值。
守护进程是在系统后台运行的进程,独立于 shell 终端,不再需要用户在 shell 中进行输入了。一般来说,守护进程用于执行周期性任务或是等待相应事件发生再进行处理。Redis server 本身就是在启动后,等待客户端输入,再进行处理。所以对于 Redis 这类服务器程序来说,我们通常会让它以守护进程方式运行。
fork 函数的不同返回值,其实代表了不同的含义,具体来说:
daemonize 函数调用 fork 函数
对于 Redis 来说,它的主要工作,包括接收客户端请求、解析请求和进行数据读写等操作,都没有创建新线程来执行,所以,Redis 主要工作的确是由单线程来执行的,这也是我们常说 Redis 是单线程程序的原因。因为 Redis 主要工作都是 IO 读写操作,所以,一般会把这个单线程称为主 IO 线程。
main 函数在初始化过程最后调用的 InitServerLast 函数。InitServerLast 函数的作用是进一步调用 bioInit 函数,来创建后台线程,让 Redis 把部分任务交给后台线程处理
void InitServerLast() { bioInit(); //调用 pthread_create 函数创建多个后台线程 … }
线程属性
bioInit 函数
设计方式是典型的生产者 - 消费者模型。bioCreateBackgroundJob 函数是生产者,负责往每种任务队列中加入要执行的后台任务,而 bioProcessBackgroundJobs 函数是消费者,负责从每种任务队列中取出任务来执行。然后 Redis 创建的后台线程,会调用 bioProcessBackgroundJobs 函数,从而实现一直循环检查任务队列。