来聊聊Redis客户端的概念

news2024/12/24 20:20:52

写在文章开头

对于每一个建立的连接redis都会通过redisClient来管理建立的socket连接的信息,本文将从源码的分析的角度来剖析的Redis客户端的基本设计和实现。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解redis客户端

redis客户端基本数据结构

我们先简单介绍一下redis的基本数据结构:

  1. 建立连接时redis服务端会通过fd记录分配的客户端socket文件描述符。
  2. 可以通过name字段来设置客户端的名称。
  3. 客户端操作的数据库都通过db来管理,记录操作的数据库的号码。
  4. 客户端可操作的指令集以及上一次指向的指令。
  5. 每当客户端通过指令进行管理数据库键值对时,对应的命令就会存储到querybuf,基于querybuf我们可以解析出命令、keyvalue等客户发送指令信息。
  6. 对应常规的响应结果,redis客户端会用buf记录。
typedef struct redisClient {
	//客户端id
    uint64_t id;            /* Client incremental unique ID. */
    //当前客户端socket的文件描述符
    int fd;
    //记录当前客户端操作的数据库指针
    redisDb *db;
    //客户端操作指令集和上一条指令的指针
    struct redisCommand *cmd, *lastcmd;
  	//......
    robj *name;             /* As set by CLIENT SETNAME */
    //客户端传入的字符串指令
    sds querybuf;
    //......

   //响应给客户端的输出缓冲区和偏移量
    int bufpos;
    char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

连接建立与客户端创建

了解了客户端的基本结构之后,我们就从客户端发起连接并建立通信的整个过程了解一下客户端初始化的逻辑,当redis服务端收到客户端的连接请求后,服务端会解析出这个establish socket的文件描述符fd,然后创建一个redisClient对象记录该fd,并完成客户端初始化,而整个初始化的核心步骤大致为:

  1. 分配可操作数据库指针。
  2. 记录客户端socket套接字的fd。
  3. 初始化各种输入、输出、指令字段等各种初始化操作。
  4. 将其添加到服务端cliens链表中。

在这里插入图片描述

对应的我们给出接收客户端连接的操作的入口,即redis服务端处理客户端连接的函数acceptTcpHandler,它拿到客户端socket的文件描述符fd之后,调用acceptCommonHandler完成客户端初始化和追加到server的客户端链表中:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
   //......

    while(max--) {
    //获取客户端socket套接字的文件描述符fd
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
         //......
       //基于cfd 完成客户端初始化并添加到服务端的clients链表中
        acceptCommonHandler(cfd,0);
    }
}

可以看到acceptCommonHandler会基于这个文件描述符创建redis客户端,并将其追加到redis链表的末尾:

tatic void acceptCommonHandler(int fd, int flags) {
    redisClient *c;
    //基于客户端socket的文件描述符创建redis客户端,如果创建失败则直接关闭socket
    if ((c = createClient(fd)) == NULL) {
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
  	//如果当前客户端数没有超过最大值(默认10000)则添加到链表clients末尾
    if (listLength(server.clients) > server.maxclients) {
        char *err = "-ERR max number of clients reached\r\n";

        /* That's a best effort error message, don't check write errors */
        if (write(c->fd,err,strlen(err)) == -1) {
            /* Nothing to do, Just to avoid the warning... */
        }
        server.stat_rejected_conn++;
        freeClient(c);
        return;
    }
    server.stat_numconnections++;
    c->flags |= flags;
}

了解了整体流程我们查看一下上文就说的客户端初始化代码,如笔者所说它会完成客户端基础信息初始化、socket的fd文件描述符信息维护、以及指令、输入、输出缓冲区空间初始化:

redisClient *createClient(int fd) {
    redisClient *c = zmalloc(sizeof(redisClient));

    //......
	//默认分配数据库0
    selectDb(c,0);
    //初始化客户端id
    c->id = server.next_client_id++;
    //记录客户端socket的文件描述符的值
    c->fd = fd;
    //名字默认空
    c->name = NULL;
    //输出缓冲区偏移量为0
    c->bufpos = 0;
    //初始化输入缓冲区
    c->querybuf = sdsempty();
   //......
   //命令指针初始化
    c->cmd = c->lastcmd = NULL;
     //......
     //追加到server的clients链表中进行统一管理
    if (fd != -1) listAddNodeTail(server.clients,c);
     //......
    return c;
}

服务端执行客户端发送的指令

完成基本连接建立工作之后,客户端就可以通过指令和服务端进行交互了,假设我们通过redis-cli键入指令set key valueredis服务端收到该指令后,通过该字符串会解析出对应的命令、keyvalue,然后通过命令的字符串set定位到指令setCommand,结合解析出的keyvalue完成键值对保存操作:

在这里插入图片描述

redis服务端轮询到对应客户端的指令后,通过fd定位到这个客户端的redisClient对象,调用该客户端的命令处理器readQueryFromClient,将命令字符串querybuf传入,调用processInputBuffer解析出命令、keyvalue存入argv中:

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    int nread, readlen;
   	//......
   	//调用processInputBuffer解析字符串并执行指令
    processInputBuffer(c);
    server.current_client = NULL;
}

我们步入processInputBuffer查看逻辑,可以看到它会调用processInlineBuffer单行字符串解析或者processMultibulkBuffer将字符串转为指令、keyvalue存到客户端的argv数组中,然后再调用processCommand处理解析出来的指令:

void processInputBuffer(redisClient *c) {
	//循环遍历querybuf解析指令和键值对
    while(sdslen(c->querybuf)) {
       //......
		//如果命令为单行则调用processInlineBuffer,如果多行则调用processMultibulkBuffer,将解析结果存入argv中
        if (c->reqtype == REDIS_REQ_INLINE) {
            if (processInlineBuffer(c) != REDIS_OK) break;
        } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != REDIS_OK) break;
        } else {
            redisPanic("Unknown request type");
        }

        //.......
        if (c->argc == 0) {
            resetClient(c);
        } else {
          	//调用processCommand处理解析出来的指令和键值对
            if (processCommand(c) == REDIS_OK)
                resetClient(c);
        }
    }
}

基于上一步解析出来的字符串数组argv,我们来到processCommand,通过数组argv[0]定位到命令为set,于是从当前服务端的命令表拿到setCommand方法并赋值给cmd指针,注意这里的命令表在客户端最后通过call函数完成调用:

int processCommand(redisClient *c) {
   //......
   //基于querybuf解析结果得到的数组argv的0索引位置定位到指令,以我们的操作为例则是set,然后赋值给cmd和lastcmd指针
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    //......
    
   
    if (c->flags & REDIS_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
       //......
    } else {//调用call方法执行我们定位到的指令函数
        call(c,REDIS_CALL_FULL);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnLists();
    }
    return REDIS_OK;
}

最终call方法会走到setCommand函数,其内部调用setGenericCommand完成我们的key value存储后,响应OK给客户端,自此一次客户端命令解析和操作完成:

void setCommand(redisClient *c) {
    //......
    //调用setGenericCommand进行键值对存储
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}


void setGenericCommand(redisClient *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    //......
    //调用key value存到客户端指向的db中,以本文演示的客户端为例,就是存到db0中
    setKey(c->db,key,val);
     //......
     //响应操作结果给客户端
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

小结

自此我们从redis客户端创建和指令交互的流程详细的分析的redis客户端这个结构体在redis中的作用,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

参考

《redis设计与实现》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通信协议总结

IIC 基本特点 同步,半双工 标准100KHz,最高400KHz(IIC主要应用于低速设备) 硬件组成 需外接上拉电阻 通信过程 空闲状态 SDA和SCL都处于高电平 开始信号S和终止信号P 在数据传输过程中,当SCL0时,SDA才…

mac14.1.2 M1芯片终端使用brew命令提示“zsh- command not found- brew ”解决方案

mac14.1.2 M1芯片终端使用brew命令提示“zsh- command not found- brew ” 原因:brew默认安装目录在/opt/homebrew/bin,zshrc文件中找不到对应的PATH路径导致。(可通过右键finder的图标选择「前往文件”-输入/opt/homebrew/bin」来查看brew是…

【图书推荐】CPython设计与实现“适合所有Python工程师阅读的书籍”

目录 一、图书推荐 |【CPython设计与实现】 1.1、书籍介绍 1.2、内容简介 1.3、适合哪些人阅读 1.4、作者译者简介 1.5、购买链接 一、图书推荐 |【CPython设计与实现】 "深入Python核心,揭秘CPython的设计智慧!📖 对于每一位热衷…

超详细的Pycharm使用虚拟环境搭建Django项目并创建新的虚拟环境教程

一、什么是虚拟环境? 通过软件虚拟出来的开发环境,不是真实存在的,一般在多套环境开发时会用到。 二、为什么要使用虚拟环境? 虚拟环境为不同的项目创建不同的开发环境,开发环境内所有使用的工具包互不影响。比如项…

初探 YOLOv8(训练参数解析)

文章目录 1、前言2、Backbone网络3、YOLOv8模型训练代码3.1、模型大小选择3.2、训练参数设置 4、训练参数说明5、目标检测系列文章 1、前言 YOLO 因为性能强大、消耗算力较少,一直以来都是实时目标检测领域的主要范式。该框架被广泛用于各种实际应用,包…

Linux C 程序 【02】创建线程

1.开发背景 上一个篇章,基于 RK3568 平台的基础上,运行了最简单的程序,然而我们使用了 Linux 系统,系统自带的多线程特性还是比较重要的,这个篇章主要描述线程的创建。 2.开发需求 设计实验: 创建一个线程…

线性相关,无关?秩?唯一解(只有零解),无穷解(有非零解)?D=0,D≠0?

目录 线性有关无关 和 唯一解(只有零解),无穷解(有非零解)之间的关系 D0,D≠0? 和 秩 的关系 串起来: 线性相关,无关?秩?唯一解(只…

【M365运维】Outlook和Teams里不显示用户的组织架构

【问题】 由于一些误操作,把用户账户禁用并重新启用后,发现在Outlook和Teams里无法查看用户的组织结构图了。如下图所示: - 在Outlook 里,用户标签页的组织一直显示“正在加载...",成员身份也是“找不到任何组。…

卸载vmware时2503,2502报错的解决办法

1.背景 windows 卸载vmware时,显示2503报错,无法完全卸载 2. 解决方案 2.1 参考安装报错2502,2503的处理方式 文献:https://blog.csdn.net/zhangvalue/article/details/80309828 2.1 步骤: 2.1.1 cmd 管理员打开…

O_CREAT创建函数的例子

代码&#xff1a; #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(void) {int fd-1;char filename[]"test.txt";fdopen(filename,O_RDWR|O_CREAT|O_EXCL,S_IRWXU);if(-1fd){printf("F…

Leetcode Hot100之链表

1.相交链表 解题思路 快慢指针&#xff1a;分别求出两个链表的长度n1和n2&#xff0c;在长度较长的那个链表上&#xff0c;快指针先走n2 - n1&#xff0c;慢指针再出发&#xff0c;最后能相遇则链表相交 时间复杂度O(mn)&#xff0c;空间复杂度O(1)代码# Definition for singl…

手写SpringMVC之调度器DispatcherServlet

DispatcherServlet&#xff1a;分发、调度 根据上一节&#xff0c;已经实现了将controller的方法添加到容器中&#xff0c;而DispatcherServlet的作用就是接收来自客户端的请求&#xff0c;然后通过URI的组合&#xff0c;来找到对应的RequestMapping注解的方法&#xff0c;调用…

基于esp-idf的arm2d移植

什么是ARM2D Arm在Github上发布了一个专门针对“全体” Cortex-M处理器的2D图形加速库——Arm-2D 我们可以简单的把这个2D图形加速库理解为是一个专门针对Cortex-M处理器的标准“显卡驱动”。虽然这里的“显卡驱动”只是一个夸张的说法——似乎没有哪个Cortex-M处理器“配得上…

记一次对ouija渗透测试c语言逆向学习

概要 初始知识 web应用枚举 二进制逆向 文件枚举 堆栈溢出 学到知识 hash长度攻击 任意文件读取 二进制逆向分析 信息收集 端口扫描 nmap --min-rate 1000 -p- 10.129.30.104 发现22&#xff0c;80&#xff0c;3000端口 网站探测 目录枚举 feroxbuster -u http://10.1…

Qt 基于FFmpeg的视频播放器 - 播放、暂停以及拖动滑动条跳转

Qt 基于FFmpeg的视频转换器 - 播放、暂停以及拖动进度条跳转 引言一、设计思路二、核心源码以及相关参考链接 引言 本文基于FFmpeg&#xff0c;使用Qt制作了一个极简的视频播放器. 相比之前的版本&#xff0c;加入了播放、暂停、拖动滑动条跳转功能&#xff0c;如上所示 (左图)…

局域网聊天软件 matrix

窝有 3 只 Android 手机 (3 号手机, 6 号手机, 9 号手机), 2 台 ArchLinux PC (4 号 PC, 6 号 PC), 1 台 Fedora CoreOS 服务器 (5 号). (作为穷人, 窝使用的基本上是老旧的二手设备, 比如 5 年前的手机, 9 年前的笔记本, 10 年前的古老 e5v3 主机, 都比较便宜. ) 窝经常需要 …

format()函数

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法介绍 format()可以对数据进行格式化处理操作&#xff0c;语法如下&#xff1a; format(value, format_spec) format_spec为格式化解释。当参数…

高性能Web服务器-Nginx的常用模块

文章目录 Nginx安装Nginx平滑升级与回滚平滑升级流程第1步&#xff0c;下载新版本第2步&#xff0c;编译第3步&#xff0c;执行make第4步&#xff0c;对比新旧版本第5步&#xff0c;备份旧nginx二进制文件第6步&#xff0c;模拟用户正在访问nginx第7步&#xff0c;替换旧的ngin…

The First Descendant第一后裔联机失败、联机报错这样处理

第一后裔/The First Descendant是一款免费的多人合作射击游戏&#xff0c;玩家将进入一片混乱的英格里斯大陆&#xff0c;扮演继承者后裔&#xff0c;通过各种主支线任务和故事剧情触发&#xff0c;最终揭开自身的秘密&#xff0c;并带领大家一起抵抗邪恶势力的入侵。为了避免玩…

Flume学习

Flume(分布式数据采集系统)学习 1.Flume架构 什么是flume&#xff1f; flume是一个分布式、可靠、和高可用的海量日志采集、聚合和传输的系统。 支持在日志系统中定制各类数据发送方&#xff0c;用于收集数据; 同时&#xff0c;Flume提供对数据进行简单处理&#xff0c;并写到…