浅析Redis③:命令处理之数据返回Client(下)

写在前面

Redis作为我们日常工作中最常使用的缓存数据库,其重要性不言而喻,作为普通开发者,我们在日常开发中使用Redis,主要聚焦于Redis的基层数据结构的命令使用,很少会有人对Redis的内部实现机制进行了解,对于我而言,也是如此,但一直以来,我对于Redis的内部实现都很好奇,它为什么会如此高效,本系列文章是旨在对Redis源代码分析拆解,通过阅读Redis源代码,了解Redis基础数据结构的实现机制。

关于Redis的源码分析,已经有非常多的大佬写过相关的内容,最为著名的是《Redis设计与实现》,对于Redis源码的分析已经非常出色,本系列文章对于源码拆解时,并不会那么详细,相信大部分读者应该不是从事Redis的二次开发工作,对于源码细节过于深入,会陷入细节的泥潭,这是我在阅读源码时尽量避免的,我尽量做到对大体的脉络进行梳理,讲清楚主干逻辑,细节部分,如果读者有兴趣,可以自行参阅源码或相关资料。

本系列源代码,基于Redis 3.2.6

前言

在上两篇中

浅析Redis①:命令处理核心源码分析(上)

浅析Redis②:命令处理之epoll实现(中)

我们大致了解了Redis客户端命令请求的处理流程,在整个流程中,我们了解了Redis是如何处理来自客户端的命令请求,epoll的执行逻辑,我们还有最后一个问题没有解释,Redis是如何将数据写回Client端的?

本篇我们就围绕第一个问题,寻找答案,继续看Redis客户端命令请求的处理流程。

Redis数据返回Client端流程

Redis在命令处理时,在命令执行的末尾,都会调用一个addReply(),这里我们以最简单的STRING get为例:

t_string.c getGenericCommand()

int getGenericCommand(client *c) {
    robj *o;
	
    // 从字典中查询数据
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return C_OK;
	
    // 将数据返回Client
    if (o->type != OBJ_STRING) {
        addReply(c,shared.wrongtypeerr);
        return C_ERR;
    } else {
        addReplyBulk(c,o);
        return C_OK;
    }
}

void addReplyBulk(client *c, robj *obj) {
    addReplyBulkLen(c,obj);
    addReply(c,obj);
    addReply(c,shared.crlf);
}

继续看addReply()的实现:

networking.c addReply()

void addReply(client *c, robj *obj) {
    if (prepareClientToWrite(c) != C_OK) return;

    // 核心,将数据写入内存缓存区,等待后续流程处理,写回Client Socket
    if (sdsEncodedObject(obj)) {
        if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
            _addReplyObjectToList(c,obj);
    } else if (obj->encoding == OBJ_ENCODING_INT) {
        if (listLength(c->reply) == 0 && (sizeof(c->buf) - c->bufpos) >= 32) {
            char buf[32];
            int len;
            len = ll2string(buf,sizeof(buf),(long)obj->ptr);
            if (_addReplyToBuffer(c,buf,len) == C_OK)
                return;
        }
        obj = getDecodedObject(obj);
        if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
            _addReplyObjectToList(c,obj);
        decrRefCount(obj);
    } else {
        serverPanic("Wrong obj->encoding in addReply()");
    }
}

上述流程,是string get命令执行后,数据处理的流程,可以发现,Redis并没有将数据直接返回Client端,而是将数据写入了一个叫做缓冲区的内存区域,那么缓冲区是什么?

Redis的内存缓冲区

在 Redis 中,缓冲区(buffer)是用于存储数据的内存区域。Redis 使用缓冲区来管理数据的读取、写入和传输过程。

Redis 的缓冲区主要有两个方面的应用:

  • 输入缓冲区(Input Buffer):当 Redis 接收到客户端发送的命令请求时,会先将请求数据存储在输入缓冲区中,然后再进行解析和处理。输入缓冲区用于临时存储从网络或其他输入源接收到的原始数据。
  • 输出缓冲区(Output Buffer):当 Redis 响应客户端的命令请求时,会先将响应数据存储在输出缓冲区中,然后再发送给客户端。输出缓冲区用于临时存储待发送的数据。

缓冲区在 Redis 中的作用是提高数据的处理效率和性能。通过使用缓冲区,Redis 可以批量读取和写入数据,减少了频繁的系统调用和网络传输开销。此外,缓冲区还可以用于临时存储数据,以便进行数据的加工和处理。

需要注意的是,Redis 缓冲区大小是有限的,它受到配置参数 client-output-buffer-limit 和 client-query-buffer-limit 的影响。

如果缓冲区已满,而输入或输出数据仍在不断到达,则可能导致连接被拒绝或数据丢失。

因此,在高并发或大数据量的场景中,需要根据实际情况调整缓冲区大小以保证系统的稳定性和性能。

OK,命令处理部分流程结束,我们把逻辑拉回到main函数中,聚焦aeMain()

ae.c aeMain()

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在前两篇中,我们介绍过aeMain(),这里使用一个死循环,aeProcessEvents()轮询epoll是否存在就绪的事件,在aeProcessEvents()之前,我们需要关注beforesleep()

if (eventLoop->beforesleep != NULL)
    eventLoop->beforesleep(eventLoop);

在轮询之前,都会执行beforesleep(),这个函数就是我们要关注的核心,继续看beforesleep()实现:

server.c beforeSleep()

void beforeSleep(struct aeEventLoop *eventLoop) {
....
....    
此处省略部分非核心代码
....  
    
    /* Write the AOF buffer on disk */
    flushAppendOnlyFile(0);

    // 将数据写回Client
    handleClientsWithPendingWrites();
}

networking.c handleClientsWithPendingWrites()

int handleClientsWithPendingWrites(void) {
    listIter li;
    listNode *ln;
    int processed = listLength(server.clients_pending_write);

    listRewind(server.clients_pending_write,&li);
    while((ln = listNext(&li))) {
        ......
        省略部分非核心代码    

        // 核心,将数据通过socket返回Client
        if (writeToClient(c->fd,c,0) == C_ERR) continue;

        // 还有部分数据没有写完,加入epoll,等待异步执行
        if (clientHasPendingReplies(c) &&
            aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
                sendReplyToClient, c) == AE_ERR)
        {
            // 释放内存
            freeClientAsync(c);
        }
    }
    return processed;
}

上述代码是执行数据返回Client的核心逻辑,可以参见代码注释,令人疑惑的部分是,为什么这段代码中,writeToClient()可能会执行两次?

原因如下:

第一次调用 writeToClient() 是为了尝试向客户端套接字写入数据。这里的目的是将服务器待发送的数据写入到套接字缓冲区中,以便后续通过网络发送给客户端。如果写入成功,则会继续判断该客户端是否还有待发送的数据。

第二次调用 writeToClient() 是在判断客户端是否还有待发送的数据后执行的。如果客户端仍然有待发送的数据,那么说明套接字的发送缓冲区已满,无法一次性将所有数据发送出去。此时,为了确保后续的数据能够被及时发送,需要将该客户端的套接字注册到可写事件上,以便在套接字可写时继续发送剩余的数据。

需要注意的是,第二次调用 writeToClient() 并不会立即执行数据的发送,而是在套接字变为可写时由事件循环机制触发相应的写入操作。

这样可以避免在套接字无法写入数据时出现阻塞的情况,提高服务器的并发性能。

OK,我们继续看writeToClient() 的实现逻辑。

networking.c writeToClient()

int writeToClient(int fd, client *c, int handler_installed) {
    ssize_t nwritten = 0, totwritten = 0;
    size_t objlen;
    size_t objmem;
    robj *o;
	
    // 循环读取内存缓冲区的数据,写回socket,返回Client端
    while(clientHasPendingReplies(c)) {
        if (c->bufpos > 0) {
            // 核心,执行socket写回
            nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            /* If the buffer was sent, set bufpos to zero to continue with
             * the remainder of the reply. */
            if ((int)c->sentlen == c->bufpos) {
                c->bufpos = 0;
                c->sentlen = 0;
            }
        } else {
            o = listNodeValue(listFirst(c->reply));
            objlen = sdslen(o->ptr);
            objmem = getStringObjectSdsUsedMemory(o);

            if (objlen == 0) {
                listDelNode(c->reply,listFirst(c->reply));
                c->reply_bytes -= objmem;
                continue;
            }

            nwritten = write(fd, ((char*)o->ptr)+c->sentlen,objlen-c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            /* If we fully sent the object on head go to the next one */
            if (c->sentlen == objlen) {
                listDelNode(c->reply,listFirst(c->reply));
                c->sentlen = 0;
                c->reply_bytes -= objmem;
            }
        }
            
        .........
    	省略部分非核心代码
    	.........    
    }
    
    .........
    省略部分非核心代码
    .........    
    
    return C_OK;
}

writeToClient()就是核心写入的部分了,这里获取redisClient对象的bufpos,可以理解为缓冲区中的标记位置,如果存在待写入的数据,循环调用系统方法write写入socketFD中。

write() 函数用于向文件描述符(包括套接字)写入数据。在这段代码中,write() 函数被用于将数据写入到客户端的套接字中,即向客户端发送数据。

就此,Redis将数据返回Client的流程,我们就了解完毕。

老规矩,我们还是用一张流程图来简略描述整个过程:
Redis命令执行结果返回Client流程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/368561.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python算法题集_合并区间

本文为Python算法题集之一的代码示例 题目56&#xff1a;合并区间 说明&#xff1a;以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需…

leetcode 1.两数之和(C++)DAY1(待补充哈希表法)

文章目录 1.题目描述示例提示 2.解答思路3.实现代码结果4.总结 1.题目描述 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&…

假期2.3

第二章 引用内联重载 一&#xff0e;选择题-* 1、适宜采用inline定义函数情况是&#xff08;C&#xff09; A. 函数体含有循环语句 B. 函数体含有递归语句‘、考科一 ’ C. 函数代码少、频繁调用 D. 函数代码多、不常调用 2、假定一个函数为A(int i4, int j0) {;}, 则执行“A …

Datawhale组队学习 Task10 环境影响

第12章 环境影响 在本章中&#xff0c;首先提出一个问题&#xff1a;大语言模型对环境的影响是什么&#xff1f; 这里给出的一个答案是&#xff1a;气候变化 一方面&#xff0c;我们都听说过气候变化的严重影响(文章1、文章2)&#xff1a; 我们已经比工业革命前的水平高出1.…

LeetCode热题HOT100【栈的压入、弹出序列】

&#x1f525;LeetCode热题HOT100【栈的压入、弹出序列】 1. 题目来源2.题目 1. 题目来源 来自LeetCode热题HOT100 https://leetcode.cn/studyplan/top-100-liked/?isDarktrue 2.题目 题目地址 Leetcode地址 3.Stack 在Java中&#xff0c;Stack 是一个基于后进先出&#…

玩美移动为花西子海外官网打造AR虚拟试妆决方案

全球领先的增强现实&#xff08;AR&#xff09;及人工智能&#xff08;AI&#xff09;美妆科技领导者及玩美系列APP开发商——玩美移动&#xff08;纽交所代码&#xff1a;PERF&#xff09;于近日宣布携手知名美妆品牌花西子&#xff0c;在其线海外官方网页提供多项彩妆虚拟试妆…

TanDEM-X30米DEM数据介绍

一、背景 之前介绍了Copernicus 30米DEM以及Alos 30米DEM数据的详细介绍以及接入到Cesium中的效果展示&#xff0c;有遥感专业工作者对比了Copernnicus、ALOA、ASTER、NASA、SRTM这几家30米DEM数据&#xff0c;得出了Copernicus 30米DEM数据是最好的全球级30米DEM数据&#xf…

Java8 中文指南(一)

Java8 中文指南&#xff08;一&#xff09; 文章目录 Java8 中文指南&#xff08;一&#xff09;《Java8 指南》中文翻译接口的默认方法(Default Methods for Interfaces)Lambda 表达式(Lambda expressions)函数式接口(Functional Interfaces)方法和构造函数引用(Method and Co…

Unity 图片不改变比例适配屏幕

Unity 图片不改变比例适配屏幕 前言项目场景布置代码编写添加并设置脚本效果 前言 遇到一个要让图片适应相机大小&#xff0c;填满屏幕&#xff0c;但不改变图片比例的需求&#xff0c;记录一下。 项目 场景布置 代码编写 创建AdaptiveImageBackground脚本 using System.C…

QT 应用中集成 Sentry

QT 应用中集成 Sentry QT应用中集成 SentrySentry SDK for C/C注册 Sentry 账号QT 应用中集成 Sentry触发 Crash 上报 QT应用中集成 Sentry Sentry 是一个开源的错误监控和日志记录平台&#xff0c;旨在帮助开发团队实时捕获、跟踪和解决软件应用程序中的错误和异常。它提供了…

Python flask 表单详解

文章目录 1 概述1.1 request 对象 2 示例2.1 目录结构2.2 student.html2.3 result.html2.4 app.py 1 概述 1.1 request 对象 作用&#xff1a;来自客户端网页的数据作为全局请求对象发送到服务器request 对象的重要属性如下&#xff1a; 属性解释form字典对象&#xff0c;包…

如何批量获取当前文件夹下的文件名

最近&#xff0c;在和网友交流时&#xff0c;对方推荐了一个视频&#xff0c;我打开一看&#xff0c;是一个手工获取当前目录下所有文件名的手机视频。用的方法是在win11中复制所有文件的路径&#xff0c;然后粘贴到Excel当中&#xff0c;通过查找替换和分列的方法&#xff0c;…

EasyX图形库学习(二)

目录 一、文字绘制函数 settextstyle 设置当前文字样式。 outtextxy 在指定位置输出字符串。 ​编辑 但如果直接使用,可能有以下报错&#xff1a; 三种解决方案&#xff1a; 将一个int类型的分数,输出到图形界面上 如果直接使用&#xff1a; 会把score输入进去根据A…

被人疯狂吐槽的预制菜,居然是资本看重的“万亿级”市场?

被人疯狂吐槽的预制菜&#xff0c;居然是资本看重的“万亿级”市场&#xff1f; 文丨微三云营销总监胡佳东&#xff0c;点击上方“关注”&#xff0c;为你分享市场商业模式电商干货。 - 大家是不是以为只有被天天吐槽难吃的外卖和小饭店&#xff0c;才会用预制菜&#xff0c;…

#从零开始# 在深度学习环境中,如何用 pycharm配置使用 pipenv 虚拟环境

为Python项目创建虚拟环境 在深度学习环境和一般python环境中安装pipenv基本一致&#xff0c;只需要确认好pipenv指定的python版本即可,安装pipenv前&#xff0c;可以通过python --version来确认安装版本 快捷键&#xff1a;crtl alt S 查看interpreter&#xff0c;查看所有…

代码随想录算法训练营第42天 | 01背包问题,你该了解这些! 01背包问题,你该了解这些! 滚动数组 416. 分割等和子集

目录 01背包问题&#xff0c;你该了解这些&#xff01; 01 背包 二维dp数组01背包 &#x1f4bb;实现代码 01背包问题&#xff0c;你该了解这些&#xff01; 滚动数组 一维dp数组&#xff08;滚动数组&#xff09; &#x1f4bb;实现代码 416. 分割等和子集 &#x1f…

《Numpy 简易速速上手小册》第9章:Numpy 在机器学习中的应用(2024 最新版)

文章目录 9.1 数据预处理9.1.1 基础知识9.1.2 完整案例&#xff1a;数据标准化9.1.3 拓展案例 1&#xff1a;缺失值处理9.1.4 拓展案例 2&#xff1a;非数值数据的转换 9.2 特征提取和处理9.2.1 基础知识9.2.2 完整案例&#xff1a;特征归一化9.2.3 拓展案例 1&#xff1a;特征…

MySQL知识点总结:构建可靠高性能的关系型数据库

摘要&#xff1a;MySQL是一款广泛使用的开源关系型数据库管理系统&#xff0c;具备可靠性和高性能的特点。本文将总结MySQL的一些重要知识点&#xff0c;帮助读者了解如何使用MySQL构建可靠高性能的关系型数据库。 正文&#xff1a; ### 1. 数据类型 MySQL支持多种数据类型&…

SpringBoot整合Activiti7—— 补偿边界/补偿中间事件(十五)

文章目录 补偿边界/补偿中间事件代码实现xml文件测试流程流程执行步骤 补偿边界/补偿中间事件 补偿事件可以被触发来回滚或修复之前已经完成的任务或活动。 补偿事件通常与错误边界事件&#xff08;Error Boundary Event&#xff09;结合使用。当任务或活动发生异常时&#xff…

SQL sever2008中创建用户并赋权

一、创建数据库dream CREATE DATABASE dream; 二、创建登录用户XZS 法一&#xff1a;使用SSMS创建 通过查询 sys.syslogins 系统视图来确定当前登录是否具有系统管理员权限。执行以下查询语句&#xff1a; SELECT name, isntname FROM sys.syslogins WHERE sysadmin 1;选…