redis学习(一)

背景

redis是一个高性能的KV数据库,在工作中经常用到,可被用作缓存、分布式锁等,作为被高频使用的组件,了解其实现对工作有很大帮助(包括面试)。为此,在对redis有一定的使用、了解之后,希望通过阅读其源码,理解redis实现过程,由简单到复杂地分析其源码,并尝试对源码从零到一地复现,以此来加深对redis的理解。

由于时间原因,无法一次性将所有的代码在短时间内理解,所以在学习过程中,尝试在每个阶段制定一个简单的目标,然后根据源码去实现。

redis版本:5.0.13,redis各版本源码下载链接

目标

此次学习目标为:实现redis server,接收redis client请求。

原理

redis client与server之间基于tcp通信,且使用IO复用的技术。

实现细节

redisServer

redis定义了一个数据结构:redisServer,用来管理server相关的信息。其定义如下:

cpp 复制代码
struct redisServer {

    aeEventLoop *el;

    /* 网络 */
    int port;                     /* TCP监听端口 */
    int tcp_backlog;              /* TCP listen() backlog */
    int ipfd[CONFIG_BINDADDR_MAX]; /* TCP socket文件描述符 */
    int ipfd_count;               /* 正在使用的ipfd的索引 */
    char neterr[ANET_ERR_LEN];   /* anet.c中错误信息的buffer */
    list *clients;                /* 活跃客户端 */
    /* 限制 */
    unsigned int maxclients;      /* 最多能同时存在的最大clients数 */
    /* 配置 */
    int verbosity;                /* 日志等级, redis.conf中配置 */
    int tcpkeepalive;             /* 如果非0,则设置SO_KEEPALIVE */
};

本次实现只考虑redisServer能接收client的请求,只将一些必要的数据列出,实际的数据远不止这些。

整体流程

先来看下整体流程:

主流程包括3步:

  1. 初始化配置initServerConfig:初始化redsiServer中的各数据默认值
  2. 初始化服务initServer:创建时间处理对象,监听端口,设置收到client连接时的回调函数
  3. 进入事件处理主函数aeMain:调用epoll,监听待处理的事件,当有事件就绪时处理事件

初始化默认值,就是对服务运行需要的一些参数设置默认值,比较简单,此处不做过多介绍,具体细节可参考代码。下面主要说一下initServer和aeMain是如何使用IO复用的技术来处理网络请求。

initServer

initServer主要流程如下,标红部分为IO多路复用关键代码:

initServer主要包含以下步骤:

  • 初始化clients:clients实际是一个列表,这里初始化仅仅是创建了一个空列表。
  • aeCreateEventLoop:初始化事件处理相关的数据,调用epoll_create来创建IO复用相关的文件描述符。
  • listenToPort:创建socket,监听对应的端口(默认6379,在initServerConfig时赋值),同时将其设置为非阻塞IO(O_NONBLOCK)。
  • aeCreateFileEvent:调用epoll_ctl,将socket文件描述符可读事件注册到epoll中。同时,将文件可读时的处理函数设置为acceptTcpHandler。

其实,InitServer中的关键代码涉及的知识点是socket编程、IO多路复用。如果对IO多路复用不了解,建议学习这本书:《Linux高性能服务器编程》。

aeMain

aeMain为一个while(true)的循环(实际是while(!server.el->stop),在没有收到停止的信号是,即为while(true)),循环里面调用aeProcessEvents来处理,其主要步骤有:

  • aeApiPoll:调用epoll_wait,获取注册到epfd中文件描述符的就绪事件,并设置事件的类型
  • rfileProc:如果为读事件,则调用该函数
  • wfileProc:如果为写事件,则调用该函数

初始状态下,epfd中仅监听了server.ipfd,即server socket的可读事件,该事件是在initServer时注册,rfileProc被设置为acceptTcpHandler。以下是acceptTcpHandler的主要流程:

主要步骤为:

  • anetTcpAccept:调用accept,获取client的ip和端口,以及client对应的socket(cfd)
  • acceptCommonHandler:创建一个client对象,用于管理对应的client状态和数据,创建client对象的主要步骤:
    • anetNonBlock:将client的连接设置为非阻塞连接。
    • anetEnableTcpNoDelay:调用setsockopt,设置TCP_NODELAY。该标志具体含义可自行查阅,个人理解为:redis为了能尽快响应每个请求,在处理完成之后,需要立即返回client数据,舍弃了整体网络的最优化,因为如果TCP每次传输的数据过少,可能会导致网络传输数据的利用率低,如果未设置TCP_NODELAY,则server端处理完成之后,可能不能立即返回client数据,需要积攒一波数据之后一次发出,这可能导致单个请求耗时增加。
    • anetKeepAlive:通过setsockopot,设置tcp keepalive相关的参数,具体含义自行查阅,代码中也有相应的注释。
    • aeCreateFileEvent:将client对应的文件描述符cfd可读事件注册到epfd中,并设置可读事件处理函数为readQueryFromClient。
    • linkClient:将client链接到server.clients这个链表的尾部。

当有client请求与server建立连接时,acceptTcpHandler被执行,同时这个连接通过struct client进行管理,与client关联的socket(cfd)可读事件被注册到epfd中,如果client发送数据到server,在aeMain中,epoll_wait会返回cfd可读,在aeApiPoll中将该事件记录在server.fired中,然后调用cfd对应的rfileProc,即:readQueryFromClient。

本次学习,仅考虑接收到client发送的数据,并不处理,所以readQueryFromClient,本次实现时,只打印接收到的client数据。

数据结构

这里对上述的代码中,总结所有用到的数据结构。对每一个数据结构,仅列出与本文相关的部分,便于理解。

redisServer

cpp 复制代码
struct redisServer {

    aeEventLoop *el;

    /* 网络 */
    int port;                     /* TCP监听端口 */
    int tcp_backlog;              /* TCP listen() backlog */
    int ipfd[CONFIG_BINDADDR_MAX]; /* TCP socket文件描述符 */
    int ipfd_count;               /* 正在使用的ipfd的索引 */
    char neterr[ANET_ERR_LEN];   /* anet.c中错误信息的buffer */
    list *clients;                /* 活跃客户端 */
    /* 限制 */
    unsigned int maxclients;      /* 最多能同时存在的最大clients数 */
    /* 配置 */
    int verbosity;                /* 日志等级, redis.conf中配置 */
    int tcpkeepalive;             /* 如果非0,则设置SO_KEEPALIVE */
};

aeEventLoop

cpp 复制代码
typedef struct aeEventLoop {
    int maxfd;           /* 当前注册的文件描述符中的最大的 */
    int setsize;         /* 最多能管理的文件描述符数量 */
    aeFileEvent *events; /* 注册的事件 */
    aeFiredEvent *fired; /* 激活的事件 */
    int stop;
    void *apidata;       /* polling API需要使用的数据 */
} aeEventLoop;

aeFileEvent

cpp 复制代码
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);

typedef struct aeFileEvent {
    int mask; /* 可选值: AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;

aeFiredEvent

cpp 复制代码
/* 激活的事件 */
typedef struct aeFiredEvent {
    int fd;
    int mask;              
} aeFiredEvent;

client

cpp 复制代码
typedef struct client {
    int fd;                     /* client socket */
    listNode *client_list_node; /* client在server clients列表中的哪个节点 */
} client;

list

cpp 复制代码
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
    listNode *head;
    listNode *tail;
    void (*free)(void *ptr);
    unsigned long len;
} list;

aeApiState

cpp 复制代码
typedef struct aeApiState {
    int epfd;
    struct epoll_event *events;
} aeApiState;

总览

代码结构

效果

client端:

server端:

client端通过redis源码编译得到,server端是本次实现的redis代码。可以看到,server端可以收到client端传过来的数据,具体的数据含义待后续学习去理解。

附录

本次功能实现的代码链接

相关推荐
野猪亨利66742 分钟前
Qt day1
开发语言·数据库·qt
本就一无所有 何惧重新开始1 小时前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
isaki1371 小时前
qt day1
开发语言·数据库·qt
流星白龙1 小时前
【Qt】4.项目文件解析
开发语言·数据库·qt
小钻风33661 小时前
HTTPS是如何确保安全的
网络·数据库
CryptoPP2 小时前
获取越南股票市场列表(包含VN30成分股)实战指南
大数据·服务器·数据库·区块链
阿巴~阿巴~3 小时前
Redis重大版本演进全解析:从2.6到7.0
服务器·数据库·redis·ubuntu·缓存·centos
qq_404643344 小时前
MySQL中RUNCATE、DELETE、DROP 的基本介绍
数据库·mysql
像风一样!5 小时前
MySQL数据库如何实现主从复制
数据库·mysql
大白的编程日记.5 小时前
【MySQL】数据库表的CURD(二)
android·数据库·mysql