【面试题】一文讲清,为啥redis单线程还有很高的性能?

面试的时候如果聊到缓存,肯定会聊到redis,因为它现在是缓存事实上的标准。

早些年一些互联网公司会用到memcached作为缓存,它是多线程的,用c语言开发,不过现在基本很少了,有兴趣的同学可以学习下它的源码。

那么聊redis,第一个问题就是它的工作原理,redis最最重要的工作原理,就是它的线程模型。

redis是单线程、nio、异步的线程模型,你要是这个都不知道,工作中用到redis,出了问题都不知道如何入手排查。

学习redis线程模型之前,必须了解socket网络编程相关知识,如果你不懂socket,肯定搞不懂redis原理。

redis使用文件事件处理器(file event handler)处理所有事件,这个文件事件处理器是单线程的,所以redis才叫单线程的。

文件事件处理器采用IO多路复用器,同时监听多个socket,将产生事件的socket放入内存队列中,事件分发器根据socket上的事件类型,选择对应的事件处理器进行处理。

图1  redis 文件事件处理器过程

文件事件处理器的结构包含 4 个部分:

  • 客户端socket
  • IO 多路复用器
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个客户端socket可能会并发产生不同的操作,每个操作对应不同的事件,但是 IO 多路复用器会监听多个socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。

连接应答处理器

在redis server启动的时候,会设置客户端建立连接时的连接应答处理器acceptTcpHandler,并和服务器监听套接字的AE_READABLE 事件关联起来,具体代码在server.c的initServer方法中:

图2 redis初始化设置连接应答处理器

在networking.c 文件中acceptTcpHandler方法实现中,redis的连接应答处理器,用于对连接服务器进行监听,对套接字的客户端进行应答相应。

#define MAX_ACCEPTS_PER_CALL 1000
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);
  //循环处理连接应答
    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_WARNING,
                    'Accepting client connection: %s', server.neterr);
            return;
        }
        serverLog(LL_VERBOSE,'Accepted %s:%d', cip, cport);
        acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
    }
}

连接应答处理器

在接收到客户端的连接后,触发链接应答处理器的acceptTcpHandler方法,这个方法里会创建客户端对于的client对象,它代表着连接到 Redis 客户端。

并且accept客户端的连接,然后把这个连接与命令处理器readQueryFromClient关联起来,服务器会将连接成功后的socket的AE_READABLE事件和命令请求处理器关联起来,命令请求处理器就可以从socket中读取数据了。

图3 建立连接后设置命令请求处理器

通过 networking.c中的readQueryFromClient方法,读取客户端发送的命令内容。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {    client *c = (client*) privdata;    int nread, readlen;    size_t qblen;    UNUSED(el);    UNUSED(mask);

    readlen = PROTO_IOBUF_LEN;    /* If this is a multi bulk request, and we are processing a bulk reply     * that is large enough, try to maximize the probability that the query     * buffer contains exactly the SDS string representing the object, even     * at the risk of requiring more read(2) calls. This way the function     * processMultiBulkBuffer() can avoid copying buffers to create the     * Redis Object representing the argument. */    if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1        && c->bulklen >= PROTO_MBULK_BIG_ARG)    {        ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf);

        /* Note that the 'remaining' variable may be zero in some edge case,         * for example once we resume a blocked client after CLIENT PAUSE. */        if (remaining > 0 && remaining < readlen) readlen = remaining;    }

    qblen = sdslen(c->querybuf);    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);    nread = read(fd, c->querybuf+qblen, readlen);    if (nread == -1) {        if (errno == EAGAIN) {            return;        } else {            serverLog(LL_VERBOSE, 'Reading from client: %s',strerror(errno));            freeClient(c);            return;        }    } else if (nread == 0) {        serverLog(LL_VERBOSE, 'Client closed connection');        freeClient(c);        return;    } else if (c->flags & CLIENT_MASTER) {        /* Append the query buffer to the pending (not applied) buffer         * of the master. We'll use this buffer later in order to have a         * copy of the string applied by the last command executed. */        c->pending_querybuf = sdscatlen(c->pending_querybuf,                                        c->querybuf+qblen,nread);    }

    sdsIncrLen(c->querybuf,nread);    c->lastinteraction = server.unixtime;    if (c->flags & CLIENT_MASTER) c->read_reploff += nread;    server.stat_net_input_bytes += nread;    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();

        bytes = sdscatrepr(bytes,c->querybuf,64);        serverLog(LL_WARNING,'Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)', ci, bytes);        sdsfree(ci);        sdsfree(bytes);        freeClient(c);        return;    }

    /* Time to process the buffer. If the client is a master we need to     * compute the difference between the applied offset before and after     * processing the buffer, to understand how much of the replication stream     * was actually applied to the master state: this quantity, and its     * corresponding part of the replication stream, will be propagated to     * the sub-slaves and to the replication backlog. */    processInputBufferAndReplicate(c);}

void processInputBufferAndReplicate(client *c) {    if (!(c->flags & CLIENT_MASTER)) {        processInputBuffer(c);    } else {        size_t prev_offset = c->reploff;        processInputBuffer(c);        size_t applied = c->reploff - prev_offset;        if (applied) {            replicationFeedSlavesFromMasterStream(server.slaves,                    c->pending_querybuf, applied);            sdsrange(c->pending_querybuf,applied,-1);        }    }}

processInputBuffer主要是将输入缓冲区中的数据解析成对应的命令,根据命令类型是 PROTO_REQ_MULTIBULK 还是 PROTO_REQ_INLINE,来分别调用 processInlineBuffer 和 processMultibulkBuffer 方法来解析命令。

然后调用 processCommand 方法来执行命令,redis中有一个类似map的东西,会记录每条命令对应的handler,根据解析出来的命令执行对应的handler即可。

命令回复处理器

命令处理完之后,redis server就要给客户端返回处理结果。networking.c中的sendReplyToClient方法就是redis 的命令回复处理器,此时会将socket的AE_WRITEABLE事件和命令回复处理器关联起来。这个处理器负责将服务器执行命令后得到的命令回复先写到缓存,再通过套接字返回给客户端。

图4 设置命令回复处理器

最后画一幅完整的流程图,方便大家理解记忆。

图5 redis线程模型
(0)

相关推荐

  • Redis主从结构主节点执行写入后wait命令对性能的影响

    这里的Redis主从结构可以是简单的主从,sentinel,redis cluster中的主从等. wait命令的作用: 此命令将阻塞当前客户端,直到当前Session连接(主节点上)所有的写命令都被 ...

  • 浅谈秒杀系统中的库存控制

    我在面试别人的时候,经常会问对方,如何设计一个秒杀系统?回答的好的同学并不多,这里我简要说一下考察这个问题的目的.秒杀系统,那么顾名思义就是抢购,库存有限情况下的竞争问题,其实就是一个高并发的处理. ...

  • redis概述

    缓存是如何实现高性能的 当第一次查询的时候,缓存里面没有数据,回去数据库查,然后存入缓存,当再去查询的时候,或者是查询相同数据的时候,就不用去数据库里面查了,直接去缓存里面找,大大提高了效率,如果这个 ...

  • Redis面试题汇总(附答案),面试突击专用

    本文的面试题如下: Redis 持久化机制 缓存雪崩.缓存穿透.缓存预热.缓存更新.缓存降级等问题 热点数据和冷数据是什么 Memcache与Redis的区别都有哪些? 单线程的redis为什么这么快 ...

  • 一喝水就想尿尿是肾虚?喝水很多,为啥口还干?5个喝水小问题,一文讲清

    喝水不仅是个健康的好习惯,还是人体得以正常运行的前提. 如果一个人不吃饭只喝水,依靠自己体内贮存的营养物质或消耗自体组织,可以存活一个月:但是如果不喝水,连一周时间也很难挨过. 然而,喝水虽然是件小事 ...

  • 2021年高考50大热门专业一文讲清!附就业难易程度分析

    我们对目前比较热门的50个专业,分五个大类,即工学类专业.经济管理类专业.理学类专业.法学类专业.文学类专业,从学习内容.就业方向两大方面,逐一详细说明,并针对就业.行业待遇.行业前景三项,用直观的星 ...

  • 一文讲清:防暑降温费全套账务税务处理(个税、社保、所得税)

    夏天即至,关于企业发生的防暑降温费,本文讲讲关于其账务处理以及企业所得税.个人所得税.社保方面的相关政策. 一.防暑降温费属于职工福利 政策依据: 国税函[2009]3号第三条"关于职工福利 ...

  • 56岁大叔心率55次每分钟正常吗?心率与心律有啥区别?一文讲清

    "医生,我的心率一分钟只有55次,正常吗?这个心率和那个心律有啥区别呀?"56岁的李叔很着急地问医生. 李叔是一名高血压患者,自确诊高血压之后,他就特别重视自己的健康,专门让儿子买 ...

  • 草甘膦和草铵膦,一字之差却大不同,一文讲清两者区别

    在农业生产中,化肥.农药.除草剂可谓是现代农业的三件法宝,为农作物的增产增收起到了关键的作用. 特别是除草剂,把农民朋友从传统的人工拔草中解放出来.在以往没有除草剂的时候,农田的杂草全靠人工拔除,一个 ...

  • 几种情况下,不建议宝宝光着脚丫,一文讲清!

    闺蜜嫁到了黑龙江,前两天她发信息说今年东北的春天特别冷,出现了"倒春寒"的现象.这个季节已经停止供暖了,楼里的天气比外面都要冷.家里的地板比夏天还会凉,宝宝还和原来一样喜欢光着脚丫 ...

  • 甲癌淋巴结转移,全切or半切?单侧or双侧颈清?术后碘-131?一文讲清

    我们都知道,分化型甲状腺癌包括乳头状癌和滤泡状癌,属于分化程度高,即便是发生转移,经过规范化综合治理后,绝大多数预后都很好. 其中,最常见的转移方式是--淋巴结转移. 那么,发生淋巴结转移后该怎么办? ...

  • 一文讲清中药通便药

    在上一次关于酚酞的话题中,我们指出,酚酞撤市的原因,除了潜在致癌风险之外,最重要的,可能还是长期用药形成的药物依赖对人体的持续损害,这是产生质变前的量变.而这种药物依赖,除了酚酞之外,很多其他通便药也 ...

  • 一文讲清土地征收成片开发

    部门简介: 土地利用事业部,负责土地利用线条相关产品涉及项目的实施,孵化并扶持省内.省外区域产品,完成自然资源利用新项目的研究和产品的转化工作.主要业务范围包含用地预审.规划选址评估.踏勘论证.占用永 ...