西安手机网站制作公司,北京北排建设公司招标网站,用page打开wordpress,石家庄网站建设模板一、redis 的线程模型
redis 内部使用文件事件处理器 file event handler#xff0c;它是单线程的#xff0c;所以redis才叫做单线程模型。它采用IO多路复用机制同时监听多个 socket#xff0c;将产生事件的 socket 压入内存队列中#xff0c;事件分派器根据 socket 上的事…一、redis 的线程模型
redis 内部使用文件事件处理器 file event handler它是单线程的所以redis才叫做单线程模型。它采用IO多路复用机制同时监听多个 socket将产生事件的 socket 压入内存队列中事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
1.1 文件事件处理器的结构
多个 socketIO 多路复用程序文件事件分派器事件处理器连接应答处理器、命令请求处理器、命令回复处理器
1.2 线程模型
多个 socket 可能会并发产生不同的操作每个操作对应不同的文件事件但是 IO多路复用程序会监听多个 socket会将产生事件的 socket 放入队列中排队事件分派器每次从队列中取出一个 socket根据 socket 的事件类型交给对应的事件处理器进行处理。
1.3 使用单线程网络模型的好处
避免过多的上下文切换开销
多线程调度过程中必然需要在 CPU 之间切换线程上下文 context而上下文的切换是有开销的。单线程则可以规避进程内频繁的线程切换开销因为程序始终运行在进程中单个线程内没有多线程切换的场景。
避免同步机制的开销
如果 Redis 选择多线程模型势必涉及到底层数据同步的问题必然会引入某些同步机制比如锁而我们知道 Redis 不仅仅提供了简单的 key-value 数据结构还有 list、set 和 hash 等等其他丰富的数据结构而不同的数据结构对同步访问的加锁粒度又不尽相同可能会导致在操作数据过程中带来很多加锁解锁的开销增加程序复杂度的同时还会降低性能。
简单可维护
Redis 的作者 Salvatore Sanfilippo (别称 antirez) 对 Redis 的设计和代码有着近乎偏执的简洁性理念。因此代码的简单可维护性必然是 Redis 早期的核心准则之一而引入多线程必然会导致代码的复杂度上升和可维护性下降。前面我们提到引入多线程必须的同步机制如果 Redis 使用多线程模式那么所有的底层数据结构都必须实现成线程安全的这无疑又使得 Redis 的实现变得更加复杂。总而言之Redis 选择单线程可以说是多方博弈之后的一种权衡在保证足够的性能表现之下使用单线程保持代码的简单和可维护性。
在 v6.0 版本之前Redis 的核心网络模型一直是一个典型的单 Reactor 模型利用 epoll/select/kqueue 等多路复用技术在单线程的事件循环中不断去处理事件客户端请求最后回写响应数据到客户端 aeEventLoop这是 Redis 自己实现的一个高性能事件库里面封装了适配各个系统的 I/O多路复用(I/O multiplexing)EventLoop除了 处理socket 的读写事件外还要处理一些定时任务。aeEventLoop本质是一个线程服务启动时就一直循环调用 aeProcessEvent 处理文件网络或者时间事件等价于Java中NIO的select线程client 代表一个客户端连接。Redis 是典型的 CS 架构Client — Server客户端通过 socket 与服务端建立网络通道然后发送请求命令服务端执行请求的命令并回复。Redis 使用结构体 client 存储客户端的所有相关信息包括但不限于封装的套接字连接 – conn当前选择的数据库指针 –db读入缓冲区 – querybuf写出缓冲区 – buf写出数据链表 – reply等acceptTcpHandler角色 Acceptor 的实现当有新的客户端连接时会调用这个方法它会调用系统 accept 创建一个 socket 对象同时创建 client 对象并将 socket 添加到 EventLoop 的监听列表中等价于NIO中注册socket到select上并注册当对应的读事件发生时的回调函数 readQueryFromClient即绑定 Handler这样当该客户端发起请求时就会调用对应的回调函数处理请求readQueryFromClient角色 Handler 的实现主要负责解析并执行客户端的命令请求并将结果写到对应的 client-buf 或者 client-reply 中beforeSleep事件循环之前的操作主要执行一些常规任务比如将 client 中的数据写会给客户端、进行一些持久化任务AOF 或者RDB操作主从同步等。
下面我们描绘一下 客户端client 与 Redis server 建立连接、发起请求到接收到返回的整个过程
首先Redis 服务器启动开启主线程事件循环 aeMain注册 acceptTcpHandler 连接应答处理器到用户配置的监听端口对应的文件描述符等待新连接到来客户端和服务端建立网络连接acceptTcpHandler 被调用主线程将 readQueryFromClient 命令读取处理器绑定到新连接对应的文件描述符上作为对应事件发生时的回调函数并初始化一个 client 绑定这个客户端连接客户端发送请求命令触发读就绪事件主线程调用 readQueryFromClient 通过 socket 读取客户端发送过来的命令存入 client-querybuf 读入缓冲区接着调用 processInputBuffer在其中使用 processInlineBuffer 或者 processMultibulkBuffer 根据 Redis 协议解析命令最后调用 processCommand 执行命令根据请求命令的类型SET, GET, DEL, EXEC 等分配相应的命令执行器去执行最后调用 addReply 函数族的一系列函数将响应数据写入到对应 client 的写出缓冲区client-buf 或者 client-reply client-buf 是首选的写出缓冲区固定大小 16KB一般来说可以缓冲足够多的响应数据但是如果客户端在时间窗口内需要响应的数据非常大那么则会自动切换到 client-reply链表上去使用链表理论上能够保存无限大的数据受限于机器的物理内存最后把 client 添加进一个 LIFO 队列 clients_pending_write在事件循环 aeMain 中主线程执行 beforeSleep -- handleClientsWithPendingWrites遍历 clients_pending_write 队列调用 writeToClient 把 client 的写出缓冲区里的数据回写到客户端如果写出缓冲区还有数据遗留则注册 sendReplyToClient 命令回复处理器到该连接的写就绪事件等待客户端可写时在事件循环中再继续回写残余的响应数据。
二、一次客户端与redis的完整通信过程图解
2.1 建立连接
首先redis 服务端进程初始化的时候会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。客户端 socket01 向 redis 进程的 server socket 请求建立连接此时 server socket 会产生一个 AE_READABLE 事件IO 多路复用程序监听到 server socket 产生的事件后将该 socket 压入队列中。文件事件分派器从队列中获取 socket交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
2.2 执行一个set请求
客户端发送了一个 set key value 请求此时 redis 中的 socket01 会产生 AE_READABLE 事件IO 多路复用程序将 socket01 压入队列此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。如果此时客户端准备好接收返回结果了那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件同样压入队列中事件分派器找到相关联的命令回复处理器由命令回复处理器对 socket01 输入本次操作的一个结果比如 ok之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
三、redis为什么效率这么高
纯内存操作。核心是基于非阻塞的 IO 多路复用机制。C 语言实现语言更接近操作系统执行速度相对会更快。单线程反而避免了多线程的频繁上下文切换问题预防了多线程可能产生的竞争问题。
参考中华石衫老师的亿级流量电商详情页缓存架构设计教程