【实战篇】Redis单线程架构的优势与不足

王柳  金牌会员 | 2024-6-14 22:00:24 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 992|帖子 992|积分 2976



  
   很多人都遇到过这么一道口试题:Redis是单线程照旧多线程?这个问题既简单又复杂。说他简单是由于大多数人都知道Redis是单线程,说复杂是由于这个答案其实并不正确。
  

  • 难道Redis不是单线程?我们启动一个Redis实例,验证一下就知道了。Redis安装摆设方式如下所示:
  1. // 下载
  2. wget https://download.redis.io/redis-stable.tar.gz
  3. tar -xzvf redis-stable.tar.gz
  4. // 编译安装
  5. cd redis-stable
  6. make
  7. // 验证是否安装成功
  8. ./src/redis-server -v
  9. Redis server v=7.2.4
复制代码


  • 接下来启动Redis实例,利用下令ps检察全部线程,如下所示:
  1. // 启动Redis实例
  2. ./src/redis-server ./redis.conf
  3. // 查看实例进程ID
  4. ps aux | grep redis
  5. root     385806  0.0  0.0 245472 11200 pts/2    Sl+  17:32   0:00 ./src/redis-server 127.0.0.1:6379
  6. // 查看所有线程
  7. ps -L -p 385806
  8.    PID    LWP TTY          TIME CMD
  9. 385806 385806 pts/2    00:00:00 redis-server
  10. 385806 385809 pts/2    00:00:00 bio_close_file
  11. 385806 385810 pts/2    00:00:00 bio_aof
  12. 385806 385811 pts/2    00:00:00 bio_lazy_free
  13. 385806 385812 pts/2    00:00:00 jemalloc_bg_thd
  14. 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:
  1. io-threads-do-reads yes
  2. 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实例,检察全部线程,效果如下:
  1. ps -L -p 104648
  2.    PID    LWP TTY          TIME CMD
  3. 104648 104648 pts/1    00:00:00 redis-server
  4. 104648 104654 pts/1    00:00:00 io_thd_1
  5. 104648 104655 pts/1    00:00:00 io_thd_2
  6. 104648 104656 pts/1    00:00:00 io_thd_3
  7. ……
复制代码
由于我们设置了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进程,添加断点,让子进程阻塞在持久化逻辑。如下所示:
  1. // 查询Redis进程ID
  2. ps aux | grep redis
  3. root     448144  0.1  0.0 270060 11520 pts/1    tl+  17:00   0:00 ./src/redis-server 127.0.0.1:6379
  4. // GDB跟踪进程
  5. gdb -p 448144
  6. // 跟踪创建的子进程(默认GDB只跟踪主进程,需手动设置)
  7. (gdb) set follow-fork-mode child
  8. // 函数rdbSaveDb用于持久化数据快照
  9. (gdb) b rdbSaveDb
  10. Breakpoint 1 at 0x541a10: file rdb.c, line 1300.
  11. (gdb) c
  12. 设置好断点之后,使用Redis客户端发送命令BGSAVE,结果如下:
  13. // 请求立即返回
  14. 127.0.0.1:6379> bgsave
  15. Background saving started
  16. // GDB输出以下信息
  17. [New process 452541]
  18. Breakpoint 1, rdbSaveDb (...) at rdb.c:1300
  19. 可以看到,GDB目前跟踪的是子进程,进程ID是452541。也可以通过Linux命令 ps 查看所有进程,结果如下:
  20. ps aux | grep redis
  21. root     448144  0.0  0.0 270060 11520 pts/1    Sl+  17:00   0:00 ./src/redis-server 127.0.0.1:6379
  22. 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 结论

   作者介绍
李乐:好未来Golang开发专家、西安电子科技大学硕士,曾就职于滴滴,乐于钻研技能与源码,合著有《高效利用Redis:一书学透数据存储与高可用集群》《Redis5计划与源码分析》《Nginx底层计划与源码分析》。
  ▼
  《高效利用Redis:一书学透数据存储与高可用集群》
  

    保举语:深入Redis数据结构与底层实现,攻克Redis数据存储与集群管理困难。
  Redis的进程模子/线程模子照旧比力复杂的,这里也只是简单介绍了部分场景下的多线程以及多进程,其他场景下的多线程、多进程另有待读者本身研究。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王柳

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表