redis消息处理--多线程IO

简介

redis版本:6.0+

一、关键点

1.事件驱动架构图,从下往上分别是aeFileEvent, connection, client。
2.消息处理流程,主要有:a、主线程取得触发了读写操作的fd。b、多IO线程同步读取fd中的数据并解析数据。c、主线程处理各事件解析出来的协议请求。d、将前一步写入缓冲区的数据多IO线程发送出去。
3.多线程IO读写数据的实现。
4.其它,包括读写缓冲区的复用。

二、事件驱动架构

2.1 类图

从底向上分别是aeFileEvent,connection,clinet。


事件驱动类图
2.2 从读事件的处理过程看调用关系。
读事件处理流程

从堆栈中可以看出,调用的过程是fd->aeFileEvent->connection->client。

2.3 aeFileEvent的定义
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc; // 读事件处理函数 由上面堆栈可知绑定了connSocketEventHandler函数
    aeFileProc *wfileProc; // 写事件处理函数 通常为null
    void *clientData; // 指向connection的指针
} aeFileEvent

通过epoll_wait可以获取触发了读写事件的fd,如何通过fd找到对应的aeFileEvent呢?

// 首先所有的aeFileEvent都存储在aeEventLoop中 删减了部分字段
    int maxfd;   /* 当前最大的文件描述符id */
    int setsize; /* events数组的大小 */
    aeFileEvent *events; /* 注册的所有事件 */
    aeFiredEvent *fired; /* 触发的IO事件*/
} aeEventLoop;

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd]; // 把fd当作events数组的下标

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc; // 绑定读处理函数
    if (mask & AE_WRITABLE) fe->wfileProc = proc; // 绑定写处理函数
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

由上面可知,fd索引对应事件的方式是把fd当作aeEventLoop->events数组的下标来进行索引。之所以可以这么做是因为fd由操作系统分配,在进程中都是从0开始递增。

2.4 connection的定义

connection主要是绑定event的处理函数,以及对应的client和fd。

struct connection {
    ConnectionType *type; // 绑定的其它的处理函数
    ConnectionState state;  // 连接状态
    void *private_data;  // 指向client的指针
    ConnectionCallbackFunc conn_handler; // 连接处理函数
    ConnectionCallbackFunc write_handler; // 写处理函数
    ConnectionCallbackFunc read_handler; // 读处理函数
    int fd; // 绑定的文件描述符
};
2.5.client的定义

client主要是客户端连接需要的缓存数据,包含读写缓冲区,clientid等。节选部分字段,定义如下:

typedef struct client {
    uint64_t id;            /* client_id. */
    connection *conn; // 绑定的connection
    sds querybuf;           /* 读缓冲区. */
    size_t qb_pos;    // 读当前读缓冲已读取数据的长度
    int argc;               /* 协议解析后当前参数的数量. */
    robj **argv;   // 协议解析后保存参数的数组
    char buf[PROTO_REPLY_CHUNK_BYTES] // 固定长度的写缓冲区
    int bufpos; // 当前缓冲区已写数据的长度
    list *reply;  // 当固定缓冲区放不下所有的写数据时 将数据写入该list中
} client;

这里提一下client保存和索引的方式,client通过id存储在radix tree中。

三、消息处理的流程--多线程

3.1.1 总的处理流程
消息处理时序图

第一个被框起来的部分是多线程读取数据并解析数据.
第二个被框起来的部分是多线程将数据写回给Client.
接下来以命令:set test_key test_value来断点绘出各处理过程的堆栈。
多线程IO需要重点关注beforeSleep这个函数,该函数会先多线程读取数据并解析,然后主线程处理所有命令,最后将所有数据写回客户端。

3.1.2 开启redis的多线程IO

redis开启多线程IO

3.2 触发读事件的client加入待处理队列

堆栈图如下,在postponeClientRead中将client加入server.clients_pending_read中。


处理读事件
int postponeClientRead(client *c) {
   if (io_threads_active &&
       server.io_threads_do_reads &&
       !ProcessingEventsWhileBlocked &&
       !(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
   {
       c->flags |= CLIENT_PENDING_READ; // 一个client只会被加入list一次
       listAddNodeHead(server.clients_pending_read,c); // 将client加入待处理读请求队列
       return 1;
   } else {
       return 0;
   }
}
3.3 读取数据并解析

在beforeSleep中,取出待处理的事件,先读数据,再尝试解析协议。


多线程模式下处理读事件堆栈

在这里重点关注2个函数,readQueryFromClient和processInputBuffer。

void readQueryFromClient(connection *conn) {
    client *c = connGetPrivateData(conn);
    // ......
    nread = connRead(c->conn, c->querybuf+qblen, readlen); // 读取数据
    // ......
    processInputBuffer(c); // 解析读取的数据
}

void processInputBuffer(client *c) {
        // ......
        if (c->reqtype == PROTO_REQ_INLINE) { // 如果是内联请求
            if (processInlineBuffer(c) != C_OK) break;
            /* If the Gopher mode and we got zero or one argument, process
             * the request in Gopher mode. */
            if (server.gopher_enabled &&
                ((c->argc == 1 && ((char*)(c->argv[0]->ptr))[0] == '/') ||
                  c->argc == 0))
            {
                processGopherRequest(c);
                resetClient(c);
                c->flags |= CLIENT_CLOSE_AFTER_REPLY;
                break;
            }
        } else if (c->reqtype == PROTO_REQ_MULTIBULK) { // 如果是数组请求
            if (processMultibulkBuffer(c) != C_OK) break;
        } else {
            serverPanic("Unknown request type");
        }
}

这里留1个问题,当读取的数据不足以组成一个完整命令时,如何处理?

3.4 处理命令

命令处理的流程主要是将从client中获取命令名称并根据名称找到处理函数。


多线程模式下Set命令的堆栈
3.5 数据发送
多线程模式下数据发送堆栈图

四、多线程IO处理的实现

4.1 IO线程的实现
void *IOThreadMain(void *myid) { 
    while(1) {
        /* Wait for start */
        for (int j = 0; j < 1000000; j++) {
            if (io_threads_pending[id] != 0) break; // 多线程模式开启但又还没有数据待处理 陷入循环  当有事件待处理时 退出去执行事件
        }

        /* Give the main thread a chance to stop this thread. */
        if (io_threads_pending[id] == 0) { // 当没有数据可处理时走到这里
            pthread_mutex_lock(&io_threads_mutex[id]);// 当多线程模式关闭时 通过锁停止线程
            pthread_mutex_unlock(&io_threads_mutex[id]); // 当多线程模式开启时 获得锁 并立即释放
            continue; 
        }

        listIter li;
        listNode *ln;
        listRewind(io_threads_list[id],&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (io_threads_op == IO_THREADS_OP_WRITE) { // 通过全局的io_threads_op来区分是读操作还是写操作
                writeToClient(c,0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(c->conn);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }
        // 所有事件读处理完成 清空待处理事件 会走到锁逻辑中
        listEmpty(io_threads_list[id]);
        io_threads_pending[id] = 0;
    }
}

// 开启多线程模式时 释放锁 以便让IO线程继续执行
void startThreadedIO(void) {
    for (int j = 1; j < server.io_threads_num; j++)
        pthread_mutex_unlock(&io_threads_mutex[j]);
    io_threads_active = 1;
}

// 关闭多线程模式时 获取锁 以便让IO线程停在锁逻辑上
void stopThreadedIO(void) {
    for (int j = 1; j < server.io_threads_num; j++)
        pthread_mutex_lock(&io_threads_mutex[j]);
    io_threads_active = 0;
}

通过上面的源码不难发现以下几点 :

  1. 主线程和IO线程的同步通过io_threads_mutext[id]来实现, 每个线程都有一把锁, 当关闭多线程模式时, IO线程停留在获取锁上面.
  2. 主线程和IO线程的事件数据同步通过io_threads_list[id]和io_threads_pending[i]来实现, 前者存放事件相关的数据, 后者存放待处理IO事件的数量.
  3. IO线程既处理读事件, 也处理写事件, 通过全局变量io_threads_op来区分当前是读操作还是写操作.
  4. io_threads_active的值代表多线程模式是否开启
    PS: 在上面的开启多线程IO文章中提到了, 只有待处理的写事件超过IO线程数的2倍时才会开启多线程IO. 所以大部分情况下, 可以认为IO线程卡在获取锁逻辑上.
4.2 并发处理请求
int handleClientsWithPendingReadsUsingThreads(void) {
    if (!io_threads_active || !server.io_threads_do_reads) return 0; // 多线程模式开启且开启多线程读
    int processed = listLength(server.clients_pending_read);
    if (processed == 0) return 0;

    /* 通过轮询的方式将所有事件分发给包括主线程在内的所有的IO线程 */
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_read,&li);
    int item_id = 0;
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        int target_id = item_id % server.io_threads_num;
        listAddNodeTail(io_threads_list[target_id],c);
        item_id++;
    }

    // 设置每个IO线程需要处理事件的数量(没有主线程
    io_threads_op = IO_THREADS_OP_READ; // 设置全局变量 标识当前在处理读事件
    for (int j = 1; j < server.io_threads_num; j++) {
        int count = listLength(io_threads_list[j]);
        io_threads_pending[j] = count;
    }

    /* 主线程同样需要处理读事件 */
    listRewind(io_threads_list[0],&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        readQueryFromClient(c->conn);
    }
    listEmpty(io_threads_list[0]);

    /* 等待所有IO事件处理完成 */
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += io_threads_pending[j];
        if (pending == 0) break;
    }

    /* Run the list of clients again to process the new buffers. */
    while(listLength(server.clients_pending_read)) { 
        ln = listFirst(server.clients_pending_read);
        client *c = listNodeValue(ln);
        c->flags &= ~CLIENT_PENDING_READ;
        listDelNode(server.clients_pending_read,ln);

        if (c->flags & CLIENT_PENDING_COMMAND) {
            c->flags &= ~CLIENT_PENDING_COMMAND;
            if (processCommandAndResetClient(c) == C_ERR) { // 在这里 主线程会处理所有的命令
                /* If the client is no longer valid, we avoid
                 * processing the client later. So we just go
                 * to the next. */
                continue;
            }
        }
        processInputBuffer(c); // 这里是因为该写事件包含了多个待处理命令 但一次只能解析一个 所以还要再尝试解析缓冲区 解析下一个命令  待确认???
    }
    return processed;
}

通过上面的源码可以发现:

  1. 主线程会把待处理事件push到io_threads_list中, 然后设置待处理事件的数量, 此时IO线程就会走到循环里处理读事件.
  2. io_threads_op会在让IO线程处理IO线程前设置为IO_THREADS_OP_READ.
  3. 主线程同样会处理读事件.
4.3 并发将数据写回
int handleClientsWithPendingWritesUsingThreads(void) {
    int processed = listLength(server.clients_pending_write);
    if (processed == 0) return 0; /* Return ASAP if there are no clients. */

    // 尝试关闭IO多线程处理模式
    if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
        return handleClientsWithPendingWrites();
    }

    /* 尝试开启IO多线程模式 */
    if (!io_threads_active) startThreadedIO();

    // 将IO写事件分发到各IO线程
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_write,&li);
    int item_id = 0;
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags &= ~CLIENT_PENDING_WRITE;
        int target_id = item_id % server.io_threads_num;
        listAddNodeTail(io_threads_list[target_id],c);
        item_id++;
    }

    // 设置全局的操作模式 并且设置待处理的事件数量 让IO线程开始运行
    io_threads_op = IO_THREADS_OP_WRITE;
    for (int j = 1; j < server.io_threads_num; j++) {
        int count = listLength(io_threads_list[j]);
        io_threads_pending[j] = count;
    }

    /* 主线程也要处理写事件 */
    listRewind(io_threads_list[0],&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        writeToClient(c,0);
    }
    listEmpty(io_threads_list[0]);

    /* 通过io_threads_pending[id] 判断IO线程是否完成写操作 */
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += io_threads_pending[j];
        if (pending == 0) break;
    }

    // 为什么这里还要再尝试写数据 前面数据没写完???
    listRewind(server.clients_pending_write,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);

        if (clientHasPendingReplies(c) &&
                connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
        {
            freeClientAsync(c);
        }
    }
    listEmpty(server.clients_pending_write);
    return processed;
}

写流程的处理和读流程的处理相似, 这里不再赘述.

五 单线程消息处理流程

5.1 消息处理的堆栈图

流程比较简单,从rfileProc一路回调到readQueryFromClient最后找到对应命令处理函数。


单线程读请求处理

总结

  1. 通过锁来开启和关闭来控制IO线程的运行. 锁的开启和关闭和多线程模式同步.
  2. 多线程IO就是在主线程处理IO事件前, 将任务分配给各个IO线程, 所有线程处理都完成后, 再由主线程统一处理所有命令.
  3. 命令的处理依然是单线程的, 因此所有存储数据结构的访问都不需要加锁.
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270