我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。
但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
为什么使用单线程?
多线程并发开销大,访问共享资源时,要确保资源的正确性,需要额外的机制保证正确性,额外的操作增加了系统开销。
Redis 的 v6.0 版本正式引入多线程。
client:客户端对象,Redis 是典型的 CS 架构(Client <—> Server),客户端通过 socket 与服务端建立网络通道然后发送请求命令,服务端执行请求的命令并回复。Redis 使用结构体 client 存储客户端的所有相关信息,包括但不限于封装的套接字连接 – *conn,当前选择的数据库指针 – *db,读入缓冲区 – querybuf,写出缓冲区 – buf,写出数据链表 – reply等。
aeApiPoll:I/O 多路复用 API,是基于 epoll_wait/select/kevent 等系统调用的封装,监听等待读写事件触发,然后处理,它是事件循环(Event Loop)中的核心函数,是事件驱动得以运行的基础。
acceptTcpHandler:连接应答处理器,底层使用系统调用 accept 接受来自客户端的新连接,并为新连接注册绑定命令读取处理器,以备后续处理新的客户端 TCP 连接;除了这个处理器,还有对应的 acceptUnixHandler 负责处理 Unix Domain Socket 以及 acceptTLSHandler 负责处理 TLS 加密连接。
readQueryFromClient:命令读取处理器,解析并执行客户端的请求命令。
beforeSleep:事件循环中进入 aeApiPoll 等待事件到来之前会执行的函数,其中包含一些日常的任务,比如把 client->buf 或者 client->reply (后面会解释为什么这里需要两个缓冲区)中的响应写回到客户端,持久化 AOF 缓冲区的数据到磁盘等,相对应的还有一个 afterSleep 函数,在 aeApiPoll 之后执行。
sendReplyToClient:命令回复处理器,当一次事件循环之后写出缓冲区中还有数据残留,则这个处理器会被注册绑定到相应的连接上,等连接触发写就绪事件时,它会将写出缓冲区剩余的数据回写到客户端。
单线程有多快?
根据官方的 benchmark,通常来说,在一台普通硬件配置的 Linux 机器上跑单个 Redis 实例,处理简单命令(时间复杂度 O(N) 或者 O(log(N))),QPS 可以达到 8w+,而如果使用 pipeline 批处理功能,则 QPS 至高能达到 100w。
单线程为什么快呢?
1.Redis基于内存存储和操作数据;
2.Redis采用高效的数据结构存储数据,例如哈希表、跳表等;
3.Redis使用多路复用机制,网络 I/O 操作中能并发处理大量的客户端请求,实现高吞吐率。
4.单线程模型,单线程无法利用多核,但是从另一个层面来说则避免了多线程频繁上下文切换,以及同步机制如锁带来的开销。
异步多线程
Redis单线程负责处理网络I/O和键值对操作,那么如果操作的的big key、主从同步、持久化、key过期、满容驱逐等如何处理呢?
答案是:异步多线程。
Redis 在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,用于处理非常耗时的操作,避免线程阻塞。
另外,随着业务的发展,QPS和网络流量越来越大,Redis 的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,Redis 的网络 I/O 瓶颈已经越来越明显。
解决方法:优化网络 I/O 模块,分为两个方向:
1.零拷贝技术或者 DPDK 技术
2.利用CPU多核优势
DPDK是INTEL公司开发的一款高性能的网络驱动组件,旨在为数据面应用程序提供一个简单方便的,完整的,快速的数据包处理解决方案,主要技术有用户态、轮询取代中断、零拷贝、网卡RSS、访存DirectIO等。
零拷贝技术有其局限性,无法完全适配 Redis 这一类复杂的网络 I/O 场景。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。
利用CPU多核优势成为首先,在Redis 6.0中实现。