Redis
基础
Redis
是什么
Redis
是一个基于BSD开源的项目,是一个把结构化的数据放在内存中的一个存储系统。
你可以把它作为数据库,缓存和消息中间件来使用。同时支持strings
,lists
,hashes
,sets
,sorted sets
,bitmaps
,hyperloglogs
和geospatial indexes
等数据类型。
它还通过redis sentinel
实现高可用,通过redis cluster
实现了自动分片。以及事务,发布/订阅,自动故障转移等等。
为什么用Redis
而在后端开发的技术选型中,Redis
已经成为了一个不可绕过的解决方案工具。因此Redis
成为了后端开发的基本技能之一。当然,也是后端面试中必考的技术栈之一。
Redis
的优点,如果只用一个字来解释,那就是:快!
Redis
有多快?官方给出的答案是读写速度 10万/秒,如果说这是在单线程情况下跑出来的成绩,你会不会惊讶?为什么单线程的 Redis
速度这么快?
Redis
为什么快
主要有以下几点:
Redis
是单线程的吗
我们经常听到,Redis
是单线程的,这句话对吗?
基本上是对的,但是不准确。
而对于为什么使用单线程,官方有一句解释:
It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound.
因为 Redis
是基于内存的操作,CPU 不是 Redis
的瓶颈。Redis
的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
为什么说不准确呢?
我们需要回顾Redis
的两个最重要的版本更新:
Redis 4.0
为了防止耗时的命令阻塞线程,导致无法处理后续事件。引入了多线程来处理一些非阻塞命令。有:UNLINK
、FLUSHALL ASYNC
、FLUSHDB ASYNC
等。 但是整个网络模型依然是单线程的,所以我们称之为单线程。Redis 6.0
就真正的在网络模型上加入多线程IO
来解决网络IO的性能瓶颈。 此时IO读写是多线程的,执行命令依旧是单线程的。
Redis
网络模型
一张图看懂Redis
的单线程模型:
redis
的网络事件处理器是基于Reactor模式,又叫做文件事件处理器。
文件事件处理器
使用I/O多路复用
来同时监听多个套接字,并根据套接字执行的任务关联到不同的事件处理器。
文件事件以单线程方式运行,但通过使用I/O多路复用
程序来监听多个套接字,文件事件处理器
实现了高性能的网络通信模型。
Redis
在处理客户端的请求时,包括接收
(socket读)、解析
、执行
、发送
(socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的单线程
。
Reactor模型
Redis
的单线程网络模型,这就是一个经典的Reactor的模型,其本质上是I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O)
的模式。
是一种基于事件驱动模型的设计模式。
我们来看一下Reactor里面两种经典的模型。
单线程Reactor模型
Redis
的单线程模型就是使用的经典的单线程Reactor模型。
我们先看看单线程的Reactor模型
消息处理流程:
- Reactor对象通过
select/poll/epoll
等IO多路复用监控连接事件,收到事件后通过dispatcher
事件分发器进行转发。 - 如果是连接建立的事件,则由
acceptor
接受连接,并创建Handler
处理后续事件。 - 如果不是建立连接事件,则Reactor会分发调用
Handler
来响应。 - Handler会完成
read->解析->执行->send
的完整业务流程。
优点:
- 单线程运行,串行操作,不需要加锁,逻辑简单。
缺点:
- 仅用一个线程处理请求,对于多核资源机器来说是有点浪费的。
- 当处理读写任务的线程负载比较重,将会阻塞后续的事件处理,导致整体延迟变大。
应用:
Redis
网络模型。(6.0版本以前)
Master-Worker Reactor模型
比起单线程模型,它是将Reactor分成两部分:
区别于单线程Reactor模式
,这种模式不再是单线程的事件循环,而是有多个线程subReactors
各自维护一个独立的事件循环,由 mainReactor
负责接收新连接并分发给 subReactors
去独立处理,最后 subReactors
回写响应给客户端。
优点:
- 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
- 可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;
缺点:
- 如果多个线程可能操作同一份数据,就涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁。增加了代码复杂度,同时增加了同步机制的开销。
应用:Nginx, Netty, Swoole, Memcached
就是使用的这个模型
Redis 6.0
的多线程网络模型
为什么Redis 6.0
要使用多线程
之前说了,CPU不是Redis
的瓶颈,Redis
的瓶颈最有可能是机器内存大小和网络带宽。 从Redis
自身角度来说,因为读写网络的read/write
系统调用占用了Redis
执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 所以选择多线程IO来实现读写。主线程来执行Redis
命令。
将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis
命令还是主线程串行执行。
Redis 6.0
网络模型
- 前面提到
Redis
最初选择单线程网络模型的理由是:CPU 通常不会成为性能瓶颈,瓶颈往往是内存和网络,因此单线程足够了。那么为什么现在Redis
又要引入多线程呢?很简单,就是Redis
的网络 I/O 瓶颈
已经越来越明显了。所以这个多线程是为了解决IO的瓶颈
的。 - 如果多线程包括了
IO读写,解析和执行
的整个过程,那么多线程需要面临线程安全的问题,Redis 6.0
版本之前是没有考虑线程安全的,如果使用多线程来处理命令的执行,需要大量的改动来保证多线程的安全机制,实现更复杂。为了避免了不必要的上下文切换和竞争条件,多线程导致的切换而消耗 CPU,也不用考虑各种锁的问题,就让执行这一步只使用主线程。
Redis 6.0
和Memcached
多线程模型对比
相同点:
- 都采用了 Master-Worker 的线程的模型
不同点:
Memcached
执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,通过各种锁机制来保证数据的线程安全。- 而
Redis
把执行逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了数据的线程安全问题。
总结
让我们来回顾一下 Redis
多线程网络模型的设计方案:
- 使用 I/O 线程实现网络 I/O 多线程化,I/O 线程只负责网络 I/O 和命令解析,不执行具体的命令。
Redis
的多线程网络模型实际上并不是一个标准的 Master-Worker Reactor
模型,Redis
的多线程方案中,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令。
所有客户端命令最后还需要回到主线程去执行
,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。
Redis
目前的多线程方案更像是一个折中的选择:既保持了原系统的兼容性,又能利用多核提升 I/O 性能,来解决网络IO的性能瓶颈。