扩展阅读-穿越Redis单线程迷雾:从面试场景到技能内核的解读
https://i-blog.csdnimg.cn/blog_migrate/45eaba9c89fe29299344e696ba8fdbd0.png目录
编辑
前言
Redis中的多线程
I/O多线程
Redis中的多历程
结论
延伸阅读
前言
很多人都遇到过这么一道面试题:Redis是单线程还是多线程?这个题目既简单又复杂。说他简单是因为大多数人都知道Redis是单线程,说复杂是因为这个答案其实并不准确。
岂非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并不是单线程。
Redis中的多线程
接下来我们逐个介绍上述6个线程的作用:
1)redis-server:
主线程,用于接收并处理客户端哀求。
2)jemalloc_bg_thd:
jemalloc 是新一代的内存分配器,Redis底层利用他管理内存。
3)bio_xxx:
以bio前缀开始的都是异步线程,用于异步实行一些耗时任务。此中,线程bio_close_file用于异步删除文件,线程bio_aof用于异步将AOF文件刷到磁盘,线程bio_lazy_free用于异步删除数据(懒删除)。
需要说明的是,主线程是通过队列将任务分发给异步线程的,并且这一操作是需要加锁的。主线程与异步线程的关系如下图所示:
https://i-blog.csdnimg.cn/blog_migrate/8dc51f642d321bc186146073740596a1.png
这里我们以懒删除为例,讲解为什么要利用异步线程。Redis是一款内存数据库,支持多种数据类型,包罗字符串、列表、哈希表、聚集等。思考一下,删除(DEL)列表类型数据的流程是怎样的呢?第一步从数据库字典中删除该键值对,第二步遍历并删除列表中的全部元素(开释内存)。想想假如列表中的元素数量非常多呢?这一步将非常耗时。这种删除方式称为同步删除,流程如下图所示:
https://i-blog.csdnimg.cn/blog_migrate/e79a3013c97ac903b9f8b5d2907e69bd.png
针对上述题目,Redis提出了懒删除(异步删除),主线程在收到删除下令(UNLINK)时,首先从数据库字典中删除该键值对,随后再将删除任务分发给异步线程bio_lazy_free,由异步线程实行第二步耗时逻辑。这时候的流程如下图所示:
https://i-blog.csdnimg.cn/blog_migrate/4a86b36e7f4b66dd4c92105bfad5e34b.png
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多线程之后,下令的实行流程如下图所示:
https://i-blog.csdnimg.cn/blog_migrate/8634d4f6c47d9202ccab6037742ab489.png
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输出以下信息
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创建子历程之后,主历程与子历程的内存数据暂时还是共享的,但是当主历程需要修改内存数据时,系统会主动将该内存块复制一份,以此实现内存数据的隔离。
下令BGSAVE的实行流程如下图所示:
https://i-blog.csdnimg.cn/blog_migrate/5f9f6da4e565de8569decfd3d4738877.png
结论
Redis的历程模型/线程模型还是比较复杂的,这里也只是简单介绍了部分场景下的多线程以及多历程,其他场景下的多线程、多历程另有待读者自己研究。
延伸阅读
书本名称:《 高效利用Redis:一书学透数据存储与高可用集群 》
https://i-blog.csdnimg.cn/blog_migrate/936e759c2217f5fed117191f104f1694.png
保举语: 深入Redis数据布局与底层实现,攻克Redis数据存储与集群管理难题。
作者介绍
李乐:好将来Golang开辟专家、西安电子科技大学硕士,曾就职于滴滴,乐于钻研技能与源码,合著有《高效利用Redis:一书学透数据存储与 高可用 集群》《Redis5设计与源码分析》《Nginx底层设计与源码分析》。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]