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端传过来的数据,具体的数据含义待后续学习去理解。

附录

本次功能实现的代码链接

相关推荐
Leo.yuan36 分钟前
数据量大Excel卡顿严重?选对报表工具提高10倍效率
数据库·数据分析·数据可视化·powerbi
Runing_WoNiu1 小时前
MySQL与Oracle对比及区别
数据库·mysql·oracle
sam-1231 小时前
k8s上部署redis高可用集群
redis·docker·k8s
天道有情战天下1 小时前
mysql锁机制详解
数据库·mysql
看山还是山,看水还是。1 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
谷新龙0011 小时前
Redis运行时的10大重要指标
数据库·redis·缓存
CodingBrother1 小时前
MySQL 中单列索引与联合索引分析
数据库·mysql
精进攻城狮@1 小时前
Redis缓存雪崩、缓存击穿、缓存穿透
数据库·redis·缓存
小酋仍在学习1 小时前
光驱验证 MD5 校验和
数据库·postgresql
keep__go2 小时前
Linux 批量配置互信
linux·运维·服务器·数据库·shell