引言
Redis 的 List 类型凭借 LPUSH / RPUSH 与 BRPOP / BLPOP 的组合,成为轻量级消息队列的首选方案。然而,生产者写入一个元素后,消费者如何被精准唤醒 ?这背后涉及命令执行、信号通知、就绪队列、阻塞客户端重放等一整套精密机制。本文基于 Redis 7.4.0 源码,以 LPUSH → 信号触发 → 调度唤醒 → BRPOP 重放 为主线,逐层剖析,让读者彻底理解阻塞队列的底层实现。
一、LPUSH:写入只是"发信号"
1.1 命令入口与通用推入
c
scss
void lpushCommand(client *c) {
pushGenericCommand(c, LIST_HEAD, 0);
}
pushGenericCommand 是 LPUSH / RPUSH / LPUSHX / RPUSHX 的通用实现,参数 where 控制头尾,xx 控制是否仅在 key 存在时推入。
关键流程:
- 通过
lookupKeyWrite获取 key 对应的值对象(robj *lobj)。 - 若 key 不存在且
xx=0,则创建新的 listpack 对象并调用dbAdd写入数据库。 - 调用
listTypeTryConversionAppend决定是否将 listpack 转换为 quicklist(当元素数量或大小超过阈值时)。 - 循环调用
listTypePush将每个元素插入 listpack 的头部或尾部,并累加server.dirty。 - 回复客户端新列表长度。
- 调用
signalModifiedKey和notifyKeyspaceEvent触发修改事件。
注意 :Redis 7.0 之后,List 底层默认使用 listpack (紧凑连续内存),仅在满足条件时才转为 quicklist(由多个 listpack 节点组成的链表),以平衡内存和性能。
1.2 信号之始:dbAdd 中的隐藏动作
当 key 首次被创建时,dbAdd 负责将键值对插入数据库字典。但它的职责不止于此:
c
scss
dictEntry *dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_existing) {
// ... 插入字典逻辑 ...
signalKeyAsReady(db, key, val->type); // 关键!
notifyKeyspaceEvent(NOTIFY_NEW, "new", key, db->id);
return de;
}
signalKeyAsReady 是唤醒阻塞客户端的"发令枪"。它最终调用 signalKeyAsReadyLogic,该函数会做以下几项检查:
- 判断该类型(List)是否可能阻塞客户端(
getBlockedTypeByType返回BLOCKED_LIST)。 - 检查全局是否有该类型的阻塞客户端(
server.blocked_clients_by_type[BLOCKED_LIST])。 - 检查该 key 是否存在于
db->blocking_keys字典中(即是否有客户端正阻塞在这个 key 上)。
若以上条件均满足,则将该 key 加入 server.ready_keys 全局链表,同时将其记录在 db->ready_keys 字典中以防止重复入队。
c
ini
/* 将 key 加入 server.ready_keys 队列 */
rl = zmalloc(sizeof(*rl));
rl->key = key;
rl->db = db;
incrRefCount(key);
listAddNodeTail(server.ready_keys, rl);
关键点 :LPUSH 本身不直接处理任何阻塞客户端,它只是向全局调度器发送一个"该 key 已就绪"的信号。这种解耦设计使得写入操作轻量且高效。
二、BRPOP:阻塞的等待艺术
2.1 命令入口与通用阻塞弹出
c
r
void brpopCommand(client *c) {
blockingPopGenericCommand(c, c->argv+1, c->argc-2, LIST_TAIL, c->argc-1, -1);
}
blockingPopGenericCommand 同时处理 BLPOP / BRPOP / BLMPOP,其核心逻辑是:
- 解析超时时间(秒)。
- 遍历所有输入 key ,一旦找到非空 的 List,立即执行弹出操作并返回结果,不进入阻塞。
- 若所有 key 都为空或不存在,则调用
blockForKeys将客户端置于阻塞状态。
2.2 注册阻塞:blockForKeys
blockForKeys 负责将客户端与阻塞的 keys 关联起来,并注册到数据库的等待结构中:
c
scss
void blockForKeys(client *c, int btype, robj **keys, int numkeys,
mstime_t timeout, int unblock_on_nokey) {
for (j = 0; j < numkeys; j++) {
/* 记录客户端阻塞在哪些 key 上(c->bstate.keys) */
dictAddRaw(c->bstate.keys, keys[j], NULL);
incrRefCount(keys[j]);
/* 将客户端加入 db->blocking_keys[key] 的等待列表 */
de = dictAddRaw(c->db->blocking_keys, keys[j], &existing);
if (de) { // 首次有客户端阻塞在该 key
l = listCreate();
dictSetVal(c->db->blocking_keys, de, l);
incrRefCount(keys[j]);
} else {
l = dictGetVal(existing);
}
listAddNodeTail(l, c); // 加入队列尾部(FIFO)
}
c->bstate.unblock_on_nokey = unblock_on_nokey;
c->flags |= CLIENT_PENDING_COMMAND; // 标记需要重放命令
blockClient(c, btype); // 设置 CLIENT_BLOCKED 标志
}
数据结构关系:
db->blocking_keys:字典,key → 阻塞在该 key 上的客户端链表(list)。c->bstate.keys:字典,客户端 → 它阻塞的所有 key(引用计数)。c->bstate.unblock_on_nokey:特殊标记(XREADGROUP 会用到,普通 BRPOP 为 0)。
阻塞后的客户端不再接受新命令,直到被唤醒或超时。
三、唤醒调度器:handleClientsBlockedOnKeys
3.1 触发时机
handleClientsBlockedOnKeys 在 每个命令执行完成之后 (processCommand 末尾)被调用,前提是 server.ready_keys 非空。它负责消费就绪队列,逐一唤醒阻塞客户端。
c
ini
void handleClientsBlockedOnKeys(void) {
static int in_handling = 0;
if (in_handling) return; // 防止递归导致公平性被破坏
in_handling = 1;
while (listLength(server.ready_keys) != 0) {
list *l = server.ready_keys;
server.ready_keys = listCreate(); // 换新队列,避免在遍历中被修改
while (listLength(l) != 0) {
ln = listFirst(l);
rl = ln->value;
dictDelete(rl->db->ready_keys, rl->key); // 从去重字典中删除
handleClientsBlockedOnKey(rl); // 处理该 key 上的所有客户端
decrRefCount(rl->key);
zfree(rl);
listDelNode(l, ln);
}
listRelease(l);
}
in_handling = 0;
}
设计亮点:
- 双队列 :将
server.ready_keys替换为新空列表,一边处理旧列表,一边允许新信号追加到新列表,避免竞争。 - 防递归 :
in_handling标志防止在唤醒过程中再次触发调度(如 BLMOVE 可能产生新的就绪信号)。
3.2 处理单个 key:handleClientsBlockedOnKey
该函数从 db->blocking_keys 获取阻塞在该 key 上的所有客户端,然后逐个尝试唤醒:
c
ini
static void handleClientsBlockedOnKey(readyList *rl) {
de = dictFind(rl->db->blocking_keys, rl->key);
if (!de) return;
clients = dictGetVal(de);
long count = listLength(clients); // 初始数量,防止无限循环
while ((ln = listNext(&li)) && count--) {
receiver = listNodeValue(ln);
robj *o = lookupKeyReadWithFlags(rl->db, rl->key, LOOKUP_NOEFFECTS);
/* 检查 key 当前是否可用且类型匹配(List) */
if (o && receiver->bstate.btype == getBlockedTypeByType(o->type)) {
unblockClientOnKey(receiver, rl->key);
}
// 注意:如果类型不匹配,该客户端不会被唤醒(留在队列中)
}
}
这里有一个重要细节:即使 key 已存在,但如果其类型不是 List(比如被 DEL 或 SET 覆盖),则 BRPOP 客户端不会 被唤醒,因为 getBlockedTypeByType 返回的类型与 BLOCKED_LIST 不匹配。只有类型正确时,才调用 unblockClientOnKey。
四、唤醒与重放:unblockClientOnKey 的使命
c
scss
static void unblockClientOnKey(client *c, robj *key) {
/* 1. 从 c->bstate.keys 中移除该 key */
de = dictFind(c->bstate.keys, key);
releaseBlockedEntry(c, de, 1); // 释放条目,减少引用
/* 2. 清除客户端的 BLOCKED 标志 */
unblockClient(c, 0);
/* 3. 若命令需要重放,则重新执行它 */
if (c->flags & CLIENT_PENDING_COMMAND) {
c->flags &= ~CLIENT_PENDING_COMMAND;
processCommandAndResetClient(c); // 重新执行原命令(BRPOP)
if (!(c->flags & CLIENT_BLOCKED)) {
queueClientForReprocessing(c); // 若执行后未被再次阻塞,则放入待处理队列
}
}
}
核心逻辑 :唤醒 BRPOP 客户端后,并非简单地将弹出的数据返回,而是重新执行整个 BRPOP 命令 。此时由于该 key 已有数据,blockingPopGenericCommand 会直接命中非空列表,执行弹出并返回结果。这种"命令重放"机制保证了命令的原子性和复制一致性,也避免了复杂的状态保存。
超时处理
若 BRPOP 等待超时,则不会进入就绪队列,而是由定时器或事件循环中的 clientsCron 调用 unblockClient 并返回空值(addReplyNullArray),同时也会清理 db->blocking_keys 中的引用。
五、数据流全景图(带关键函数调用)
text
scss
[生产者] LPUSH mylist "hello"
│
▼
lpushCommand()
└─ pushGenericCommand()
├─ lookupKeyWrite() → 若不存在则 createListListpackObject()
├─ dbAdd() ──────────────────────────────────────┐
│ └─ signalKeyAsReady() │
│ └─ signalKeyAsReadyLogic() │
│ ├─ 检查 db->blocking_keys 有无 │
│ └─ 将 key 加入 server.ready_keys │
├─ listTypePush() 写入元素 │
└─ signalModifiedKey() / notifyKeyspaceEvent() │
│
│ (命令执行完毕)
▼
[调度器] processCommand() 末尾
└─ handleClientsBlockedOnKeys()
└─ 遍历 server.ready_keys
└─ handleClientsBlockedOnKey(key)
├─ 从 db->blocking_keys 取出等待客户端列表
└─ 对每个客户端:
└─ 检查 key 类型 == LIST?
└─ unblockClientOnKey(client)
├─ releaseBlockedEntry()
├─ unblockClient() 清除 CLIENT_BLOCKED
└─ processCommandAndResetClient()
└─ 重新执行 BRPOP mylist 10
└─ blockingPopGenericCommand()
└─ 此时列表非空 → 直接弹出 "hello" 并返回
六、设计精髓与思考
- 信号与处理分离
LPUSH 只负责发信号(入队ready_keys),真正的唤醒由统一调度器在命令结束阶段完成。这避免了在写入路径中直接操作复杂的阻塞客户端列表,保证了写入操作的快速响应。 - 命令重放而非状态保存
Redis 选择在唤醒后完整重放原始阻塞命令,而不是保存中间状态。这使得代码逻辑清晰,且天然支持复制和 AOF,因为重放的命令就是原始 BRPOP 命令,复制副本会得到相同结果。 - 类型安全
唤醒时严格检查 key 的类型是否匹配客户端期望的类型(如 List),防止因 key 被意外覆盖而导致数据语义错误。 - FIFO 公平性
所有阻塞客户端按阻塞顺序排列在db->blocking_keys的链表中,唤醒时按顺序处理,保证了公平性。 - 去重优化
db->ready_keys字典确保同一 key 在一次调度周期内只入队一次,避免重复处理,提高效率。
七、结语
通过深入剖析 LPUSH 和 BRPOP 的源码,我们看到了 Redis 如何以极简而高效的方式实现阻塞队列:写入时轻量发信号,调度时统一处理,唤醒时重放命令。这套机制不仅适用于 List,也适用于 Stream、ZSet 等类型的阻塞操作(如 BZPOPMIN、XREAD)。理解这些底层逻辑,有助于我们在使用 Redis 构建消息队列时,更好地把握性能特征和异常行为,写出更健壮的应用代码。
Redis 源码的魅力,正在于这种"简单中见精巧"的设计哲学。
#源码 `
/* LPUSH ... */ void lpushCommand(client *c) { pushGenericCommand(c,LIST_HEAD,0); }
/*-----------------------------------------------------------------------------
- List Commands ----------------------------------------------------------------------------/
/* Implements LPUSH/RPUSH/LPUSHX/RPUSHX.
-
'xx': push if key exists. */ void pushGenericCommand(client *c, int where, int xx) { int j;
robj *lobj = lookupKeyWrite(c->db, c->argv1); if (checkType(c,lobj,OBJ_LIST)) return; if (!lobj) { if (xx) { addReply(c, shared.czero); return; }
inilobj = createListListpackObject(); dbAdd(c->db,c->argv[1],lobj);}
listTypeTryConversionAppend(lobj,c->argv,2,c->argc-1,NULL,NULL); for (j = 2; j < c->argc; j++) { listTypePush(lobj,c->argvj,where); server.dirty++; }
addReplyLongLong(c, listTypeLength(lobj));
char *event = (where == LIST_HEAD) ? "lpush" : "rpush"; signalModifiedKey(c,c->db,c->argv1); notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv1,c->db->id); }
dictEntry *dbAdd(redisDb *db, robj *key, robj val) { return dbAddInternal(db, key, val, 0); } / Add the key to the DB. It's up to the caller to increment the reference
- counter of the value if needed.
- If the update_if_existing argument is false, the program is aborted
- if the key already exists, otherwise, it can fall back to dbOverwrite. */ static dictEntry *dbAddInternal(redisDb *db, robj *key, robj *val, int update_if_existing) { dictEntry *existing; int slot = getKeySlot(key->ptr); dictEntry *de = kvstoreDictAddRaw(db->keys, slot, key->ptr, &existing); if (update_if_existing && existing) { dbSetValue(db, key, val, 1, existing); return existing; } serverAssertWithInfo(NULL, key, de != NULL); kvstoreDictSetKey(db->keys, slot, de, sdsdup(key->ptr)); initObjectLRUOrLFU(val); kvstoreDictSetVal(db->keys, slot, de, val); signalKeyAsReady(db, key, val->type); notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id); return de; } void signalKeyAsReady(redisDb *db, robj *key, int type) { signalKeyAsReadyLogic(db, key, type, 0); }
/* If the specified key has clients blocked waiting for list pushes, this
-
function will put the key reference into the server.ready_keys list.
-
Note that db->ready_keys is a hash table that allows us to avoid putting
-
the same key again and again in the list in case of multiple pushes
-
made by a script or in the context of MULTI/EXEC.
-
The list will be finally processed by handleClientsBlockedOnKeys() */ static void signalKeyAsReadyLogic(redisDb *db, robj *key, int type, int deleted) { readyList *rl;
/* Quick returns. / int btype = getBlockedTypeByType(type); if (btype == BLOCKED_NONE) { / The type can never block. / return; } if (!server.blocked_clients_by_typebtype && !server.blocked_clients_by_typeBLOCKED_MODULE) { / No clients block on this type. Note: Blocked modules are represented * by BLOCKED_MODULE, even if the intention is to wake up by normal * types (list, zset, stream), so we need to check that there are no * blocked modules before we do a quick return here. */ return; }
if (deleted) { /* Key deleted and no clients blocking for this key? No need to queue it. / if (dictFind(db->blocking_keys_unblock_on_nokey,key) == NULL) return; / Note: if we made it here it means the key is also present in db->blocking_keys / } else { / No clients blocking for this key? No need to queue it. */ if (dictFind(db->blocking_keys,key) == NULL) return; }
dictEntry *de, existing; de = dictAddRaw(db->ready_keys, key, &existing); if (de) { / We add the key in the db->ready_keys dictionary in order * to avoid adding it multiple times into a list with a simple O(1) * check. / incrRefCount(key); } else { / Key was already signaled? No need to queue it again. */ return; }
/* Ok, we need to queue this key into server.ready_keys. */ rl = zmalloc(sizeof(*rl)); rl->key = key; rl->db = db; incrRefCount(key); listAddNodeTail(server.ready_keys,rl); }
/* This function should be called by Redis every time a single command,
-
a MULTI/EXEC block, or a Lua script, terminated its execution after
-
being called by a client. It handles serving clients blocked in all scenarios
-
where a specific key access requires to block until that key is available.
-
All the keys with at least one client blocked that are signaled as ready
-
are accumulated into the server.ready_keys list. This function will run
-
the list and will serve clients accordingly.
-
Note that the function will iterate again and again (for example as a result of serving BLMOVE
-
we can have new blocking clients to serve because of the PUSH side of BLMOVE.)
-
This function is normally "fair", that is, it will serve clients
-
using a FIFO behavior. However this fairness is violated in certain
-
edge cases, that is, when we have clients blocked at the same time
-
in a sorted set and in a list, for the same key (a very odd thing to
-
do client side, indeed!). Because mismatching clients (blocking for
-
a different type compared to the current key type) are moved in the
-
other side of the linked list. However as long as the key starts to
-
be used only for a single type, like virtually any Redis application will
-
do, the function is already fair. */ void handleClientsBlockedOnKeys(void) {
/* In case we are already in the process of unblocking clients we should
- not make a recursive call, in order to prevent breaking fairness. */ static int in_handling_blocked_clients = 0; if (in_handling_blocked_clients) return; in_handling_blocked_clients = 1;
/* This function is called only when also_propagate is in its basic state
- (i.e. not from call(), module context, etc.) */ serverAssert(server.also_propagate.numops == 0);
/* If a command being unblocked causes another command to get unblocked,
-
like a BLMOVE would do, then the new unblocked command will get processed
-
right away rather than wait for later. */ while(listLength(server.ready_keys) != 0) { list *l;
/* Point server.ready_keys to a fresh list and save the current one
- locally. This way as we run the old list we are free to call
- signalKeyAsReady() that may push new elements in server.ready_keys
- when handling clients blocked into BLMOVE. */ l = server.ready_keys; server.ready_keys = listCreate();
while(listLength(l) != 0) { listNode *ln = listFirst(l); readyList *rl = ln->value;
scss/* First of all remove this key from db->ready_keys so that * we can safely call signalKeyAsReady() against this key. */ dictDelete(rl->db->ready_keys,rl->key); handleClientsBlockedOnKey(rl); /* Free this item. */ decrRefCount(rl->key); zfree(rl); listDelNode(l,ln);} listRelease(l); /* We have the new list on place at this point. */ } in_handling_blocked_clients = 0; }
/* Helper function for handleClientsBlockedOnKeys(). This function is called
-
whenever a key is ready. we iterate over all the clients blocked on this key
-
and try to re-execute the command (in case the key is still available). */ static void handleClientsBlockedOnKey(readyList *rl) {
/* We serve clients in the same order they blocked for
- this key, from the first blocked to the last. */ dictEntry *de = dictFind(rl->db->blocking_keys,rl->key);
if (de) { list *clients = dictGetVal(de); listNode *ln; listIter li; listRewind(clients,&li);
scss/* Avoid processing more than the initial count so that we're not stuck * in an endless loop in case the reprocessing of the command blocks again. */ long count = listLength(clients); while ((ln = listNext(&li)) && count--) { client *receiver = listNodeValue(ln); robj *o = lookupKeyReadWithFlags(rl->db, rl->key, LOOKUP_NOEFFECTS); /* 1. In case new key was added/touched we need to verify it satisfy the * blocked type, since we might process the wrong key type. * 2. We want to serve clients blocked on module keys * regardless of the object type: we don't know what the * module is trying to accomplish right now. * 3. In case of XREADGROUP call we will want to unblock on any change in object type * or in case the key was deleted, since the group is no longer valid. */ if ((o != NULL && (receiver->bstate.btype == getBlockedTypeByType(o->type))) || (o != NULL && (receiver->bstate.btype == BLOCKED_MODULE)) || (receiver->bstate.unblock_on_nokey)) { if (receiver->bstate.btype != BLOCKED_MODULE) unblockClientOnKey(receiver, rl->key); else moduleUnblockClientOnKey(receiver, rl->key); } }} }
/* Unblock a client once a specific key became available for it.
-
This function will remove the client from the list of clients blocked on this key
-
and also remove the key from the dictionary of keys this client is blocked on.
-
in case the client has a command pending it will process it immediately. */ static void unblockClientOnKey(client *c, robj *key) { dictEntry *de;
de = dictFind(c->bstate.keys, key); releaseBlockedEntry(c, de, 1);
/* Only in case of blocking API calls, we might be blocked on several keys. however we should force unblock the entire blocking keys */ serverAssert(c->bstate.btype == BLOCKED_STREAM || c->bstate.btype == BLOCKED_LIST || c->bstate.btype == BLOCKED_ZSET);
/* We need to unblock the client before calling processCommandAndResetClient
- because it checks the CLIENT_BLOCKED flag / unblockClient(c, 0); / In case this client was blocked on keys during command
- we need to re process the command again / if (c->flags & CLIENT_PENDING_COMMAND) { c->flags &= ~CLIENT_PENDING_COMMAND; / We want the command processing and the unblock handler (see RM_Call 'K' option)
- to run atomically, this is why we must enter the execution unit here before
- running the command, and exit the execution unit after calling the unblock handler (if exists).
- Notice that we also must set the current client so it will be available
- when we will try to send the client side caching notification (done on 'afterCommand'). */ client *old_client = server.current_client; server.current_client = c; enterExecutionUnit(1, 0); processCommandAndResetClient(c); if (!(c->flags & CLIENT_BLOCKED)) { if (c->flags & CLIENT_MODULE) { moduleCallCommandUnblockedHandler(c); } else { queueClientForReprocessing(c); } } exitExecutionUnit(); afterCommand(c); server.current_client = old_client; } }
/* This function calls processCommand(), but also performs a few sub tasks
-
for the client that are useful in that context:
-
- It sets the current client to the client 'c'.
-
- calls commandProcessed() if the command was handled.
-
The function returns C_ERR in case the client was freed as a side effect
-
of processing the command, otherwise C_OK is returned. */ int processCommandAndResetClient(client *c) { int deadclient = 0; client old_client = server.current_client; server.current_client = c; if (processCommand(c) == C_OK) { commandProcessed(c); / Update the client's memory to include output buffer growth following the * processed command. */ if (c->conn) updateClientMemUsageAndBucket(c); }
if (server.current_client == NULL) deadclient = 1; /*
- Restore the old client, this is needed because when a script
- times out, we will get into this code from processEventsWhileBlocked.
- Which will cause to set the server.current_client. If not restored
- we will return 1 to our caller which will falsely indicate the client
- is dead and will stop reading from its buffer. / server.current_client = old_client; / performEvictions may flush slave output buffers. This may
- result in a slave, that may be the active client, to be
- freed. */ return deadclient ? C_ERR : C_OK; }
/* If this function gets called we already read a whole
-
command, arguments are in the client argv/argc fields.
-
processCommand() execute the command or prepare the
-
server for a bulk read from the client.
-
If C_OK is returned the client is still alive and valid and
-
other operations can be performed by the caller. Otherwise
-
if C_ERR is returned the client was destroyed (i.e. after QUIT). */ int processCommand(client c) { if (!scriptIsTimedout()) { / Both EXEC and scripts call call() directly so there should be * no way in_exec or scriptIsRunning() is 1. * That is unless lua_timedout, in which case client may run * some commands. */ serverAssert(!server.in_exec); serverAssert(!scriptIsRunning()); }
/* in case we are starting to ProcessCommand and we already have a command we assume
- this is a reprocessing of this command, so we do not want to perform some of the actions again. */ int client_reprocessing_command = c->cmd ? 1 : 0;
/* only run command filter if not reprocessing command */ if (!client_reprocessing_command) { moduleCallCommandFilters(c); reqresAppendRequest(c); }
/* Handle possible security attacks. */ if (!strcasecmp(c->argv0->ptr,"host:") || !strcasecmp(c->argv0->ptr,"post")) { securityWarningCommand(c); return C_ERR; }
/* If we're inside a module blocked context yielding that wants to avoid
- processing clients, postpone the command. */ if (server.busy_module_yield_flags != BUSY_MODULE_YIELD_NONE && !(server.busy_module_yield_flags & BUSY_MODULE_YIELD_CLIENTS)) { blockPostponeClient(c); return C_OK; }
/* Now lookup the command and check ASAP about trivial error conditions
-
such as wrong arity, bad command name and so forth.
-
In case we are reprocessing a command after it was blocked,
-
we do not have to repeat the same checks */ if (!client_reprocessing_command) { c->cmd = c->lastcmd = c->realcmd = lookupCommand(c->argv,c->argc); sds err; if (!commandCheckExistence(c, &err)) { rejectCommandSds(c, err); return C_OK; } if (!commandCheckArity(c, &err)) { rejectCommandSds(c, err); return C_OK; }
/* Check if the command is marked as protected and the relevant configuration allows it */ if (c->cmd->flags & CMD_PROTECTED) { if ((c->cmd->proc == debugCommand && !allowProtectedAction(server.enable_debug_cmd, c)) || (c->cmd->proc == moduleCommand && !allowProtectedAction(server.enable_module_cmd, c))) { rejectCommandFormat(c,"%s command not allowed. If the %s option is set to "local", " "you can run it from a local connection, otherwise you need to set this option " "in the configuration file, and then restart the server.", c->cmd->proc == debugCommand ? "DEBUG" : "MODULE", c->cmd->proc == debugCommand ? "enable-debug-command" : "enable-module-command"); return C_OK;
}} }
const uint64_t cmd_flags = getCommandFlags(c);
int is_read_command = (cmd_flags & CMD_READONLY) || (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_READONLY)); int is_write_command = (cmd_flags & CMD_WRITE) || (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_WRITE)); int is_denyoom_command = (cmd_flags & CMD_DENYOOM) || (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_DENYOOM)); int is_denystale_command = !(cmd_flags & CMD_STALE) || (c->cmd->proc == execCommand && (c->mstate.cmd_inv_flags & CMD_STALE)); int is_denyloading_command = !(cmd_flags & CMD_LOADING) || (c->cmd->proc == execCommand && (c->mstate.cmd_inv_flags & CMD_LOADING)); int is_may_replicate_command = (cmd_flags & (CMD_WRITE | CMD_MAY_REPLICATE)) || (c->cmd->proc == execCommand && (c->mstate.cmd_flags & (CMD_WRITE | CMD_MAY_REPLICATE))); int is_deny_async_loading_command = (cmd_flags & CMD_NO_ASYNC_LOADING) || (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_NO_ASYNC_LOADING)); int obey_client = mustObeyClient(c);
if (authRequired(c)) { /* AUTH and HELLO and no auth commands are valid even in * non-authenticated state. */ if (!(c->cmd->flags & CMD_NO_AUTH)) { rejectCommand(c,shared.noautherr); return C_OK; } }
if (c->flags & CLIENT_MULTI && c->cmd->flags & CMD_NO_MULTI) { rejectCommandFormat(c,"Command not allowed inside a transaction"); return C_OK; }
/* Check if the user can run this command according to the current
- ACLs. */ int acl_errpos; int acl_retval = ACLCheckAllPerm(c,&acl_errpos); if (acl_retval != ACL_OK) { addACLLogEntry(c,acl_retval,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,acl_errpos,NULL,NULL); sds msg = getAclErrorMessage(acl_retval, c->user, c->cmd, c->argvacl_errpos->ptr, 0); rejectCommandFormat(c, "-NOPERM %s", msg); sdsfree(msg); return C_OK; }
/* If cluster is enabled perform the cluster redirection here.
- However we don't perform the redirection if:
-
- The sender of this command is our master.
-
- The command has no key arguments. */ if (server.cluster_enabled && !mustObeyClient(c) && !(!(c->cmd->flags&CMD_MOVABLE_KEYS) && c->cmd->key_specs_num == 0 && c->cmd->proc != execCommand)) { int error_code; clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc, &c->slot,cmd_flags,&error_code); if (n == NULL || !clusterNodeIsMyself(n)) { if (c->cmd->proc == execCommand) { discardTransaction(c); } else { flagTransaction(c); } clusterRedirectClient(c,n,c->slot,error_code); c->duration = 0; c->cmd->rejected_calls++; return C_OK; } }
/* Disconnect some clients if total clients memory is too high. We do this
- before key eviction, after the last command was executed and consumed
- some client output buffer memory. / evictClients(); if (server.current_client == NULL) { / If we evicted ourself then abort processing the command */ return C_ERR; }
/* Handle the maxmemory directive. *
-
Note that we do not want to reclaim memory if we are here re-entering
-
the event loop since there is a busy Lua script running in timeout
-
condition, to avoid mixing the propagation of scripts with the
-
propagation of DELs due to eviction. */ if (server.maxmemory && !isInsideYieldingLongCommand()) { int out_of_memory = (performEvictions() == EVICT_FAIL);
/* performEvictions may evict keys, so we need flush pending tracking
- invalidation keys. If we don't do this, we may get an invalidation
- message after we perform operation on the key, where in fact this
- message belongs to the old value of the key before it gets evicted.*/ trackingHandlePendingKeyInvalidations();
/* performEvictions may flush slave output buffers. This may result
- in a slave, that may be the active client, to be freed. */ if (server.current_client == NULL) return C_ERR;
if (out_of_memory && is_denyoom_command) { rejectCommand(c, shared.oomerr); return C_OK; }
/* Save out_of_memory result at command start, otherwise if we check OOM
- in the first write within script, memory used by lua stack and
- arguments might interfere. We need to save it for EXEC and module
- calls too, since these can call EVAL, but avoid saving it during an
- interrupted / yielding busy script / module. */ server.pre_command_oom_state = out_of_memory; }
/* Make sure to use a reasonable amount of memory for client side
- caching metadata. */ if (server.tracking_clients) trackingLimitUsedSlots();
/* Don't accept write commands if there are problems persisting on disk
- unless coming from our master, in which case check the replica ignore
- disk write error config to either log or crash. / int deny_write_type = writeCommandsDeniedByDiskError(); if (deny_write_type != DISK_ERROR_TYPE_NONE && (is_write_command || c->cmd->proc == pingCommand)) { if (obey_client) { if (!server.repl_ignore_disk_write_error && c->cmd->proc != pingCommand) { serverPanic("Replica was unable to write command to disk."); } else { static mstime_t last_log_time_ms = 0; const mstime_t log_interval_ms = 10000; if (server.mstime > last_log_time_ms + log_interval_ms) { last_log_time_ms = server.mstime; serverLog(LL_WARNING, "Replica is applying a command even though " "it is unable to write to disk."); } } } else { sds err = writeCommandsGetDiskErrorMessage(deny_write_type); / remove the newline since rejectCommandSds adds it. */ sdssubstr(err, 0, sdslen(err)-2); rejectCommandSds(c, err); return C_OK; } }
/* Don't accept write commands if there are not enough good slaves and
- user configured the min-slaves-to-write option. */ if (is_write_command && !checkGoodReplicasStatus()) { rejectCommand(c, shared.noreplicaserr); return C_OK; }
/* Don't accept write commands if this is a read only slave. But
- accept write commands if this is our master. */ if (server.masterhost && server.repl_slave_ro && !obey_client && is_write_command) { rejectCommand(c, shared.roslaveerr); return C_OK; }
/* Only allow a subset of commands in the context of Pub/Sub if the
- connection is in RESP2 mode. With RESP3 there are no limits. */ if ((c->flags & CLIENT_PUBSUB && c->resp == 2) && c->cmd->proc != pingCommand && c->cmd->proc != subscribeCommand && c->cmd->proc != ssubscribeCommand && c->cmd->proc != unsubscribeCommand && c->cmd->proc != sunsubscribeCommand && c->cmd->proc != psubscribeCommand && c->cmd->proc != punsubscribeCommand && c->cmd->proc != quitCommand && c->cmd->proc != resetCommand) { rejectCommandFormat(c, "Can't execute '%s': only (P|S)SUBSCRIBE / " "(P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context", c->cmd->fullname); return C_OK; }
/* Only allow commands with flag "t", such as INFO, REPLICAOF and so on,
- when replica-serve-stale-data is no and we are a replica with a broken
- link with master. */ if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED && server.repl_serve_stale_data == 0 && is_denystale_command) { rejectCommand(c, shared.masterdownerr); return C_OK; }
/* Loading DB? Return an error if the command has not the
- CMD_LOADING flag. */ if (server.loading && !server.async_loading && is_denyloading_command) { rejectCommand(c, shared.loadingerr); return C_OK; }
/* During async-loading, block certain commands. */ if (server.async_loading && is_deny_async_loading_command) { rejectCommand(c,shared.loadingerr); return C_OK; }
/* when a busy job is being done (script / module)
- Only allow a limited number of commands.
- Note that we need to allow the transactions commands, otherwise clients
- sending a transaction with pipelining without error checking, may have
- the MULTI plus a few initial commands refused, then the timeout
- condition resolves, and the bottom-half of the transaction gets
- executed, see Github PR #7022. */ if (isInsideYieldingLongCommand() && !(c->cmd->flags & CMD_ALLOW_BUSY)) { if (server.busy_module_yield_flags && server.busy_module_yield_reply) { rejectCommandFormat(c, "-BUSY %s", server.busy_module_yield_reply); } else if (server.busy_module_yield_flags) { rejectCommand(c, shared.slowmoduleerr); } else if (scriptIsEval()) { rejectCommand(c, shared.slowevalerr); } else { rejectCommand(c, shared.slowscripterr); } return C_OK; }
/* Prevent a replica from sending commands that access the keyspace.
- The main objective here is to prevent abuse of client pause check
- from which replicas are exempt. */ if ((c->flags & CLIENT_SLAVE) && (is_may_replicate_command || is_write_command || is_read_command)) { rejectCommandFormat(c, "Replica can't interact with the keyspace"); return C_OK; }
/* If the server is paused, block the client until
- the pause has ended. Replicas are never paused. */ if (!(c->flags & CLIENT_SLAVE) && ((isPausedActions(PAUSE_ACTION_CLIENT_ALL)) || ((isPausedActions(PAUSE_ACTION_CLIENT_WRITE)) && is_may_replicate_command))) { blockPostponeClient(c); return C_OK;
}
/* Exec the command */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && c->cmd->proc != discardCommand && c->cmd->proc != multiCommand && c->cmd->proc != watchCommand && c->cmd->proc != quitCommand && c->cmd->proc != resetCommand) { queueMultiCommand(c, cmd_flags); addReply(c,shared.queued); } else { int flags = CMD_CALL_FULL; if (client_reprocessing_command) flags |= CMD_CALL_REPROCESSING; call(c,flags); if (listLength(server.ready_keys) && !isInsideYieldingLongCommand()) handleClientsBlockedOnKeys(); } return C_OK; }
/* BRPOP ... */ void brpopCommand(client *c) { blockingPopGenericCommand(c,c->argv+1,c->argc-2,LIST_TAIL,c->argc-1,-1); }
/* Blocking RPOP/LPOP/LMPOP *
-
'numkeys' is the number of keys.
-
'timeout_idx' parameter position of block timeout.
-
'where' LIST_HEAD for LEFT, LIST_TAIL for RIGHT.
-
'count' is the number of elements requested to pop, or -1 for plain single pop.
-
When count is -1, a reply of a single bulk-string will be used.
-
When count > 0, an array reply will be used. */ void blockingPopGenericCommand(client *c, robj **keys, int numkeys, int where, int timeout_idx, long count) { robj *o; robj *key; mstime_t timeout; int j;
if (getTimeoutFromObjectOrReply(c,c->argvtimeout_idx,&timeout,UNIT_SECONDS) != C_OK) return;
/* Traverse all input keys, we take action only based on one key. */ for (j = 0; j < numkeys; j++) { key = keysj; o = lookupKeyWrite(c->db, key);
scss/* Non-existing key, move to next key. */ if (o == NULL) continue; if (checkType(c, o, OBJ_LIST)) return; long llen = listTypeLength(o); /* Empty list, move to next key. */ if (llen == 0) continue; if (count != -1) { /* BLMPOP, non empty list, like a normal [LR]POP with count option. * The difference here we pop a range of elements in a nested arrays way. */ listPopRangeAndReplyWithKey(c, o, key, where, count, 1, NULL); /* Replicate it as [LR]POP COUNT. */ robj *count_obj = createStringObjectFromLongLong((count > llen) ? llen : count); rewriteClientCommandVector(c, 3, (where == LIST_HEAD) ? shared.lpop : shared.rpop, key, count_obj); decrRefCount(count_obj); return; } /* Non empty list, this is like a normal [LR]POP. */ robj *value = listTypePop(o,where); serverAssert(value != NULL); addReplyArrayLen(c,2); addReplyBulk(c,key); addReplyBulk(c,value); decrRefCount(value); listElementsRemoved(c,key,where,o,1,1,NULL); /* Replicate it as an [LR]POP instead of B[LR]POP. */ rewriteClientCommandVector(c,2, (where == LIST_HEAD) ? shared.lpop : shared.rpop, key); return;}
/* If we are not allowed to block the client, the only thing
- we can do is treating it as a timeout (even with timeout 0). */ if (c->flags & CLIENT_DENY_BLOCKING) { addReplyNullArray(c); return; }
/* If the keys do not exist we must block */ blockForKeys(c,BLOCKED_LIST,keys,numkeys,timeout,0); }
/* Set a client in blocking mode for the specified key, with the specified timeout.
-
The 'type' argument is BLOCKED_LIST,BLOCKED_ZSET or BLOCKED_STREAM depending on the kind of operation we are
-
waiting for an empty key in order to awake the client. The client is blocked
-
for all the 'numkeys' keys as in the 'keys' argument.
-
The client will unblocked as soon as one of the keys in 'keys' value was updated.
-
the parameter unblock_on_nokey can be used to force client to be unblocked even in the case the key
-
is updated to become unavailable, either by type change (override), deletion or swapdb */ void blockForKeys(client *c, int btype, robj **keys, int numkeys, mstime_t timeout, int unblock_on_nokey) { dictEntry *db_blocked_entry, *db_blocked_existing_entry, *client_blocked_entry; list *l; int j;
if (!(c->flags & CLIENT_REPROCESSING_COMMAND)) { /* If the client is re-processing the command, we do not set the timeout * because we need to retain the client's original timeout. */ c->bstate.timeout = timeout; }
for (j = 0; j < numkeys; j++) { /* If the key already exists in the dictionary ignore it. */ if (!(client_blocked_entry = dictAddRaw(c->bstate.keys,keysj,NULL))) { continue; } incrRefCount(keysj);
scss/* And in the other "side", to map keys -> clients */ db_blocked_entry = dictAddRaw(c->db->blocking_keys,keys[j], &db_blocked_existing_entry); /* In case key[j] did not have blocking clients yet, we need to create a new list */ if (db_blocked_entry != NULL) { l = listCreate(); dictSetVal(c->db->blocking_keys, db_blocked_entry, l); incrRefCount(keys[j]); } else { l = dictGetVal(db_blocked_existing_entry); } listAddNodeTail(l,c); dictSetVal(c->bstate.keys,client_blocked_entry,listLast(l)); /* We need to add the key to blocking_keys_unblock_on_nokey, if the client * wants to be awakened if key is deleted (like XREADGROUP) */ if (unblock_on_nokey) { db_blocked_entry = dictAddRaw(c->db->blocking_keys_unblock_on_nokey, keys[j], &db_blocked_existing_entry); if (db_blocked_entry) { incrRefCount(keys[j]); dictSetUnsignedIntegerVal(db_blocked_entry, 1); } else { dictIncrUnsignedIntegerVal(db_blocked_existing_entry, 1); } }} c->bstate.unblock_on_nokey = unblock_on_nokey; /* Currently we assume key blocking will require reprocessing the command.
- However in case of modules, they have a different way to handle the reprocessing
- which does not require setting the pending command flag */ if (btype != BLOCKED_MODULE) c->flags |= CLIENT_PENDING_COMMAND; blockClient(c,btype); }
`