IO多路複用和多執行緒會影響Redis分散式鎖嗎?

LinkinStar發表於2023-03-18

前言

前置知識

  • Redis 雖然是單執行緒的,但是它利用了核心的 IO 多路複用,從而能同時監聽多個連線
  • Redis6 出現了可以利用多個 IO 執行緒併發進行的操作

那麼問題來了,這兩者會導致我們的分散式鎖的原子性有影響嗎?

我們知道當我們使用 redis 作為分散式鎖的時候,通常會使用 SET key value EX 10 NX 命令來加鎖,獲得鎖的客戶端才能成功 SET 這個 key,那麼問題來了,這條命令在多執行緒的情況下是一個原子操作嗎?

其實答案是顯而易見的,因為 redis 的設計者肯定考慮到了向前相容的問題,並且也不會讓這樣的特性消失,所以在問這個問題以前,我雖然不能肯定,但是還是能自信的回答,但沒有足夠的底氣。 今天的目標就是找到真正的原因。

問題的兩個方面

上鎖

上鎖,沒啥多說的直接 SET key value EX 10 NX 就可以了

解鎖

解鎖,有兩種:

  • 一種是客戶端自行保證鎖只有自己拿自己解,那麼直接讓自己去 DEL 就可以了
  • 另一種是不信任客戶端,那麼可以使用 lua 指令碼,先透過 get 確定對應 key 的值是否正確,如果正確再 del,整個 lua 指令碼透過 EVAL 執行

只要上鎖和解鎖操作都能保證,就能解決問題。

執行命令的過程

那麼問題的關鍵就是命令的執行過程,Redis 執行命令也是需要有過程的,客戶端一個命令過來,不會直接就啪的執行了,而是有很多前置條件和步驟。

大致可分為:

  1. 讀取
  2. 解析
  3. 執行
  4. 返回

其中,命令讀取和解析顯然是不會影響資料的,所以當然多執行緒執行也沒有問題。最關鍵的步驟也就是執行了。

IO 多路複用

先來看看 IO 多路複用會有影響嗎?

程式碼來自: https://github.com/redis/redis/blob/074e28a46eb2646ab33002731fac6b4fc223b0bb/src/ae_epoll.c#L109

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    } else if (retval == -1 && errno != EINTR) {
        panic("aeApiPoll: epoll_wait, %s", strerror(errno));
    }

    return numevents;
}

沒事,不要擔心看不懂,只要抓住最關鍵的地方 epoll_wait 這個我們很熟悉對吧,我們就可以看到這裡一次迴圈拿出了一組 events,這些事件都是一股腦兒過來的。

其實 IO 多路複用本身沒有問題,無論是 select 還是 epoll 只是將所有的 socket 的 fd 做了一個集合而已,而告訴你那些 fd 出現了事件,讓你具體去處理。如果你不願意多執行緒處理這些讀寫事件,那麼 IO 多路複用是不會逼你的。

多執行緒

多執行緒倒是真的有可能會出問題。那如果我們自己去考慮實現的話,當一個命令被多執行緒去同時執行,那勢必會有競爭,所以我們為了儘可能利用多執行緒去加速,也只能加速,命令接收/解析/返回執行結果的部分。故,其實 Redis 的設計者也只是將多執行緒運用到了執行命令的前後。

程式碼在: https://github.com/redis/redis/blob/4ba47d2d2163ea77aacc9f719db91af2d7298905/src/networking.c#L2465

int processInputBuffer(client *c) {
    /* Keep processing while there is something in the input buffer */
    while(c->qb_pos < sdslen(c->querybuf)) {
        /* Immediately abort if the client is in the middle of something. */
        if (c->flags & CLIENT_BLOCKED) break;

        /* Don't process more buffers from clients that have already pending
         * commands to execute in c->argv. */
        if (c->flags & CLIENT_PENDING_COMMAND) break;

        /* Don't process input from the master while there is a busy script
         * condition on the slave. We want just to accumulate the replication
         * stream (instead of replying -BUSY like we do with other clients) and
         * later resume the processing. */
        if (isInsideYieldingLongCommand() && c->flags & CLIENT_MASTER) break;

        /* CLIENT_CLOSE_AFTER_REPLY closes the connection once the reply is
         * written to the client. Make sure to not let the reply grow after
         * this flag has been set (i.e. don't process more commands).
         *
         * The same applies for clients we want to terminate ASAP. */
        if (c->flags & (CLIENT_CLOSE_AFTER_REPLY|CLIENT_CLOSE_ASAP)) break;

        /* Determine request type when unknown. */
        if (!c->reqtype) {
            if (c->querybuf[c->qb_pos] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }

        if (c->reqtype == PROTO_REQ_INLINE) {
            if (processInlineBuffer(c) != C_OK) break;
        } else if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break;
        } else {
            serverPanic("Unknown request type");
        }

        /* Multibulk processing could see a <= 0 length. */
        if (c->argc == 0) {
            resetClient(c);
        } else {
            /* If we are in the context of an I/O thread, we can't really
             * execute the command here. All we can do is to flag the client
             * as one that needs to process the command. */
            if (io_threads_op != IO_THREADS_OP_IDLE) {
                serverAssert(io_threads_op == IO_THREADS_OP_READ);
                c->flags |= CLIENT_PENDING_COMMAND;
                break;
            }

            /* We are finally ready to execute the command. */
            if (processCommandAndResetClient(c) == C_ERR) {
                /* If the client is no longer valid, we avoid exiting this
                 * loop and trimming the client buffer later. So we return
                 * ASAP in that case. */
                return C_ERR;
            }
        }
    }

同樣的,也不用慌,抓住重點的部分

  • 當出現 CLIENT_PENDING_COMMAND 狀態的時候是直接 break 的,後面就根本不處理,而這個狀態就是表示客戶端當前正在等待執行的命令。在這個狀態下,客戶端不能傳送其他命令,直到當前命令的執行結果返回。
  • 最終執行命令是在 processCommandAndResetClient 方法

總結

總結一下,IO 多路複用本身其實沒有影響,而 Redis 真正執行命令的前後利用多執行緒來加速,加速命令的讀取和解析,加速將執行結果返回客戶端。所以,本質上 “IO多路複用和多執行緒會影響Redis分散式鎖嗎?” 而這個問題與分散式鎖其實沒有必然聯絡,分散式鎖本質其實也是執行一條命令。故,其實面試官問這個問題的原因更多的是關心你對 IO 多路複用和多執行緒在 Redis 實踐的理解。

相關文章