当前位置:   article > 正文

由面试题“Redis是否为单线程”引发的思考【文末送书】_高效使用redis:一书学透数据存储与高可用集群

高效使用redis:一书学透数据存储与高可用集群

Redis是否为单线程引发的思考

在面试过程中,关于Redis是否为单线程的问题常常成为技术面试中的热门话题。这个问题涉及到Redis的核心架构,也牵扯到了对于单线程和多线程的理解。在探讨这个问题的过程中,我们不仅仅可以了解Redis的内部工作机制,还能深入思考单线程在现代计算机科学中的应用和局限性。

首先,让我们解答这个常见的面试问题:是的,Redis被称为单线程的数据库。但是,这并不代表Redis无法处理多个客户端请求。相反,Redis通过使用I/O多路复用的技术,能够在一个线程中高效地处理成千上万个并发连接。这使得Redis在读写密集型的场景下表现出色,因为它避免了多线程之间的竞争和同步开销。

然而,这并不是说Redis完全摒弃了多线程。虽然Redis主要采用单线程处理命令请求,但在后台会有一些工作线程用于执行一些耗时的操作,比如持久化和复制。这种设计使得Redis能够充分利用单线程的优势,同时通过少量的后台工作线程处理一些可能会阻塞主线程的任务,确保系统的稳定性和性能。

从这个问题引发的思考,我们可以深入探讨单线程在当今计算机科学领域的应用和局限性。单线程在某些场景下能够简化程序设计,避免了多线程之间的复杂的同步问题,提高了程序的可维护性。然而,对于高并发和计算密集型任务,单线程的性能可能会受到限制,因为它无法充分利用多核处理器的优势。

在实际开发中,我们需要根据具体场景选择合适的技术栈和架构。对于I/O密集型的任务,像Redis这样的单线程模型可能是一个不错的选择;而对于计算密集型任务,采用多线程或者多进程的模型可能更为合适。

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

接下来启动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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

竟然有6个线程!不是说Redis是单线程吗?怎么会有这么多线程呢?

这6个线程的含义你可能不太了解,但是通过这个示例至少说明Redis并不是单线程。

01 Redis中的多线程

接下来我们逐个介绍上述6个线程的作用:

1)redis-server:

主线程,用于接收并处理客户端请求。

2)jemalloc_bg_thd

jemalloc 是新一代的内存分配器,Redis底层使用他管理内存。

3)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
  • 1
  • 2

这两个配置含义如下:

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
……
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

由于我们设置了io-threads等于4,所以会创建4个线程用于执行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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

设置好断点之后,使用Redis客户端发送命令BGSAVE,结果如下:

// 请求立即返回
127.0.0.1:6379> bgsave
Background saving started

// GDB输出以下信息
[New process 452541]
Breakpoint 1, rdbSaveDb (...) at rdb.c:1300
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可以看到,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
  • 1
  • 2
  • 3

可以看到子进程的名称是redis-rdb-bgsave,也就是该进程将所有数据的快照持久化在RDB文件。

最后再思考两个问题。

问题1:为什么采用子进程而不是子线程呢?

因为RDB是将数据快照持久化存储,如果采用子线程,主线程与子线程将会共享内存数据,主线程在持久化的同时还会修改内存数据,这有可能导致数据不一致。而主进程与子进程的内存数据是完全隔离的,不存在此问题。

问题2:假设Redis内存中存储了10GB的数据,在创建子进程执行持久化操作之后,此时子进程也需要10GB的内存吗?复制10GB的内存数据,也会比较耗时吧?另外如果系统只有15GB的内存,还能执行BGSAVE命令吗?

这里有一个概念叫写时复制(copy on write),在使用系统调用fork创建子进程之后,主进程与子进程的内存数据暂时还是共享的,但是当主进程需要修改内存数据时,系统会自动将该内存块复制一份,以此实现内存数据的隔离。
命令BGSAVE的执行流程如下图所示:
在这里插入图片描述

04 结论

Redis的进程模型/线程模型还是比较复杂的,这里也只是简单介绍了部分场景下的多线程以及多进程,其他场景下的多线程、多进程还有待读者自己研究。

高效使用Redis:一书学透数据存储与高可用集群

1)知名企业专家联合撰写,助你攻克Redis数据存储与集群管理难题 2)详解Redis新特性、多种数据结构、启动与命令执行过程,以及持久化、主从复制和高可用集群的原理、实现与应用技巧

在这里插入图片描述
内容简介
全书主要分为三部分介绍Redis。
第1部分介绍Redis6中使用的数据结构,包括动态字符串、跳跃表、压缩列表、字典、整数集合和快速链表,详细介绍其基本结构及常见操作。
第二部分为本书核心篇章,首先介绍了Redis6的启动流程,命令解析流程,之后对Redis6中的命令实现进行了全面的介绍,包括键命令、字符串命令、哈希表命令、列表命令、集合及有序集合命令、地理位置相关的GEO命令、统计相关的HyperLogLog命令。
第三部分,主要介绍了Redis6的一些特性及使用,包括事务、持久化、主从复制以及集群等。
作者简介
熊浩含,字节跳动后端高级工程师,曾就职于百度、腾讯和滴滴。对Redis等开源软件有较深的研究。乐于钻研技术难点,喜欢折腾,在学习方面总结了一套较实用的方法论。
陈 雷,希望学产研负责人,清华大学与北京邮电大学硕士,曾在百度、腾讯和滴滴等公司工作,拥有15年产品研发经验,合著有《PHP 7底层设计与源码实现》。
黄 桃,希望学增长研发部负责人,从事互联网服务端研发与架构工作多年,熟悉PHP、Nginx、Redis等源码实现,乐于学习与分享,合著有《PHP 7底层设计与源码实现》。
李 乐,好未来Golang开发专家、西安电子科技大学硕士,曾就职于滴滴,乐于钻研技术与源码,合著有《Redis 5设计与源码分析》《Nginx底层设计与源码分析》。
施洪宝,Shopee后端研发工程师、东南大学硕士,对Redis、Nginx等开源软件有较深的理解,对高并发、分布式技术有浓厚兴趣。
周生政,与爱为舞后端高级工程师,曾就职于字节跳动、滴滴和北京环球国广媒体科技有限公司,熟悉To B和To C业务,拥有多年后端开源软件研究与实践经验,曾用Java、Go、Python、PHP开发线上系统。

正版购买地址:
https://product.dangdang.com/29667600.html

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号