难道redis不是单线程?我们启动一个redis实例,验证一下就知道了。redis安装部署方式如下所示:
// 下载 wget https://download.redis.io/redis-stable.tar.gz tar -xzvf redis-stable.tar.gz // 编译安装 cd redis-stable make // 验证是否安装成功 ./src/redis-server -v redis server v=7.2.4
接下来启动redis实例,使用命令ps查看所有线程,如下所示:
// 启动redis实例 ./src/redis-server ./redis.conf // 查看实例进程id ps aux | grep redis root 385806 0.0 0.0 245472 11200 pts/2 sl+ 17:32 0:00 ./src/redis-server 127.0.0.1:6379 // 查看所有线程 ps -l -p 385806 pid lwp tty time cmd 385806 385806 pts/2 00:00:00 redis-server 385806 385809 pts/2 00:00:00 bio_close_file 385806 385810 pts/2 00:00:00 bio_aof 385806 385811 pts/2 00:00:00 bio_lazy_free 385806 385812 pts/2 00:00:00 jemalloc_bg_thd 385806 385813 pts/2 00:00:00 jemalloc_bg_thd
竟然有6个线程!不是说redis是单线程吗?怎么会有这么多线程呢?
这6个线程的含义你可能不太了解,但是通过这个示例至少说明redis并不是单线程。
01 redis中的多线程
接下来我们逐个介绍上述6个线程的作用:
redis-server:
主线程,用于接收并处理客户端请求。
jemalloc_bg_thd
jemalloc 是新一代的内存分配器,redis底层使用他管理内存。
bio_xxx:
以bio前缀开始的都是异步线程,用于异步执行一些耗时任务。其中,线程bio_close_file用于异步删除文件,线程bio_aof用于异步将aof文件刷到磁盘,线程bio_lazy_free用于异步删除数据(懒删除)。
需要说明的是,主线程是通过队列将任务分发给异步线程的,并且这一操作是需要加锁的。主线程与异步线程的关系如下图所示:
- 主线程与异步线程
这里我们以懒删除为例,讲解为什么要使用异步线程。redis是一款内存数据库,支持多种数据类型,包括字符串、列表、哈希表、集合等。思考一下,删除(del)列表类型数据的流程是怎样的呢?第一步从数据库字典中删除该键值对,第二步遍历并删除列表中的所有元素(释放内存)。想想如果列表中的元素数目非常多呢?这一步将非常耗时。这种删除方式称为同步删除,流程如下图所示:
- 同步删除流程图
针对上述问题,redis提出了懒删除(异步删除),主线程在收到删除命令(unlink)时,首先从数据库字典中删除该键值对,随后再将删除任务分发给异步线程bio_lazy_free,由异步线程执行第二步耗时逻辑。这时候的流程如下图所示:
- 懒删除流程图
02 i/o多线程
难道redis是多线程?那为什么我们老说redis是单线程呢?这是因为读取客户端命令请求,执行命令以及向客户端返回结果都是在主线程完成的。不然的话,多线程同时操作内存数据库,并发问题如何解决?如果每次操作之前都加锁,那和单线程又有什么区别呢?
当然这一流程在redis6.0版本也发生了改变,redis官方指出,redis是基于内存的键值对数据库,执行命令的过程是非常快的,读取客户端命令请求和向客户端返回结果(即网络i/o)通常会成为redis的性能瓶颈。
因此,在redis 6.0版本,作者加入了多线程i/o的能力,即可以开启多个i/o线程,并行读取客户端命令请求,并行向客户端返回结果。i/o多线程能力使得redis性能提升至少一倍。
为了开启多线程i/o能力,需要先修改配置文件redis.conf:
io-threads-do-reads yes io-threads 4
这两个配置含义如下:
io-threads-do-reads:是否开启多线程i/o能力,默认为"no";
io-threads:i/o线程数目,默认为1,即只使用主线程执行网络i/o,线程数最大为128;该配置应该根据cpu核数设置,作者建议,4核cpu设置2~3个i/o线程,8核cpu设置6个i/o线程。
开启多线程i/o能力之后,重新启动redis实例,查看所有线程,结果如下:
ps -l -p 104648 pid lwp tty time cmd 104648 104648 pts/1 00:00:00 redis-server 104648 104654 pts/1 00:00:00 io_thd_1 104648 104655 pts/1 00:00:00 io_thd_2 104648 104656 pts/1 00:00:00 io_thd_3 ……
由于我们设置了io-threads等于4,所以会创建4个线程用于执行i/o操作(包括主线程),上述结果符合预期。
当然,只有i/o阶段才使用了多线程,处理命令请求还是单线程,毕竟多线程操作内存数据存在并发问题。
最后,开启了i/o多线程之后,命令的执行流程如下图所示:
- i/o多线程流程图
03 redis中的多进程
redis还有多进程?是的。在某些场景下,redis也会创建多个子进程来执行一些任务。以持久化为例,redis支持两种类型的持久化:
- aof(append only file):可以看作是命令的日志文件,redis会将每一个写命令都追加到aof文件。
- rdb(redis database):以快照的方式存储redis内存中的数据。命令save用于手动触发rdb持久化。想想如果redis中的数据量非常大,持久化操作必然耗时比较长,而redis是单线程处理命令请求,那么当命令save的执行时间过长时,必然会影响其他命令的执行。
命令save有可能会阻塞其他请求,为此,redis又引入了命令bgsave,该命令会创建一个子进程来执行持久化操作,这样就不会影响主进程执行其他请求了。
我们可以手动执行命令bgsave验证。首先,使用gdb跟踪redis进程,添加断点,让子进程阻塞在持久化逻辑。如下所示:
// 查询redis进程id ps aux | grep redis root 448144 0.1 0.0 270060 11520 pts/1 tl+ 17:00 0:00 ./src/redis-server 127.0.0.1:6379 // gdb跟踪进程 gdb -p 448144 // 跟踪创建的子进程(默认gdb只跟踪主进程,需手动设置) (gdb) set follow-fork-mode child // 函数rdbsavedb用于持久化数据快照 (gdb) b rdbsavedb breakpoint 1 at 0x541a10: file rdb.c, line 1300. (gdb) c 设置好断点之后,使用redis客户端发送命令bgsave,结果如下: // 请求立即返回 127.0.0.1:6379> bgsave background saving started // gdb输出以下信息 [new process 452541] breakpoint 1, rdbsavedb (...) at rdb.c:1300 可以看到,gdb目前跟踪的是子进程,进程id是452541。也可以通过linux命令 ps 查看所有进程,结果如下: ps aux | grep redis root 448144 0.0 0.0 270060 11520 pts/1 sl+ 17:00 0:00 ./src/redis-server 127.0.0.1:6379 root 452541 0.0 0.0 270064 11412 pts/1 t+ 17:19 0:00 redis-rdb-bgsave 127.0.0.1:6379
可以看到子进程的名称是redis-rdb-bgsave,也就是该进程将所有数据的快照持久化在rdb文件。
问题
问题1:为什么采用子进程而不是子线程呢?
因为rdb是将数据快照持久化存储,如果采用子线程,主线程与子线程将会共享内存数据,主线程在持久化的同时还会修改内存数据,这有可能导致数据不一致。而主进程与子进程的内存数据是完全隔离的,不存在此问题。
问题2:假设redis内存中存储了10gb的数据,在创建子进程执行持久化操作之后,此时子进程也需要10gb的内存吗?复制10gb的内存数据,也会比较耗时吧?另外如果系统只有15gb的内存,还能执行bgsave命令吗?
这里有一个概念叫写时复制(copy on write),在使用系统调用fork创建子进程之后,主进程与子进程的内存数据暂时还是共享的,但是当主进程需要修改内存数据时,系统会自动将该内存块复制一份,以此实现内存数据的隔离。
04 结论
redis的进程模型/线程模型还是比较复杂的,这里也只是简单介绍了部分场景下的多线程以及多进程,其他场景下的多线程、多进程还有待读者自己研究。
以上就是详解redis单线程架构的优势与不足的详细内容,更多关于redis单线程架构的资料请关注代码网其它相关文章!
发表评论