前言
前置常识
- 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 实际的了解。