1.Redis介绍
Redis 是C语言开发的一个开源高性能键值对的内存数据库,也是一种NoSQL(not-only sql,非关系型数据库)的数据库,可以用来做数据库、缓存、消息中间件等场景。在目前的技术选型中,Redis常常被用来作为数据缓存和分布式锁的解决方案。
Redis具有以下几个特点,分别是高性能,多种数据结构,支持持久化,分布式等等:
1)高性能:Redis是基于内存的数据存储系统,可以实现高速读写操作,适合用作数据缓存和高速读写的存储系统。
2)多种数据结构:Redis支持多种数据结构,包括字符串、列表、哈希、集合和有序集合,可以满足不同的应用场景需求。Redis也更新了三种新型数据类型,包括bitmaps,HyperLogLog和GeoSpatial。
3)支持持久化:Redis支持数据持久化,可以将数据存储到硬盘上,避免因为程序重启导致数据丢失的情况发生。
4)分布式:Redis支持分布式架构,可以通过分片和复制等方式实现高可用和横向扩展。
5)丰富的功能:Redis提供了丰富的功能,包括事务、发布订阅、Lua脚本、数据过期等。
基于Redis的这些特点,它常常被用作以下几个场景,分别是数据缓存,分布式锁,计数器和排行榜:
1)数据缓存:将常用的数据存储在Redis中,提高系统读取速度,降低数据库的访问压力。
2)分布式锁:利用Redis的分布式特性,实现分布式锁,避免多个实例同时操作同一个资源的问题。
3)计数器:利用Redis的原子操作,实现分布式计数器的功能。
4)实时排行榜:将实时的数据存储在Redis中,可以实现排行榜的实时更新和查询。
2.Redis为什么这么快
Redis是基于内存运行的高性能 K-V 数据库,官方提供的测试报告是单机可以支持约10w/s的QPS。
1)C 语言实现。C语言可以更好地对内存地址进行操作;虽然 C 对 Redis 的性能有助力,但语言并不是最核心因素。
2)纯内存 I/O。相较于其他基于磁盘的数据库,Redis是纯内存操作,有着天然的性能优势。
3)I/O 多路复用。基于 epoll/select/kqueue 等 I/O 多路复用技术,实现高吞吐的网络 I/O。
4)单线程模型。虽然单线程无法利用多核的优势,但是从另一个层面来说则避免了多线程频繁上下文切换,以及同步机制如锁带来的开销,反而性能更加优秀。
这里我们强调的单线程,指的是网络请求模块使用一个线程来处理,即一个线程处理所有网络请求,其他模块仍用了多个线程。为什么Redis要保持单线程。主要原因有三点:避免过多的上下文切换开销,避免同步机制的开销和简单可维护。
1)避免过多的上下文切换开销
多线程调度过程中必然需要在 CPU 之间切换线程上下文 context,而上下文的切换又涉及程序计数器、堆栈指针和程序状态字等一系列的寄存器置换、程序堆栈重置甚至是 CPU 高速缓存、TLB 快表的汰换,如果是进程内的多线程切换还好一些,因为单一进程内多线程共享进程地址空间,因此线程上下文比之进程上下文要小得多,如果是跨进程调度,则需要切换掉整个进程地址空间。如果是单线程则可以规避进程内频繁的线程切换开销,因为程序始终运行在进程中单个线程内,没有多线程切换的场景。
2)避免同步机制的开销
如果 Redis 选择多线程模型,又因为 Redis 是一个数据库,那么势必涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁,而我们知道 Redis 不仅仅提供了简单的 key-value 数据结构,还有 list、set 和 hash 等等其他丰富的数据结构,而不同的数据结构对同步访问的加锁粒度又不尽相同,可能会导致在操作数据过程中带来很多加锁解锁的开销,增加程序复杂度的同时还会降低性能。
3)简单可维护
一方面这是Redis开发者有着近乎偏执的简洁性理念,在阅读Redis的源码或者给 Redis 提交 PR 的之时感受到这份偏执。因此代码的简单可维护性必然是 Redis 早期的核心准则之一,而引入多线程必然会导致代码的复杂度上升和可维护性下降。另外一方面,因为CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
Redis 从本质上来讲是一个网络服务器,而对于一个网络服务器来说,网络模型是它的核心内容。接下来将介绍Redis 网络模型的设计模式,介绍其从单线程进化到多线程的工作原理。
3.Reactor模式
Reactor 模式也叫做反应器设计模式,是一种为处理服务请求并发提交到一个或者多个服务处理器的事件设计模式。当请求抵达后,通过服务处理器Reactor将这些请求采用多路分离的方式分发给相应的请求处理器Handler。Reactor模式主要由服务处理器Reactor,连接器Acceptor和处理器 Handler 这三个核心部分组成。
3.1 Reactor模式读写流程
Reactor: 负责响应事件,将事件分发到绑定了对应事件的Handler,如果是连接事件,则分发到Acceptor。
Handler: 事件处理器,负责执行对应事件对应的业务逻辑。
Acceptor: 绑定了connect事件,当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。
3.2 Reactor模式分类
Reactor 模型中的 Reactor 可以是单个也可以是多个,Handler 同样可以是单线程也可以是多线程,所以组合的模式大致有如下三种:单 Reactor 单线程模型,单 Reactor 多线程模型和主从Reactor多线程模型。
1)单Reactor单线程模型
处理流程:
(1)Reactor 线程通过 select 监听事件,收到事件后通过 Dispatch 进行分发
(2)如果是连接建立事件,则将事件分发给 Acceptor,Acceptor 会通过 accept() 方法获取连接,并创建一个Handler 对象来处理后续的响应事件
(3)如果是IO读写事件,则 Reactor 会将该事件交由当前连接的 Handler 来处理
(4)Handler 会完成 read -> 业务处理 -> send 的完整业务流程
优缺点:
单 Reactor 单线程模型的优点在于将所有处理逻辑放在一个线程中实现,没有多线程、进程通信、竞争的问题。但该模型在性能与可靠性方面存在比较严重的问题:在处理读写请求时,其他请求只能等待,容易造成系统的性能瓶颈;一旦 Reactor 线程意外中断或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
所以该单Reactor单线程模型不适用于计算密集型的场景,只适用于业务处理非常快速的场景。
2)单Reactor多线程模型
处理流程:
(1)Reactor 线程通过 select 监听事件,收到事件后通过 Dispatch 进行分发
(2)如果是连接建立事件,则将事件分发给 Acceptor,Acceptor 会通过 accept() 方法获取连接,并创建一个Handler 对象来处理后续的响应事件
(3)如果是IO读写事件,则 Reactor 会将该事件交由当前连接对应的 Handler 来处理
(4)与单Reactor单线程不同的是,Handler 不再做具体业务处理,只负责接收和响应事件,通过 read 接收数据后,将数据发送给后面的 Worker 线程池进行业务处理。
(5)Worker 线程池再分配线程进行业务处理,完成后将响应结果发给 Handler 进行处理。
(6)Handler 收到响应结果后通过 send 将响应结果返回给 Client。
优缺点:
相对于第一种模型来说,在处理业务逻辑,也就是获取到 IO读写事件之后,交由线程池来处理,Handler 收到响应后通过send 将响应结果返回给客户端。这样可以降低 Reactor 的性能开销,从而更专注的做事件分发工作了,提升整个应用的吞吐,并且 Handler 使用了多线程模式,可以充分利用 CPU 的性能。但是这个模型存在的问题:单个 Reactor 承担所有事件的监听、分发和响应,对于高并发场景,容易造成性能瓶颈。
3)主从Reactor多线程模型
单Reactor多线程模型解决了 Handler 单线程的性能问题,但是 Reactor 还是单线程的,对于高并发场景还是会有性能瓶颈,所以需要将 Reactor 调整为多线程模式,也就是接下来要介绍的主从 Reactor 多线程模型。主从 Reactor 多线程模型将Reactor 分成两部分:
(1)MainReactor:只负责处理连接建立事件,通过 select 监听 server socket,将建立的 socketChannel 指定注册给subReactor,通常一个线程就可以了
(2)SubReactor:负责读写事件,维护自己的 selector,基于 MainReactor 注册的 SocketChannel 进行多路分离 IO 读写事件,读写网络数据,并将业务处理交由 worker 线程池来完成。SubReactor 的个数一般和 CPU 个数相同
主从 Reactor 多线程模型的优点在于主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理,同时主线程和子线程的交互也很简单,子线程接收主线程的连接后,只管业务处理即可,无须关注主线程,可以直接在子线程将处理结果发送给客户端。该 Reactor模型适用于高并发场景,并且 Netty 网络通信框架也是采用这种实现 rocketmq
4.单线程模型
redis网络IO模型底层使用IO多路复用,通过reactor模式实现的,在redis 6.0以前属于单reactor单线程模式。
Client:客户端通过 socket 与服务端建立网络通道然后发送请求命令,服务端执行请求的命令并回复。
IO多路复用:基于 epoll_wait/select/kevent 等系统调用的封装,监听等待读写事件触发
事件分发器:事件循环中的核心部分,对事件进行派发,是事件驱动得以运行的基础。
命令请求处理器:解析并执行客户端的请求命令。
命令回复处理器,当一次事件循环之后写出缓冲区中还有数据残留,则这个处理器会被注册绑定到相应的连接上,等连接触发写就绪事件时,它会将写出缓冲区剩余的数据回写到客户端。
命令应答处理器:底层使用系统调用 accept 接受来自客户端的新连接,并为新连接注册绑定命令读取处理器,以备后续处理新的客户端 TCP 连接;除了这个处理器,还有对应的 acceptUnixHandler 负责处理 Unix Domain Socket 以及 acceptTLSHandler 负责处理 TLS 加密连接。
复制处理器:在执行事件之前会复制数据,比如将持久化 AOF 缓冲区的数据到磁盘等。
redis io请求底层数据请求
5.多线程模型
Redis6.0 版本之后,Redis 正式在核心网络模型中引入了多线程,也就是所谓的 I/O threading,至此 Redis 真正拥有了多线程模型,这对应了主从Reactor多线程模型的Reactor设计模式。
这里大部分逻辑和之前的单线程模型是一致的,变动的地方是把读取客户端请求命令和回写响应数据的逻辑异步化了,交给 I/O 线程去完成,这里需要特别注意的一点是:I/O 线程仅仅是读取和解析客户端命令而不会真正去执行命令,客户端命令的执行最终还是要在主线程上完成。
单线程模型和多线程模型的比较与分析:
但是 Redis 多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers模型。Redis 的多线程方案中,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令。所有客户端命令最后还需要回到主线程去执行,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。