目录
- 1. Redis 是单线程的,还是多线程的?
- 2. Redis单线程模式是怎么样的?
- Redis 单线程模式的优势
- Redis 单线程的局限性
- Redis 单线程的优化策略
- 3. Redis采用单线程为什么还这么快
- 4. Redis 6.0 之前为什么使用单线程?
- 5. Redis 6.0 之后为何引入多线程?
1. Redis 是单线程的,还是多线程的?
Redis 的主要
操作是单线程的,这意味着它在主线程上处理所有请求,而不像一些数据库采用多线程模型。这是因为 Redis 的开发者追求的是简化并发操作、避免线程锁机制带来的复杂性和性能开销。通过单线程模型,Redis 避免了常见的线程切换、死锁等问题,大大提升了效率。
Redis 单线程指的是「接收客户端清求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过
程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis6.0版本之后引入了多线程来处理网络请求(提高网络 I0 读写性能)
但是,Redis 程序并不是仅仅是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
- Redis 在 2.6 版本,会启动2个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
- Redis 在 4.0 版本之后,新增了一个新的后台线程,用来
异步释放 Redis 内存
,也就是 lazyfree 线程。例如执行 unlink key/flushdb async/flushall async 等命令,会把这些删除操作交给后台线程来执行好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除
因为 del是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用unlink 命令来异步删除大key
。
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存
」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
2. Redis单线程模式是怎么样的?
Redis6.0版本之前的单线程模式如下图:(图片来源小林coding)
在 Redis 6.0 版本之前,Redis 的处理模型完全是 单线程 的,所有操作都在一个线程中完成。这种单线程模式虽然看似简单,但由于 Redis 的设计精巧,结合了高效的内存操作和 I/O 多路复用机制,它依然能够在很多场景下表现出色。
Redis 使用 I/O 多路复用机制
来处理并发客户端连接。在这种机制下,一个线程可以同时监听多个文件描述符(即客户端连接)。当某个文件描述符有事件发生(如客户端发送了命令或等待处理的请求到来),Redis 会处理这些事件,而不是为每个连接创建一个独立的线程。
具体的步骤如下:
1. 事件监听:Redis 的主线程通过 I/O 多路复用(如 epoll、select 等系统调用)监听多个客户端连接。当某个连接有新的请求到来时,Redis 会记录这个事件。
2. 事件处理:当有多个客户端的事件需要处理时,Redis 按照队列的顺序处理每个请求。这意味着 Redis 通过单线程的轮询方式来处理每一个客户端请求。
3. 命令执行:Redis 按顺序处理请求,包括读取命令、执行命令、返回结果等所有操作都在主线程中完成。
Redis 单线程模式的优势
- 避免复杂的线程同步问题
Redis 采用单线程设计的一个主要原因是避免了多线程中的复杂同步问题。在多线程环境下,多个线程对共享资源的并发访问需要使用锁机制来保护数据的完整性,但这也会带来性能开销。单线程的 Redis 不需要考虑加锁问题,因为所有的请求都是在同一个线程中按顺序执行,天然避免了竞争条件(race conditions)、死锁和其他并发问题。
- 高效的内存操作
Redis 是一个基于内存的数据库,大部分数据操作都是直接在内存中进行的。内存操作的速度非常快,通常在微秒级别。这使得即使 Redis 是单线程的,它在处理绝大多数简单的读写请求时也能够表现得非常高效。
- I/O 多路复用
Redis 使用 I/O 多路复用(如 epoll)来处理大量并发连接。I/O 多路复用允许一个线程同时监控多个客户端连接的输入输出,而不会阻塞在某个连接上。这意味着,虽然 Redis 是单线程,但它依然可以高效地处理成千上万的并发连接。
非阻塞 I/O:Redis 的网络操作是非阻塞的,这意味着即便某个连接上没有立即准备好数据,Redis 也不会停下来等待,而是继续处理其他连接上的请求。事件驱动模型:Redis 使用事件驱动模型来处理网络 I/O,即当某个连接上有新的事件(如数据可读、可写等)时,Redis 会立即响应并处理这个事件。这样,Redis 不需要为每个连接分配单独的线程,而是通过事件驱动的方式让单线程高效运作。
Redis 单线程的局限性
虽然 Redis 单线程模式在大多数场景下表现良好,但也存在一些局限性,特别是在特定条件下可能成为瓶颈:
- CPU 密集型任务
虽然 Redis 的大部分操作都是非常轻量的,但在某些 CPU 密集型任务中,如处理非常大的集合或执行复杂的 Lua 脚本,单线程的性能可能会不足。当 Redis 需要在主线程中执行耗时较长的计算时,其他客户端的请求可能会因此被阻塞。
- 大数据量的磁盘 I/O
当 Redis 执行持久化操作时(如将数据写入 AOF 文件或 RDB 文件),写入磁盘是一个相对慢的操作。虽然 Redis 通过异步机制和后台线程来优化某些持久化任务(如 AOF 重写和 RDB 文件生成),但在 6.0 之前版本中,网络 I/O 和命令处理仍然完全依赖主线程,因此在高并发场景下,磁盘 I/O 可能成为性能瓶颈。
- 处理大型数据集的阻塞问题
对于像删除大量数据(如一个包含数百万个元素的哈希或列表)或从主线程直接进行内存清理这样的操作,单线程可能会导致 Redis 短暂阻塞,影响其他请求的处理。为此,Redis 提供了 UNLINK 等命令来将大型对象的删除任务交由后台线程异步处理,减少主线程的负载。
Redis 单线程的优化策略
为了解决单线程的性能瓶颈,Redis 在 6.0 之前引入了一些优化策略:
异步删除:通过命令 UNLINK 和 FLUSHALL/FLUSHDB ASYNC,Redis 能够将大数据块的删除操作交由后台线程异步执行,从而避免删除大对象时主线程的阻塞。Lazy-Free 机制:Redis 提供了“惰性删除”机制,允许用户在删除大对象时,分步释放对象占用的内存,而不是一次性完成,从而减小对主线程的影响。后台持久化:对于 RDB 生成和 AOF 重写操作,Redis 通过后台子进程完成。这些操作涉及到磁盘 I/O,通常比较耗时,但通过子进程执行,避免了主线程的阻塞。
总结:
Redis 6.0 版本之前的单线程模式基于 I/O 多路复用和事件驱动模型,能够高效处理大量并发请求。它的优点在于实现简单、避免复杂的并发问题,同时依赖内存操作和非阻塞 I/O,保证了大多数场景下的高性能表现。
然而,在 CPU 密集型任务、大数据集操作和磁盘 I/O 瓶颈等场景下,单线程模式可能会遇到一些性能瓶颈。因此,在 Redis 6.0版本中,引入了多线程来优化网络 I/O 的处理能力,从而进一步提升 Redis 在高并发场景下的性能表现。
3. Redis采用单线程为什么还这么快
官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:
Redis 采用单线程(处理网络 I/O 和执行命令)却依然能够保持极高的性能,主要有以下几个原因:
- 基于内存的操作
Redis 是一个 内存数据库,所有数据操作都直接在内存中完成。相比传统的磁盘数据库,内存读写速度极快,通常在微秒级别。这使得 Redis 能够非常迅速地处理请求,减少了 I/O 等待时间。
- I/O 多路复用机制
Redis 使用 I/O 多路复用(如 epoll、kqueue 或 select)来同时处理多个客户端连接。尽管 Redis 是单线程的,但它能够高效地处理大量并发连接,因为它不需要为每个连接创建单独的线程或进程,而是通过事件驱动的方式监听和处理多个连接上的请求。多路复用允许 Redis 处理大量并发请求的能力远超一般的单线程程序。
- 非阻塞 I/O
Redis 采用 非阻塞 I/O 模式,主线程不会因为某个 I/O 操作(如等待客户端请求数据或写入响应)而被卡住。即使某个客户端数据没有完全到达,Redis 也不会阻塞,而是先处理其他已经准备好的请求,保证主线程的连续执行和高效处理。
- 单线程避免了锁的开销
多线程编程通常需要处理线程之间的同步问题,比如加锁、解锁等操作,这些都会引入性能开销。Redis 的单线程模型完全避免了这些复杂性,因为所有的操作都是按顺序执行的,不需要使用锁来保证数据一致性。因此,Redis 能够避免常见的多线程编程中的资源竞争、锁定和上下文切换带来的性能损失。
- 简单高效的事件驱动模型
Redis 的事件驱动模型设计简洁,主线程通过事件循环不断监听并处理新的客户端连接、请求和响应。这种模型非常轻量化,减少了上下文切换和系统调用的开销,使得 Redis 在处理大量连接时依然保持高效。
- 高效的数据结构
Redis 的数据结构设计非常高效,它在内存中使用了优化的编码方式(如 ziplist、intset 等)来存储数据。这些数据结构专为快速访问和操作而设计,使得 Redis 可以在单线程模式下快速执行各种数据操作。
4. Redis 6.0 之前为什么使用单线程?
虽然说 Redis 是单线程模型,但实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。不过,Redis 4.0增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。
Redis官方的F&Q
使用单线程的原因:
- 单线程编程容易并且更容易维护;
- Redis的性能瓶颈不在 CPU,主要在内存和网络IO;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
5. Redis 6.0 之后为何引入多线程?
虽然 Redis 的主要工作(网络 I/O和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用多个IO线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上
。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以不要误解 Redis 有多线程同时执行命令。
Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。
//读请求也使用io多线程
io-threads-do-reads yes
同时, Redis.conf 配置文件中提供了IO 多线程个数的配置项。
// io-threads ,表示启用 N-1 个 I/0 多线程(主线程也算一个 I/0 线程)
io-threads 4
关于线程数的设置,官方的建议是如果为4核的 CPU,建议线程数设置为2 或 3,如果为8核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建6个线程(这里的线程数不包括主线程)
。
- Redis-server:Redis的主线程,主要负责执行命令;
- bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
- io_ thd_1、io_thd_2、io_thd_3:三个I/O 线程,io-threads 默认是4,所以会启动3(4-1)个I/O多线程,用来分担 Redis 网络I/O 的压力。