1

作者:施洪宝 顺风车运营研发团队
一. 压缩列表
压缩列表是Redis的关键数据结构之一。目前已经有大量的相关资料,下面几个链接都已经对Ziplist进行了详细的介绍。

http://origin.redisbook.com/c...
https://segmentfault.com/a/11...
本文只对其进行总体上的介绍,更多详细内容可以参考上面3个链接以及Redis源码。

  1. Ziplist的整体结构

Ziplist整体结构图如下:

clipboard.png

上图中,每个域的具体功能为:

clipboard.png

2.节点结构
Ziplist的节点结构如下:

clipboard.png

(1) pre_entry_length

pre_entry_length字段记录了上一个节点的长度,通过这个值,我们可以很容易的从当前节点跳转到上一节点。例如:

clipboard.png

根据编码方式的不同,pre_entry_length可能占用一个字节,也可能占用5个字节。具体处理规则如下:

如果前一个节点长度小于254,便使用1个字节记录上一个节点长度。

如果前一个字节长度大于254,pre_entry_length便占用5个字节,第一个字节写入254,后面4个字节记录上一个节点的具体长度。

(2) enconding以及length

encoding 和 length 决定了content 保存的数据类型及长度,encodingz占用2个bit,encoding以及length的总长度可能是1个,2个或者5个字节。

00,01,10表示content的内容为字符数组
11表示content的内容为整形
具体的规则如下:

clipboard.png

(3) content

content为所存储的数据,其类型和长度由encoding 和 length 决定。

(4) Example

clipboard.png

3.基本操作
数据结构的基本操作有:增加节点、删除节点、查找节点等。由于Ziplist在内存上是连续存储的,故而在特定位置插入或者删除操作的复杂度较高。Ziplist定义了下面的几种操作:

clipboard.png

值得一提的是,在特定位置插入或者删除时,程序需要进行一种称之为连锁更新的操作以维持Ziplist结构的性质。以上操作的具体代码实现,可以参考Redis源码,此处不再赘述。

二. Server
这部分主要是从服务端,分析Redis响应客户请求的原理。

1.基础知识
Linux下有“一切皆文件”的思想。我们可以将socket、pipe、硬件等资源都看作文件,利用操作文件的方式对其进行操作。具体到网络通信中,服务端与客户端的数据交换可以看作是对文件的读写操作。

例如:服务器端首先监听端口,具体实现就是新建一个监听文件描述符,当这个监听文件可读时,就是有新的客户端请求连接。

       服务端在接受一个新的连接请求时,也会新建一个文件描述符,这个描述符即代表这个网络连接。通过对这个文件的读写,即可实现与客户端的通信。

正常情况下,一个服务端通常要同时服务多个客户,服务端处理客户请求的主逻辑如下:

while(1){
    //等待网络事件发生
    //根据每个事件,调用相关的回调函数。
}

注:客户端向服务端发送数据时,网卡首先接收到数据,之后向操作系统提出中断请求,操作系统将通知服务进程处理这些数据。

2.IO多路复用
《UNIX网络编程》中提到,IO模型可以分为5种:同步阻塞、同步非阻塞、IO复用、信号驱动以及异步非阻塞。阻塞、非阻塞是针对调用方而言的。同步、异步是针对被调用方而言的。本节,我们重点介绍IO多路复用。

IO复用:顾名思义,就是多个IO复用一个端口。常见的IO多路复用的方案有:select, poll, kqueue, epoll等。

(1) Select简介
select 的函数原型如下:

int select(int maxfdAdd1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);
 
//maxfdAdd1, 所要监听的最大文件描述符加1
 
//readset, writeset, exceptset, 需要监听读、写、错误的文件描述符
fd_set 可能是由数组,或者是由二进制位构成的数组。这个取决于操作系统的具体实现。
 
//timeout, 超时时间
struct timeval{
    long tv_sec; //秒
    long tv_usec; //微妙
};

函数返回值为发生事件的FD总数。
select的过程可以简述如下:应用程序首先调用select函数,通过入口参数将所要监听的文件描述符传递给操作系统,操作系统负责具体监听这些文件描述符,当有事件发生时,通知进程处理。

(2) Epoll简介
对于每个文件描述符,epoll可以设置2种事件触发方式,水平触发与边沿触发。

水平触发:顾名思义,就是当FD有事件时,会一直通知进程。例如:当文件可读时,操作系统告诉进程,文件可读,进程处理之后,如果由于时间原因,无法读取全部数据。此时,操作系统会仍然告诉进程FD可读。

边沿触发:所谓边沿,就是指事物状态发生改变。边沿触发,就是仅当FD状态发生改变时,通知进程。如果操作系统通知进程之后,进程并没有全部读取数据,操作系统仅会在FD状态发生改变时(例如:新数据到来),才会通知进程去处理。

//函数原型
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, strcut epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *event, int maxEvent, int timeout);
//epfd本身会占用一个文件描述符,不用时应将其关闭。
 
typedef union epoll_data { 
    void *ptr; 
    int fd; 
    __uint32_t u32; 
    __uint64_t u64; 
} epoll_data_t; 
   
struct epoll_event { 
    __uint32_t events; /* Epoll events */ 
    epoll_data_t data; /* User data variable */ 
}; 
 
 
//简单的使用用例,限于篇幅,本部分只列出其中的关键语句。
int epfd = epoll_create(size);
//调用epoll_ctl(epfd, op, fd, *events), 增加或者减少所要监听的Fd
while(1){
    int num = epoll_wait(epfd, *events, maxEvent, timeout); //events 记录所有发生事件的FD
    for(int i = 0; i < num; ++i){
        //根据每个FD,调用对应的事件处理函数
    }
}

Epoll和Select相比,效率更高,这主要由于以下几个原因:

Select每次调用,都需要将所有文件描述符从用户态复制到内核态,Epoll可以通过epoll_ctl,每个文件描述符只需要复制一次。
对select而言,操作系统需要遍历所有文件,从而找出发生事件的文件描述符。操作系统为每个epoll维护了一个双向链表,当某个文件可读或者可写时,通过回调事先设定的回调函数,将文件描述符写入这个双向链表。操作系统每次只需要查看这个链表,即可知道是否有事件发生。
Select返回时,需要程序遍历所有监听的文件描述符,从而找出发生事件的文件。Epoll可以直接得到发生事件的文件描述符(epoll_wait函数,结果会被写入events)。

  1. Redis

Redis 是作为服务端向客户提供服务的。网络编程的基本模型有单进程单线程、单进程多线程、多进程单线程以及多进程多线程。Redis在提供服务时,是基于单进程单线程的模型,通过IO复用技术,实现高并发。Redis源码4.0.9版,已经实现了select, kqueue, epoll以适应不同的需求(redis-4.0.9/src/ae_select.c, redis-4.0.9/src/ae_kqueue.c, redis-4.0.9/src/ae_epoll.c)。

(1) 关键数据结构
aeEventLoop为Reids最重要的数据结构之一,代表服务进程的事件处理循环。

/* 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;
    time_t lastTime;     /* Used to detect system clock skew */
    aeFileEvent *events; /* Registered events */
    aeFiredEvent *fired; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
    aeBeforeSleepProc *aftersleep;
} aeEventLoop;

aeFileEvent封装一般的文件事件

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

aeTimeEvent用来封装定时器事件

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *next;
} aeTimeEvent;

记录已经发生事件的文件描述符集合

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

(2)Reis服务端简要流程
Reis服务启动的入口为Server.c中的main函数,在main函数中,会进行一些数据初始化、配置初始化、设置回调函数等工作。之后Redis会进入事件主循环(aeMain函数),等待事件发生,处理事件。


AI及LNMPRG研究
7.2k 声望12.8k 粉丝

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人