Redis请求处理的流程分析

目录
  • 概述#
  • 调用流程#
  • 命令执行过程 & 回写客户端#
    • 命令执行#
    • 数据回写客户端#
  • 总结#
  • Reference

本文使用的Redis 5.0源码

感觉这部分的代码还是挺有意思的,我尽量用比较通俗的方式进行讲解

概述#

我记得我在 一文说透 Go 语言 HTTP 标准库 这篇文章里面解析了对于 Go 来说是如何创建一个 Server 端程序的:

  • 首先是注册处理器;
  • 开启循环监听端口,每监听到一个连接就会创建一个 Goroutine;
  • 然后就是 Goroutine 里面会循环的等待接收请求数据,然后根据请求的地址去处理器路由表中匹配对应的处理器,然后将请求交给处理器处理;

用代码表示就是这样:

func (srv *Server) Serve(l net.Listener) error {
    ...
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        // 接收 listener 过来的网络连接
        rw, err := l.Accept()
        ...
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew)
        // 创建协程处理连接
        go c.serve(connCtx)
    }
}

对于 Redis 来说就有些不太一样,因为它是单线程的,无法使用多线程处理连接,所以 Redis 选择使用基于 Reactor 模式的事件驱动程序来实现事件的并发处理。

在 Redis 中所谓 Reactor 模式就是通过 epoll 来监听多个 fd,每当这些 fd 有响应的时候会以事件的形式通知 epoll 进行回调,每一个事件都有一个对应的事件处理器。

如: accept 对应 acceptTCPHandler 事件处理器、read & write 对应readQueryFromClient 事件处理器等,然后通过事件的循环派发的形式将事件分配给事件处理器进行处理。

所以说上面的这个 Reactor 模式都是通过 epoll 来实现的,对于 epoll 来说主要有这三个方法:

//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_create(int size);

/*
 * 可以理解为,增删改 fd 需要监听的事件
 * epfd 是 epoll_create() 创建的句柄。
 * op 表示 增删改
 * epoll_event 表示需要监听的事件,Redis 只用到了可读,可写,错误,挂断 四个状态
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

/*
 * 可以理解为查询符合条件的事件
 * epfd 是 epoll_create() 创建的句柄。
 * epoll_event 用来存放从内核得到事件的集合
 * maxevents 获取的最大事件数
 * timeout 等待超时时间
 */
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

所以我们可以根据这三个方法实现一个简单的 server:

// 创建监听
int listenfd = ::socket();

// 绑定ip和端口
int r = ::bind();
// 创建 epoll 实例
int epollfd = epoll_create(xxx);
// 添加epoll要监听的事件类型
int r = epoll_ctl(..., listenfd, ...);

struct epoll_event* alive_events =  static_cast<epoll_event*>(calloc(kMaxEvents, sizeof(epoll_event)));

while (true) {
    // 等待事件
    int num = epoll_wait(epollfd, alive_events, kMaxEvents, kEpollWaitTime);
	// 遍历事件,并进行事件处理
    for (int i = 0; i < num; ++i) {
        int fd = alive_events[i].data.fd;
        // 获取事件
        int events = alive_events[i].events;
		// 进行事件的分发
        if ( (events & EPOLLERR) || (events & EPOLLHUP) ) {
            ...
        } else  if (events & EPOLLRDHUP) {
            ...
        }
        ...
    }
}

调用流程#

所以根据上面的介绍,可以知道对于 Redis 来说一个事件循环无非也就这么几步:

  • 注册事件监听及回调函数;
  • 循环等待获取事件并处理;
  • 调用回调函数,处理数据逻辑;
  • 回写数据给 Client;

  • 注册 fd 到 epoll 中,并设置回调函数 acceptTcpHandler,如果有新连接那么会调用回调函数;
  • 启动一个死循环调用 epoll_wait 等待并持续处理事件,待会我们回到 aeMain 函数中循环调 aeProcessEvents 函数;
  • 当有网络事件过来的时候,会顺着回调函数 acceptTcpHandler 一路调用到 readQueryFromClient 进行数据的处理,readQueryFromClient 会解析 client 的数据,找到对应的 cmd 函数执行;
  • Redis 实例在收到客户端请求后,会在处理客户端命令后,将要返回的数据写入客户端输出缓冲区中而不是立马返回;
  • 然后在 aeMain 函数每次循环时都会调用 beforeSleep 函数将缓冲区中的数据写回客户端;

上面的整个事件循环的过程实际上代码步骤已经写的非常清晰,网上也有很多文章介绍,我就不多讲了。

命令执行过程 & 回写客户端#

命令执行#

下面我们讲点网上很多文章都没提及的,看看 Redis 是如何执行命令,然后存入缓存,以及将数据从缓存写回 Client 这个过程。

在前一节我们也提到了,如果有网络事件过来的时候会调用到 readQueryFromClient 函数,它是真正执行命令的地方。我们也就顺着这个方法一直往下看:

  • readQueryFromClient 里面会调用 processInputBufferAndReplicate 函数处理请求的命令;
  • 在 processInputBufferAndReplicate 函数里面会调用 processInputBuffer 以及判断一下如果是集群模式的话,是否需要将命令复制给其他节点;
  • processInputBuffer 函数里面会循环处理请求的命令,并根据请求的协议调用 processInlineBuffer 函数,将 redisObject 对象后调用 processCommand 执行命令;
  • processCommand 在执行命令的时候会通过 lookupCommand 去 server.commands 表中根据命令查找对应的执行函数,然后经过一系列的校验之后,调用相应的函数执行命令,调用 addReply 将要返回的数据写入客户端输出缓冲区;

server.commands会在 populateCommandTable 函数中将所有的 Redis 命令注册进去,作为一个根据命令名获取命令函数的表。

比如说,要执行 get 命令,那么会调用到 getCommand 函数:

void getCommand(client *c) {
    getGenericCommand(c);
}

int getGenericCommand(client *c) {
    robj *o;
	// 查找数据
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return C_OK;
    ...
}

robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
    //到db中查找数据
    robj *o = lookupKeyRead(c->db, key);
    // 写入到缓存中
    if (!o) addReply(c,reply);
    return o;
}

在 getCommand 函数中查找到数据,然后调用 addReply 将要返回的数据写入客户端输出缓冲区。

数据回写客户端#

在上面执行完命令写入到缓冲区后,还需要从缓冲区取出数据返回给 Client。对于数据回写客户端这个流程来说,其实也是在服务端的事件循环中完成的。

  • 首先 Redis 会在 main 函数中调用 aeSetBeforeSleepProc 函数将回写包的函数 beforeSleep 注册到 eventLoop 中去;
  • 然后 Redis 在调用 aeMain 函数进行事件循环的时候都会判断一下 beforesleep 有没有被设值,如果有,那么就会进行调用;
  • beforesleep 函数里面会调用到 handleClientsWithPendingWrites 函数,它会调用 writeToClient 将数据从缓冲区中回写给客户端;

总结#

这篇文章介绍了整个 Redis 的请求处理模型到底是怎样的。从注册监听 fd 事件到执行命令,到最后将数据回写给客户端都做了个大概的分析。当然这篇文章也和我以往的文章有点不同,没有长篇大论的贴代码,主要我觉得也没啥必要,感兴趣可以顺着流程图去看看代码。

Reference

http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf

https://time.geekbang.org/column/article/408491

http://remcarpediem.net/article/1aa2da89/

https://github.com/Junnplus/blog/issues/37

https://www.cnblogs.com/neooelric/p/9629948.html

到此这篇关于Redis 是如何进行请求处理的文章就介绍到这了,更多相关Redis请求处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot+Redis防止恶意刷新与暴力请求接口的实现

    目录 配置 目录结构 实现代码 本项目采用 springboot+Redis的方式来实现:所采用的全部参考文献在文末,包括软件的安装.测试等等 实验环境: centos 7 安装Redis ,采用wget安装, IDE :IDEA 配置 pom文件: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"

  • Scrapy-Redis结合POST请求获取数据的方法示例

    前言 通常我们在一个站站点进行采集的时候,如果是小站的话 我们使用scrapy本身就可以满足. 但是如果在面对一些比较大型的站点的时候,单个scrapy就显得力不从心了. 要是我们能够多个Scrapy一起采集该多好啊 人多力量大. 很遗憾Scrapy官方并不支持多个同时采集一个站点,虽然官方给出一个方法: **将一个站点的分割成几部分 交给不同的scrapy去采集** 似乎是个解决办法,但是很麻烦诶!毕竟分割很麻烦的哇 下面就改轮到我们的额主角Scrapy-Redis登场了! 能看到这篇文章的小

  • Redis原子计数器incr,防止并发请求操作

    一.前言 在一些对高并发请求有限制的系统或者功能里,比如说秒杀活动,或者一些网站返回的当前用户过多,请稍后尝试.这些都是通过对同一时刻请求数量进行了限制,一般用作对后台系统的保护,防止系统因为过大的流量冲击而崩溃.对于系统崩溃带来的后果,显然还是拒绝一部分请求更能被维护者所接受. 而在各种限流中,除了系统自身设计的带锁机制的计数器外,利用Redis实现显然是一种既高效安全又便捷方便的方式. 二.incr命令 Redis Incr 命令将 key 中储存的数字值增一. 如果 key 不存在,那么

  • scrapy-redis源码分析之发送POST请求详解

    1 引言 这段时间在研究美团爬虫,用的是scrapy-redis分布式爬虫框架,奈何scrapy-redis与scrapy框架不同,默认只发送GET请求,换句话说,不能直接发送POST请求,而美团的数据请求方式是POST,网上找了一圈,发现关于scrapy-redis发送POST的资料寥寥无几,只能自己刚源码了. 2 美团POST需求说明 先来说一说需求,也就是说美团POST请求形式.我们以获取某个地理坐标下,所有店铺类别列表请求为例.获取所有店铺类别列表时,我们需要构造一个包含位置坐标经纬度等

  • Redis请求处理的流程分析

    目录 概述# 调用流程# 命令执行过程 & 回写客户端# 命令执行# 数据回写客户端# 总结# Reference 本文使用的Redis 5.0源码 感觉这部分的代码还是挺有意思的,我尽量用比较通俗的方式进行讲解 概述# 我记得我在 一文说透 Go 语言 HTTP 标准库 这篇文章里面解析了对于 Go 来说是如何创建一个 Server 端程序的: 首先是注册处理器: 开启循环监听端口,每监听到一个连接就会创建一个 Goroutine: 然后就是 Goroutine 里面会循环的等待接收请求数据,

  • 使用celery和Django处理异步任务的流程分析

    介绍 我们可能需要一些可以安排一些任务并定期运行一些任务或异步处理长任务的东西,而这一切都可以通过在Django Project中使用Celery来实现. 什么是Celery? Celery是 一个专注于实时处理的任务队列,它还支持任务调度. Celery快速,简单,高度可用且灵活. Celery需要消息传输来发送和接收消息,这可以由Redis或RabbitMQ完成. 入门 让我们开始在您的virtualenv中安装Celery软件包. 安装Celery <span class="nv&q

  • 基于 antd pro 的短信验证码登录功能(流程分析)

    概要 最近使用 antd pro 开发项目时遇到个新的需求, 就是在登录界面通过短信验证码来登录, 不使用之前的用户名密码之类登录方式. 这种方式虽然增加了额外的短信费用, 但是对于安全性确实提高了不少. antd 中并没有自带能够倒计时的按钮, 但是 antd pro 的 ProForm components 中倒是提供了针对短信验证码相关的组件. 组件说明可参见: https://procomponents.ant.design/components/form 整体流程 通过短信验证码登录的

  • docker部署蜗牛影院系统详细流程分析

    环境声明 宿主机OS: Cetnos7.9 最小化安装 docker Version: 20.10.6 系统要求硬件配置: CPU2核以上,内存8G cpu核心数低于2核,影院端将无法登录 mysql数据库: mysql5.6 容器 redis数据库: redis4.0 容器 安装centos7.9 先停止防火墙和关闭SELinux 查看防火墙状态 firewall-cmd --state #或 systemctl status firewalld.service 停止firewall syst

  • 一个依赖搞定 Spring Boot 接口防盗刷的流程分析

    目录 系统要求​ 工作流程​ 命中规则后 接入使用 注意 配置一览表 kk-anti-reptile 是适用于基于 spring-boot 开发的分布式系统的反爬虫组件. 系统要求​ 基于 spring-boot 开发(spring-boot1.x, spring-boot2.x均可) 需要使用 redis 工作流程​ kk-anti-reptile 使用基于 Servlet 规范的的 Filter 对请求进行过滤,在其内部通过 spring-boot 的扩展点机制,实例化一个 Filter,并

  • Php中文件下载功能实现超详细流程分析

    客户端从服务端下载文件的流程分析: 浏览器发送一个请求,请求访问服务器中的某个网页(如:down.php),该网页的代码如下. 服务器接受到该请求以后,马上运行该down.php文件 运行该文件的时候,必然要把将要被下载的文件读入内存当中(这里是圣诞狂欢.jpg这张图片),这里通过fopen()函数完成该动作 注意:任何有关从服务器下载的文件操作,必然需要先在服务端将文件读入内存当中 现在文件已经在内存当中了,这是需要从内存当中读取文件,通过fread()函数完成该动作 需要注意的是,如果文件较

  • TCP/IP协议中三次握手四次挥手的原理及流程分析

    当初学的是通信专业,毕业以后,同学们各奔东西,去追逐自己的梦想,奔波于大大小小的工地之间.哈哈,开个玩笑,也有厉害的,进了某某研究所,嗯?他爸不是所长,内心不要太阴暗.记得有一门十分高大上的课程,名字叫做计算机网络(大概是这个名字吧).里面有一个关于握手的概念,现在温习一下. 先来看看原理图: TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接.在TCP/IP协议中,TCP 协议提供可靠的连接服务,连接是通过三次握手进行初始化的.三次握手的目的是同步连接双方的序列

  • koa2服务端使用jwt进行鉴权及路由权限分发的流程分析

    大体思路 后端书写REST api时,有一些api是非常敏感的,比如获取用户个人信息,查看所有用户列表,修改密码等.如果不对这些api进行保护,那么别人就可以很容易地获取并调用这些 api 进行操作. 所以对于一些api,在调用之前,我们在服务端必须先对操作者进行"身份认证",这就是所谓的鉴权. Json Web Token 简称为 JWT,它定义了一种通信双方之间以 JSON 对象的形式安全传递信息的方法.JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名,复杂度较

  • 详解Spring IOC 容器启动流程分析

    使用 Spring 时,XML 和注解是使用得最多的两种配置方式,虽然是两种完全不同的配置方式,但对于 IOC 容器来说,两种方式的不同主要是在 BeanDefinition 的解析上.而对于核心的容器启动流程,仍然是一致的. AbstractApplicationContext 的 refresh 方法实现了 IOC 容器启动的主要逻辑,启动流程中的关键步骤在源码中也可以对应到独立的方法.接下来以  AbstractApplicationContext 的实现类  ClassPathXmlAp

  • 详解Android4.4 RIL短信接收流程分析

    最近有客户反馈Android接收不到短信,于是一头扎进RIL里面找原因.最后发现不是RIL的问题,而是BC72上报 短信的格式不对,AT+CNMA=1无作用等几个小问题导致的.尽管问题不在RIL,但总算把RIL短信接收流程搞清楚了. 接收到新信息的log: D/ATC ( 1269): AT< +CMT:,27 D/ATC ( 1268): AT< 0891683108705505F0040d91683117358313f500009101329154922307ea31da2c36a301

随机推荐