Databases 16 min read

Deep Dive into Redis Blocking Commands: Implementation, Timeout Handling, and Normal Pop Flow

This article explores the internal implementation of Redis blocking commands such as BLPOP and BRPOP, detailing how keys are blocked, the data structures involved, the timeout detection across event loops, and the mechanisms that unblock clients both on timeout and on normal list pushes.

Beike Product & Technology
Beike Product & Technology
Beike Product & Technology
Deep Dive into Redis Blocking Commands: Implementation, Timeout Handling, and Normal Pop Flow

The author discovered that the Redis BLPOP command’s blocking duration is imprecise, often exceeding the expected 100‑500 ms, which sparked an investigation into the source code of Redis blocking commands.

1. Introduction – The article uses BLPOP as a case study to examine Redis’s internal handling of blocking operations.

2. Setting the Blocking Key – After skipping client connection and server initialization, both BLPOP and BRPOP call the common function blockingPopGenericCommand. If the target list is empty and the client is not in a transaction, blockForKeys is invoked to set up the block.

Before diving into the source, the article reviews Redis’s core data structures: redisObject holds various underlying types such as dict, adlist, and sds. The dict provides O(1) lookups, adlist is a doubly‑linked list, and sds is Redis’s dynamic string implementation.

Redis also defines three structures for server, client, and database state: redisServer, client, and redisDb. Each server has a single redisServer instance, multiple redisDb instances, and each client is linked to a specific DB.

Source snippet for the relevant structs:

struct redisServer { /* ... */ unsigned int bpop_blocked_clients; /* ... */ };

typedef struct redisDb { /* ... */ dict *blocking_keys; /* ... */ } redisDb;

typedef struct client { /* ... */ int btype; blockingState bpop; /* ... */ } client;

typedef struct blockingState { mstime_t timeout; dict *keys; robj *target; int numreplicas; long long reploffset; } blockingState;

The redisServer field bpop_blocked_clients simply counts blocked clients for INFO output. redisDb ’s blocking_keys is a dict mapping each blocked key to an adlist of waiting clients. The client structure stores the block type in btype and the detailed state in bpop, including the timeout and the set of blocked keys.

3. Blocking Timeout – Timeout handling occurs in the time‑event clientsCronHandleTimeout. When a client’s CLIENT_BLOCKED flag is set, the function checks bpop.timeout against the current time and, if expired, calls replyToBlockedClientTimedOut and then unblockClient. The event loop runs at a configurable frequency ( hz, default 10, i.e., every 100 ms), which explains the extra few hundred milliseconds observed.

Key source fragment:

int clientsCronHandleTimeout(client *c, mstime_t now_ms) {
    time_t now = now_ms/1000;
    if (c->flags & CLIENT_BLOCKED) {
        if (c->bpop.timeout != 0 && c->bpop.timeout < now_ms) {
            replyToBlockedClientTimedOut(c);
            unblockClient(c);
        }
    }
    return 0;
}

The unblockClient function clears the client’s block state, updates the global server.bpop_blocked_clients counter, and moves the client to server.unblocked_clients for later processing.

void unblockClient(client *c) {
    if (c->btype == BLOCKED_LIST) {
        unblockClientWaitingData(c);
    } else if (c->btype == BLOCKED_WAIT) {
        unblockClientWaitingReplicas(c);
    } else {
        serverPanic("Unknown btype in unblockClient().");
    }
    c->flags &= ~CLIENT_BLOCKED;
    c->btype = BLOCKED_NONE;
    server.bpop_blocked_clients--;
    if (!(c->flags & CLIENT_UNBLOCKED)) {
        c->flags |= CLIENT_UNBLOCKED;
        listAddNodeTail(server.unblocked_clients,c);
    }
}

4. Normal Pop – When another client pushes data onto a blocked list, dbAdd detects the list type and calls signalListAsReady. This creates a readyList entry and adds it to server.ready_keys. After each command execution, processCommand checks server.ready_keys and invokes handleClientsBlockedOnLists to deliver the popped element to the waiting client.

void signalListAsReady(redisDb *db, robj *key) {
    if (dictFind(db->blocking_keys,key) == NULL) return;
    if (dictFind(db->ready_keys,key) != NULL) return;
    readyList *rl = zmalloc(sizeof(*rl));
    rl->key = key; rl->db = db; incrRefCount(key);
    listAddNodeTail(server.ready_keys,rl);
    incrRefCount(key);
    serverAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);
}

The final handler iterates over server.ready_keys, pops values from the list, calls unblockClient, and sends the result to the client.

void handleClientsBlockedOnLists(void) {
    while (listLength(server.ready_keys) != 0) {
        list *l = server.ready_keys;
        server.ready_keys = listCreate();
        while (listLength(l) != 0) {
            listNode *ln = listFirst(l);
            readyList *rl = ln->value;
            dictDelete(rl->db->ready_keys,rl->key);
            robj *o = lookupKeyWrite(rl->db,rl->key);
            if (o && o->type == OBJ_LIST) {
                dictEntry *de = dictFind(rl->db->blocking_keys,rl->key);
                if (de) {
                    list *clients = dictGetVal(de);
                    int numclients = listLength(clients);
                    while (numclients--) {
                        listNode *clientnode = listFirst(clients);
                        client *receiver = clientnode->value;
                        robj *value = listTypePop(o, LIST_TAIL);
                        if (value) {
                            unblockClient(receiver);
                            serveClientBlockedOnList(receiver, rl->key, NULL, rl->db, value, LIST_TAIL);
                            decrRefCount(value);
                        } else {
                            break;
                        }
                    }
                }
            }
            decrRefCount(rl->key);
            zfree(rl);
            listDelNode(l,ln);
        }
        listRelease(l);
    }
}

In summary, Redis’s single‑process, event‑driven architecture spreads blocking‑command handling across multiple stages: setting the block, detecting timeout in a periodic time event, and delivering results either on timeout or when the list becomes ready. The default event‑loop interval of 100 ms accounts for the observed extra latency.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackenddatabasesTimeoutsource codeBlocking Commands
Beike Product & Technology
Written by

Beike Product & Technology

As Beike's official product and technology account, we are committed to building a platform for sharing Beike's product and technology insights, targeting internet/O2O developers and product professionals. We share high-quality original articles, tech salon events, and recruitment information weekly. Welcome to follow us.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.