Redis List 阻塞队列底层探秘:LPUSH 如何唤醒沉睡的 BRPOP

引言

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 存在时推入。

关键流程

  1. 通过 lookupKeyWrite 获取 key 对应的值对象(robj *lobj)。
  2. 若 key 不存在且 xx=0,则创建新的 listpack 对象并调用 dbAdd 写入数据库。
  3. 调用 listTypeTryConversionAppend 决定是否将 listpack 转换为 quicklist(当元素数量或大小超过阈值时)。
  4. 循环调用 listTypePush 将每个元素插入 listpack 的头部或尾部,并累加 server.dirty
  5. 回复客户端新列表长度。
  6. 调用 signalModifiedKeynotifyKeyspaceEvent 触发修改事件。

注意 :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,其核心逻辑是:

  1. 解析超时时间(秒)。
  2. 遍历所有输入 key ,一旦找到非空 的 List,立即执行弹出操作并返回结果,不进入阻塞
  3. 若所有 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" 并返回

六、设计精髓与思考

  1. 信号与处理分离
    LPUSH 只负责发信号(入队 ready_keys),真正的唤醒由统一调度器在命令结束阶段完成。这避免了在写入路径中直接操作复杂的阻塞客户端列表,保证了写入操作的快速响应。
  2. 命令重放而非状态保存
    Redis 选择在唤醒后完整重放原始阻塞命令,而不是保存中间状态。这使得代码逻辑清晰,且天然支持复制和 AOF,因为重放的命令就是原始 BRPOP 命令,复制副本会得到相同结果。
  3. 类型安全
    唤醒时严格检查 key 的类型是否匹配客户端期望的类型(如 List),防止因 key 被意外覆盖而导致数据语义错误。
  4. FIFO 公平性
    所有阻塞客户端按阻塞顺序排列在 db->blocking_keys 的链表中,唤醒时按顺序处理,保证了公平性。
  5. 去重优化
    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; }

    ini 复制代码
     lobj = 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:

    1. It sets the current client to the client 'c'.
    1. 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:
      1. The sender of this command is our master.
      1. 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); }

`

相关推荐
向日的葵0063 小时前
Redis会话机制vsJWT机制深度解析
数据库·redis·python·缓存·系统架构·jwt
小龙报4 小时前
【优选算法】1. 水果成蓝 2.找到字符串中所有字母的异位词
java·c语言·数据结构·数据库·c++·redis·算法
三言老师5 小时前
CentOS7.9:Redis 数据持久化结构化实战教程
数据库·redis·缓存
亿牛云爬虫专家6 小时前
如何设计一套高可用的爬虫任务队列,保证断点续爬与故障转移?
redis·爬虫·故障转移·任务队列·代理ip·requests·隧道代理
YOU OU15 小时前
Redis基础常识与命令
数据库·redis·缓存
吳所畏惧20 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
赵丙双21 小时前
Redis 概率相关的数据类型
redis
Y3815326621 天前
SERP API + Redis 缓存层:4 种方案对比与选型
数据库·redis·缓存
海兰1 天前
【高速缓存】RedisVL 高级查询(全文搜索、混合搜索和 多向量搜索)
数据库·人工智能·redis·缓存