redis事件循环源码分析

一、背景

redis是内存数据库,并且是单线程,为什么单线程也能够这么快?

因为,所有线上请求的set、get操作都是在内存中,涉及到磁盘和网络的部分都是由后台线程执行,尽量减少了主线程的开销。单线程只是说对字典空间set、get时是单线程的,不需要同步机制,而将数据在用户空间和页缓存之间的拷贝是由IOThreads做的,其中主线程也算是其中一个IOThread。

二、epoll使用案例

为了降低redis理解时的复杂度,先给出一个epoll使用时的case,有助于后面的理解。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<errno.h>
#include<fcntl.h>
#define _BACKLOG_ 5
#define _BUF_SIZE_ 10240
#define _MAX_ 64


typedef struct _data_buf
{
   
    int fd;
    char buf[_BUF_SIZE_];
}data_buf_t,*data_buf_p;
static void usage(const char* proc)
{
   
    printf("usage:%s[ip][port]\n",proc);
}

static int start(int port,char *ip)
{
   
    assert(ip);
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
   
        perror("socket");
        exit(1);
    }

    struct sockaddr_in local;
    local.sin_port=htons(port);
    local.sin_family=AF_INET;
    local.sin_addr.s_addr=inet_addr(ip);

    int opt=1;  //设置为接口复用
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
   
        perror("bind");
        exit(2);
    }

    if(listen(sock,_BACKLOG_)<0)
    {
   
        perror("listen");
        exit(3);
    }
    return sock;
}

static int epoll_server(int listen_sock)
{
   
    int epoll_fd=epoll_create(256);//生成一个专用的epoll文件描述符
    if(epoll_fd<0)
    {
   
        perror("epoll_create");
        exit(1);
    }

    struct epoll_event ev;//用于注册事件
    struct epoll_event ret_ev[_MAX_];//数组用于回传要处理的事件
    int ret_num=_MAX_;
    int read_num=-1;
    ev.events=EPOLLIN;
    ev.data.fd=listen_sock;
    //注册监听套接字
    if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ev)<0)//用于控制某个文件描述符上的事件(注册,修改,删除)
    {
   
        perror("epoll_ctl");
        return -2;
    }

    int done=0;
    int i=0;
    int timeout=5000;
    struct sockaddr_in client;
    socklen_t len=sizeof(client);
    while(!done)
    {
   
        //ret_ev存放的是触发的事件
        switch(read_num=epoll_wait(epoll_fd,ret_ev,ret_num,timeout))//用于轮寻I/O事件的发生
        {
   
            case 0:                                                                          
                printf("time out\n");
                break;
            case -1:
                perror("epoll");
                exit(2);
            default:
                {
   
                    for(i=0;i<read_num;++i)
                    {
   
                        //连接建立事件
                        if(ret_ev[i].data.fd==listen_sock&&(ret_ev[i].events&EPOLLIN))
                        {
   
                            int fd=ret_ev[i].data.fd;
                            int new_sock=accept(fd,(struct sockaddr*)&client,&len);
                            if(new_sock<0)
                            {
   
                                perror("accept");
                                continue;
                            }

                            ev.events=EPOLLIN;
                            ev.data.fd=new_sock;
                            epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&ev);
                            printf("get a new client...\n");
                        }
                        else  //normal sock
                        {
   
                            //读事件
                            if(ret_ev[i].events&EPOLLIN)
                            {
   
                                int fd=ret_ev[i].data.fd;
                                data_buf_p mem=(data_buf_p)malloc(sizeof(data_buf_t));
                                if(!mem){
   
                                    perror("malloc failed...");                                                                      
                                    continue;
                                }
                                mem->fd=fd;
                                memset(mem->buf,'\0',sizeof(mem->buf));
                                ssize_t _s=read(mem->fd,mem->buf,sizeof(mem -> buf)-1);
                                if(_s>0)
                                {
   
                                    mem->buf[_s-1]='\0';
                                    printf("client: %s\n",mem->buf);
                                    ev.events=EPOLLOUT;
                                    ev.data.ptr=mem;
                                    epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fd,&ev);
                                }
                                else if(_s==0)
                                {
   
                                    printf("client close...\n");
                                    epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,NULL);
                                    close(fd);
                                    free(mem);
                                }
                            }
                            else if(ret_ev[i].events&EPOLLOUT)  //写事件准备就绪
                            {
   
                                data_buf_p mem=(data_buf_p)ret_ev[i].data.ptr;
                                char* msg="http/1.0 200 ok\r\n\r\nhello bit\r\n";
                                int fd=mem->fd;
                            
                                write(fd,msg,strlen(msg));
                                close(fd);
                                epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,&ev);  //写完服务端直接退出
                                free(mem);
                            }
                        }
                    }
                }
        }
    }
}

int main(int argc,char* argv[])
{
   
    if(argc!=3)
    {
   
        usage(argv[0]);
        return 1;
    }

    int port=atoi(argv[2]);
    char *ip=argv[1];
    //创建监听套接字
    int listen_sock=start(port,ip);
    printf("listening... ip: %s, port: %d\n",ip,port);
    //注册监听套接字事件,accept新链接并注册进入epoll
    epoll_server(listen_sock);
    close(listen_sock);
    return 0;
}

在这里插入图片描述

主要流程如下:

  1. 创建socket,并转化成为listen socket。
  2. 将listen socket fd注册到epoll中,epollfd是用来标识epoll这个对象的,处处皆文件。
  3. 如果触发事件的fd是listen socket fd,那表示来了新连接,
    • 先要epoll_wait,将数据读入到用户空间;
    • 调用accept创建新连接newfd;
    • 将newfd注册读事件到epoll中。
  4. 如果触发的是写事件,则调用write,并删除这个事件。
  5. 如果触发的是读事件,则调用read获取请求,并投递到业务线程池,执行完后会注册一个写事件到达epoll中。服务端一般采用ET模式,没读完下次也不会返回这个事件,读时一般会用while循环一次性读完。

三、redis事件注册

redis事件循环
redis初始化时会创建所有的listen socket并注册到eventloop中监听,绑定accept Handler,当连接建立时就会accept新连接,创建client并向eventloop注册读事件。

下面主要会从以下几方面叙述:

  1. 事件的注册逻辑,包括listen socket fd以及client fd。
  2. 读事件分发处理逻辑。
  3. 过期key逻辑。
  4. 写事件分发处理逻辑。
  5. 监听器暂时不讲。

事件注册包括创建listen socket,注册事件,设置handler等。

事件分发流程如下所示,后面会慢慢介绍。
redis事件分发处理流程

1 数据结构

//只涉及后面用到的字段
struct redisServer {
   
    redisDb *db; 
    
    aeEventLoop *el; //epoll

    socketFds cfd;              /*  listening socket */
    list *clients;              /* List of active clients */
    list *clients_to_close;     /* Clients to close asynchronously */
    list *clients_pending_write; /* There is to write or install handler. */
    list *clients_pending_read;  /* Client has pending read socket buffers. */
    client *current_client;    
}

//io多路复用

/* State of an event based program */
typedef struct aeEventLoop {
   
    int maxfd;   /* highest file descriptor currently registered */
    int setsize; /* max number of file descriptors tracked */
    long long timeEventNextId;
    aeFileEvent *events; /* 当前所有注册的事件,包括已经触发和没有触发的*/
    aeFiredEvent *fired; /* 本次触发的事件*/
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* aeApiState */
    aeBeforeSleepProc *beforesleep; //预处理,很重要,请求分发逻辑
    aeBeforeSleepProc *aftersleep; //尾处理
    int flags;
} aeEventLoop;

/* File event structure */
typedef struct aeFileEvent {
   
    int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;

/* A fired event */
typedef struct aeFiredEvent {
   
    int fd;
    int mask;
} aeFiredEvent;

2 创建listen socket

这一阶段完成了socket、bind、listen三步。

int listenToPort(int port, socketFds *sfd) {
   
    int j;
    char **bindaddr = server.bindaddr;

    /* If we have no bind address, we don't listen on a TCP socket */
    if (server.bindaddr_count == 0) 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值