深入浅出Redis(二):Redis单线程模型与通信流程

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入浅出Redis(二):Redis单线程模型与通信流程

引言

Redis是一款基于键值对的数据结构存储系统,它的特点是基于内存操作、单线程处理命令、IO多路复用模型处理网络请求、键值对存储与简单丰富的数据结构等等

本篇文章主要围绕Redis中IO多路复用模型处理网络请求的特点来先从介绍IO模型,IO多路复用模型以及客户端与服务端的通信

IO模型

  • IO请求(读)数据会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:
  1. 等待数据:调用后需要等待数据准备好
  2. 复制数据:当准备好数据后,将数据从内核空间复制到用户空间

常见IO模型

  • 同步阻塞IO:发出IO请求(系统调用)后,阻塞等待内核准备数据,数据准备好了再把数据从内核空间拷贝到用户空间 image.png
  • 一个线程处理一个客户端,同时处理大量网络请求时需要的线程太多 ,且线程IO请求时阻塞
  • 同步非阻塞IO:线程轮循发起IO请求,如果没准备好数据返回告知数据未准备好,这样就会下次再轮循访问,如果数据准备好了就能够将数据从内核空间复制到用户空间

image.png

  • 一个线程处理一个客户端,同时处理大量网络请求时需要的线程太多,虽然线程IO请求时不阻塞,但是轮循发起IO请求会浪费CPU(CPU空转)
  • IO多路复用:使用选择器(select)阻塞等待事件,当监听accept事件说明要建立连接(与对应客户端建立套接字连接才能进行读写事件),一次监听可能携带多个事件需要处理image-20221129073737746
  • 一个线程监听多个客户端,轮循select阻塞,监听到套接字触发读/写事件时再进行处理(循环处理可能有多个客户端同时触发读写事件)

没看懂IO多路复用模型的同学可以继续往下看,下文会详细介绍IO多路复用模型的流程

通信

通信流程主要划分为:服务端要进行初始化,初始化后才开始循环处理事件,服务端在处理事件期间会维护客户端相关信息

服务端初始化

初始化

  1. 初始化服务端默认配置
  2. 根据启动命令更改配置
  3. 初始化数据结构
  4. 根据AOF或RDB恢复数据(根据持久化策略恢复数据,后续持久化文章会详细介绍)
  5. 开始事件循环(处理事件)

处理事件

处理事件可以看成处理客户端请求与维护管理服务端自身的资源

事件被分为文件事件和时间时间

文件事件常是处理客户端请求,时间事件常是定时、周期任务来检查/管理服务端资源

文件事件

Redis 使用IO多路复用模型 监听多个客户端的套接字,当感知到套接字上发生事件时,将事件放入队列中,由文件事件分派器依次取出事件并交给对应事件处理器处理

image.png

事件类型可以分为读事件AE_READABLE、写事件AE_WRITEABLE,读写是以服务器为中心(起始)的,比如客户端发起连接请求、发送命令请求都是触发读事件,而客户端需要读响应时是触发写事件

事件处理器有连接应答处理器(处理连接的读事件),命令请求处理器(处理读事件),命令回复处理器(处理写事件),复制处理器(用于主从复制) 等等,本文主要使用连接应答、命令请求、回复三种处理器

  • 流程
  1. 服务端初始化时,连接应答处理器与服务端监听套接字的读事件关联
  2. 客户端请求连接时,服务端套接字触发读事件,服务端监听到读事件并放入队列中,事件分派器取出后交给连接应答处理器处理,并将客户端套接字的读事件与命令请求处理器关联
  3. 客户端发送命令请求时,客户端套接字触发读事件,服务端监听到读事件并放入队列,事件分派器交给命令请求处理器处理,执行命令,准备回复,将客户端套接字的写事件与命令回复处理器关联
  4. 客户端准备读回复时,客户端套接字触发写事件,服务端监听到写事件并放入队列,事件分派器交给命令回复处理器处理,返回响应,取消命令回复处理器与客户端套接字写事件的关联

image-20221117172239493

时间事件

时间事件分为定时时间事件和周期时间事件,定时为规定事件做一次,周期为以多少时间为周期做一次

时间事件处理器使用链表管理定时、周期事件,定期遍历链表,判断时间事件是否到期,到期则执行,执行完判断时间事件如果为定时则删除,为周期则更改下个周期到达时间

时间事件较少,基本上都是做一些定期检查,主要处理文件事件

服务器优先处理文件事件再处理时间事件

客户端信息

服务端使用RedisClient对象来存储客户端相关信息,使用链表管理RedisClient(所有连接的客户端)

  • redis client 信息
  • 套接字描述符,判断客户端是否为伪客户端
  • aof伪客户端:aof客户端执行aof文件,执行完关闭
  • lua脚本伪客户端:执行lua脚本,整个生命周期都存在
  • 客户端名字、客户端标志(主从,状态等)、是否身份验证
  • 输入缓冲区:保存序列化的命令请求
  • 命令argv 与 参数个数 args :解析序列化命令请求 得到命令与参数个数
  • 命令相关信息cmd : 根据argv 查询字典 得到命令相关的信息rediscommand
  • 输出缓冲区:保存回复响应,如果短字符串使用固定缓冲区(字节数组),如果长字符串使用动态缓冲区(链表+字符串)
  • 时间:记录连接时间等信息

通信流程

  • 服务端处理请求流程
  1. 用户发送命令到客户端,客户端序列化后发送给服务端 (客户端与服务端建立连接时,连接应答处理器处理,让客户端套接字读事件关联到命令请求处理器)
  2. 服务端读取命令请求(监听到读事件发生,最终由命令请求处理器处理)
  • 服务端接收序列化请求,解析出命令和参数个数填充属性argv、args参数
  • 通过命令argv与字典查询该命令相关信息 cmd指向该rediscommand
  1. 服务端执行命令(执行完放到缓冲区,让客户端套接字写事件关联到命令回复处理器)
  • 执行前检查参数个数、身份验证等操作
  • 执行并将回复保存在输出缓冲区
  • 执行后检查慢查询、写AOF缓冲等操作
  1. 服务端回复响应给客户端,客户端反序列化展示给用户(客户端准备读取触发写事件,命令回复处理器处理响应回去,取消关联)

定时任务通常用来管理服务器资源:更新缓冲时间、每秒执行命令数量、已使用内存峰值,处理sigterm信号关闭前RDB,管理客户端连接、数据库资源,判断是否需要持久化等

总结

本文以Redis使用IO多路复用模型处理网络请求的为起点,介绍了IO模型,服务端初始化,服务端处理文件、时间事件,客户端信息以及完整的通信流程

同步阻塞IO模型,在处理大量网络请求时需要耗费一比一的线程,且发生系统调用读数据时线程会阻塞

同步非阻塞IO模型,虽然不阻塞但存在CPU空转,浪费性能

IO多路复用模型使用select监听套接字上的读写事件,select会阻塞,当监听到客户端套接字触发读写事件时,遍历处理所有套接字的读写事件

服务端初始化时主要是根据配置文件以及启动命令进行资源、数据结构的初始化,同时会根据持久化策略寻找RDB、AOF文件进行数据恢复,初始化完才开始循环处理事件

事件可以分为文件事件和时间事件,文件事件常用来处理客户端请求,分为读、写事件,当客户端套接字触发读、写事件时,将事件放入队列,文件事件分派器将队列中的事件依次交给对应的事件处理器;时间事件常是定时、周期任务,用来检查/管理服务端自身资源等

服务端处理事件期间,会使用链表管理维护客户端相关信息:输入缓冲区(序列化的命令请求)、命令与命令参数个数、命令相关信息(通过这些能够执行命令)、输出缓冲区(保存回复响应)

整体流程:

  1. 服务端根据配置文件、启动命令初始化数据结构,将连接应答处理器与服务端监听套接字的读事件关联
  2. 客户端发起请求建立连接时,服务端监听套接字读事件触发,连接应答处理器将客户端套接字读事件与命令请求处理器关联
  3. 当客户端发送到服务端时,触发读事件,由命令请求处理器处理
  • 解析输入缓冲区的序列化请求,解析完保存完善客户端信息(命令相关信息)
  • 执行前检查参数个数、身份验证等
  • 根据客户端保存命令相关信息执行函数
  • 执行后还可能需要检查一些操作(如:检查慢查询、是否要写AOF缓冲区等),执行后将结果保存在输出缓冲区,让客户端套接字写事件关联命令回复处理器
  1. 当客户端准备读时触发写事件,命令回复处理器将输出缓冲区响应返回

最后

  • 参考资料
  • 《Redis深度历险》
  • 《Redis设计与实现》
  • 部分图片可能来源网络


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
监控 NoSQL 安全
如何在 Redis 中正确使用多线程?
【10月更文挑战第16天】正确使用 Redis 多线程需要综合考虑多个因素,并且需要在实践中不断摸索和总结经验。通过合理的配置和运用,多线程可以为 Redis 带来性能上的提升,同时也要注意避免可能出现的问题,以保障系统的稳定和可靠运行。
52 2
|
2月前
|
存储 NoSQL Redis
Redis 新版本引入多线程的利弊分析
【10月更文挑战第16天】Redis 新版本引入多线程是一个具有挑战性和机遇的改变。虽然多线程带来了一些潜在的问题和挑战,但也为 Redis 提供了进一步提升性能和扩展能力的可能性。在实际应用中,我们需要根据具体的需求和场景,综合评估多线程的利弊,谨慎地选择和使用 Redis 的新版本。同时,Redis 开发者也需要不断努力,优化和完善多线程机制,以提供更加稳定、高效和可靠的 Redis 服务。
53 1
|
13天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
43 12
|
2月前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
41 1
[Java]线程生命周期与线程通信
|
1月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
37 3
|
2月前
|
并行计算 JavaScript 前端开发
单线程模型
【10月更文挑战第15天】
|
1月前
|
存储 SQL NoSQL
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
22 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
42 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
28 1