前言
前置知識
- 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 執行命令也是需要有過程的,客戶端一個命令過來,不會直接就啪的執行了,而是有很多前置條件和步驟。
大致可分為:
- 讀取
- 解析
- 執行
- 返回
其中,命令讀取和解析顯然是不會影響資料的,所以當然多執行緒執行也沒有問題。最關鍵的步驟也就是執行了。
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 實踐的理解。