Redis源碼剖析和註釋(十)--- 列表鍵命令實現(t_list)

Redis 列表類型命令實現(t_list)

1.列表類型命令介紹

redis中所有列表類型的命令如下:列表類型命令詳解

序號 命令及描述
1 BLPOP key1 [key2 ] timeout:移出並獲取列表的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
2 BRPOP key1 [key2 ] timeout:移出並獲取列表的最後一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
3 BRPOPLPUSH source destination timeout:從列表中彈出一個值,將彈出的元素插入到另外一個列表中並返回它;如但果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
4 LINDEX key index:通過索引獲取列表中的元素
5 LINSERT key BEFORE|AFTER pivot value:在列表的元素前或者後插入元素
6 LLEN key:獲取列表長度
7 LPOP key:移出並獲取列表的第一個元素
8 LPUSH key value1 [value2]:將一個或多個值插入到列表頭部
9 LPUSHX key value:將一個或多個值插入到已存在的列表頭部
10 LRANGE key start stop:獲取列表指定範圍內的元素
11 LREM key count value:移除列表元素
12 LSET key index value:通過索引設置列表元素的值
13 LTRIM key start stop:對一個列表進行修剪(trim),就是說,讓列表只保留指定區間內的元素,不在指定區間之內的元素都將被刪除。
14 RPOP key:移除並獲取列表最後一個元素
15 RPOPLPUSH source destination:移除列表的最後一個元素,並將該元素添加到另一個列表並返回
16 RPUSH key value1 [value2]:在列表中添加一個或多個值
17 RPUSHX key value:爲已存在的列表添加值

2. 列表類型的實現

2.1 列表類型

之前在剖析redis對象系統(3.2 版本)的實現時,我們得出列表類型的對象的底層實現的數據結構是快速列表(OBJ_ENCODING_QUICKLIST)和壓縮列表(OBJ_ENCODING_ZIPLIST)。但是quicklist本質上就是以ziplist爲節點的雙向鏈表,因此列表類型的命令的底層編碼只對OBJ_ENCODING_QUICKLIST類型進行操作。

在redis中,列表類型封裝了一層自己的接口,當然是基於quicklist的接口進行封裝。

這些函數的註釋請上github查看:列表命令實現代碼註釋

/* List data type */
void listTypeTryConversion(robj *subject, robj *value);
// 列表類型的從where插入一個value,PUSH命令的底層實現
void listTypePush(robj *subject, robj *value, int where);
// 列表類型的從where彈出一個value,POP命令底層實現
robj *listTypePop(robj *subject, int where);
// 返回對象的長度,entry節點個數
unsigned long listTypeLength(robj *subject);
// 初始化列表類型的迭代器爲一個指定的下標
listTypeIterator *listTypeInitIterator(robj *subject, long index, unsigned char direction);
// 釋放迭代器空間
void listTypeReleaseIterator(listTypeIterator *li);
// 將列表類型的迭代器指向的entry保存在提供的listTypeEntry結構中,並且更新迭代器,1表示成功,0失敗
int listTypeNext(listTypeIterator *li, listTypeEntry *entry);
// 返回一個節點的value對象,根據當前的迭代器
robj *listTypeGet(listTypeEntry *entry);
// 列表類型的插入操作,將value對象插到where
void listTypeInsert(listTypeEntry *entry, robj *value, int where);
// 比較列表類型的entry結構與對象的entry節點的值是否等,相等返回1
int listTypeEqual(listTypeEntry *entry, robj *o);
// 刪除迭代器指向的entry
void listTypeDelete(listTypeIterator *iter, listTypeEntry *entry);
// 轉換ZIPLIST編碼類型爲quicklist類型,enc指定OBJ_ENCODING_QUICKLIST
void listTypeConvert(robj *subject, int enc);

由於這些接口都是調用的quicklist的實現,因此各個操作都非常簡單,例如:

  • 列表類型的PUSH操作
//列表類型的從where插入一個value,PUSH命令的底層實現
void listTypePush(robj *subject, robj *value, int where) {

    //對列表對象編碼爲quicklist類型操作
    if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
        //根據where保存quicklist的頭節點地址或尾節點地址
        int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;

        //獲得value編碼爲RAW的字符串對象
        value = getDecodedObject(value);

        //保存value的長度
        size_t len = sdslen(value->ptr);

        //PUSH value的值到quicklist的頭或尾
        quicklistPush(subject->ptr, value->ptr, len, pos);

        //value的引用計數減1
        decrRefCount(value);
    } else {
        serverPanic("Unknown list encoding");   //不是quicklist類型的編碼則發送錯誤信息
    }
}

在比如:

  • 列表類型的POP操作
//拷貝對象類型的方法,用於listTypePop函數的調用
void *listPopSaver(unsigned char *data, unsigned int sz) {
    return createStringObject((char*)data,sz);
}

//列表類型的從where彈出一個value,POP命令底層實現
robj *listTypePop(robj *subject, int where) {
    long long vlong;
    robj *value = NULL;

    //獲得POP的位置,quicklist的頭部或尾部
    int ql_where = where == LIST_HEAD ? QUICKLIST_HEAD : QUICKLIST_TAIL;

    //對列表對象編碼爲quicklist類型操作
    if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
        //從ql_where位置POP出一個entry節點,保存在value或vlong中
        if (quicklistPopCustom(subject->ptr, ql_where, (unsigned char **)&value,
                               NULL, &vlong, listPopSaver)) {
            if (!value) //如果彈出的entry節點是整型的
                //則根據整型值創建一個字符串對象
                value = createStringObjectFromLongLong(vlong);
        }
    } else {
        serverPanic("Unknown list encoding");
    }
    return value;   //返回彈出entry節點的value值
}

這裏,我們只簡單分析這兩個操作的過程,其他的可以上列表命令實現代碼註釋查看所有的註釋。

2.2 列表對象

因爲redis實現了自己的對象系統,因此所有的操作也都是基於對象的列表類型的對象結構如下:

//這裏分析列表類型的對象
typedef struct redisObject {
    //對象的數據類型,只對 OBJ_LIST 類型進行操作
    unsigned type:4;        
    //對象的編碼類型,只對 OBJ_ENCODING_QUICKLIST 編碼類型進行操作。因爲,底層實現就只有這一種。
    unsigned encoding:4;

    //least recently used
    //實用LRU算法計算相對server.lruclock的LRU時間
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */

    //引用計數
    int refcount;

    //指向底層數據實現的指針,指向的一定是一個列表
    void *ptr;
} robj;

我們大致描繪出一個列表對象的樣子,如下圖:

這裏寫圖片描述

2.3 列表類型的迭代器

redis定義了列表類型自己的迭代器和管理節點信息的結構。

/* Structure to hold list iteration abstraction. */
typedef struct {
    robj *subject;          //迭代器指向的對象
    unsigned char encoding; //迭代器指向對象的編碼類型
    unsigned char direction;//迭代器的方向
    quicklistIter *iter;    //quicklist的迭代器
} listTypeIterator; //列表類型迭代器

/* Structure for an entry while iterating over a list. */
//管理列表節點信息的結構
typedef struct {
    listTypeIterator *li;   //所屬的列表類型迭代器
    quicklistEntry entry;   //quicklist中的quicklistEntry結構,該結構是quicklist用來管理entry的所定義的結構
} listTypeEntry;    //列表類型的entry結構

迭代器的操作,我們分析兩個。正因爲定義了迭代器,所以在遍歷列表的代碼看起來非常像C++代碼,而且更方便閱讀。

  • 初始化迭代器
/* Initialize an iterator at the specified index. */
//初始化列表類型的迭代器爲一個指定的下標
listTypeIterator *listTypeInitIterator(robj *subject, long index,
                                       unsigned char direction) {
    listTypeIterator *li = zmalloc(sizeof(listTypeIterator));   //分配空間
    //設置迭代器的各個成員的初始值
    li->subject = subject;
    li->encoding = subject->encoding;
    li->direction = direction;
    li->iter = NULL;    //quicklist迭代器爲空

    /* LIST_HEAD means start at TAIL and move *towards* head.
     * LIST_TAIL means start at HEAD and move *towards tail. */
    //獲得迭代方向
    int iter_direction =
        direction == LIST_HEAD ? AL_START_TAIL : AL_START_HEAD;

    //對列表對象編碼爲quicklist類型操作
    if (li->encoding == OBJ_ENCODING_QUICKLIST) {
        //將迭代器和下標爲index的quicklistNode結合,迭代器指向該節點
        li->iter = quicklistGetIteratorAtIdx(li->subject->ptr,
                                             iter_direction, index);
    } else {
        serverPanic("Unknown list encoding");
    }
    return li;
}
  • 迭代器的迭代
//將列表類型的迭代器指向的entry保存在提供的listTypeEntry結構中,並且更新迭代器,1表示成功,0失敗
int listTypeNext(listTypeIterator *li, listTypeEntry *entry) {
    /* Protect from converting when iterating */
    //確保對象編碼類型和迭代器中encoding成員相等
    serverAssert(li->subject->encoding == li->encoding);

    //設置listTypeEntry的entry成員關聯到當前列表類型的迭代器
    entry->li = li;
    //對列表對象編碼爲quicklist類型操作
    if (li->encoding == OBJ_ENCODING_QUICKLIST) {
        //保存當前的entry到listTypeEntry的entry成員,並更新迭代器
        return quicklistNext(li->iter, &entry->entry);
    } else {
        serverPanic("Unknown list encoding");
    }
    return 0;
}

3. 列表類型命令實現

列表類型命令與其他類型命令的不同是,列表命令實現了阻塞命令,例如:BLPOP、BRPOP、BLPOPRPUSH。

查看下載所有函數的註釋:列表命令實現代碼註釋

3.1 非阻塞命令

我們只分析一類命令的最底層實現,如果PUSH一類、POP一類。

  • PUSH一類命令的底層實現
//PUSH命令的底層實現,where保存push的位置
void pushGenericCommand(client *c, int where) {
    int j, waiting = 0, pushed = 0;
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);  //以寫操作讀取key對象的value

    //如果value對象不是列表類型則發送錯誤信息,返回
    if (lobj && lobj->type != OBJ_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }

    //從第一個value開始遍歷
    for (j = 2; j < c->argc; j++) {
        c->argv[j] = tryObjectEncoding(c->argv[j]);     //將value對象優化編碼
        //如果沒有找到key對象
        if (!lobj) {
            //創建一個quicklist類型的對象
            lobj = createQuicklistObject();
            //設置ziplist最大的長度和壓縮程度,配置文件指定
            quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
                                server.list_compress_depth);
            //將新的key對象和優化編碼過的value對象進行組成鍵值對
            dbAdd(c->db,c->argv[1],lobj);
        }

        //在where推入一個value對象
        listTypePush(lobj,c->argv[j],where);
        pushed++;   //更新計數器
    }
    //發送當前列表中元素的個數
    addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));
    //如果推入元素成功
    if (pushed) {
        char *event = (where == LIST_HEAD) ? "lpush" : "rpush";

        //當數據庫的鍵被改動,則會調用該函數發送信號
        signalModifiedKey(c->db,c->argv[1]);
        //發送"lpush"或"rpush"事件通知
        notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
    }
    server.dirty += pushed; //更新髒鍵
}
  • POP一類命令的底層實現
//POP命令的底層實現,where保存pop的位置
void popGenericCommand(client *c, int where) {
    //以寫操作取出key對象的value值
    robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk);
    // 如果key沒找到或value對象不是列表類型則直接返回
    if (o == NULL || checkType(c,o,OBJ_LIST)) return;

    //從where 彈出一個value
    robj *value = listTypePop(o,where);
    //如果value爲空,則發送空信息
    if (value == NULL) {
        addReply(c,shared.nullbulk);
    } else {
        //保存時間名稱
        char *event = (where == LIST_HEAD) ? "lpop" : "rpop";

        //發送value給client
        addReplyBulk(c,value);
        //釋放value對象
        decrRefCount(value);
        //發送事件通知
        notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
        //如果彈出一個元素後,列表爲空
        if (listTypeLength(o) == 0) {
            //發送"del"時間通知
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                                c->argv[1],c->db->id);
            //從數據庫中刪除當前的key
            dbDelete(c->db,c->argv[1]);
        }
        //當數據庫的鍵被改動,則會調用該函數發送信號
        signalModifiedKey(c->db,c->argv[1]);
        //更新髒鍵
        server.dirty++;
    }
}
  • PUSHX、INSERT命令的底層實現
//當key存在時則push,PUSHX,INSERT命令的底層實現
void pushxGenericCommand(client *c, robj *refval, robj *val, int where) {
    robj *subject;
    listTypeIterator *iter;
    listTypeEntry entry;
    int inserted = 0;

    //以寫操作讀取key對象的value
    //如果讀取失敗或讀取的value對象不是列表類型則返回
    if ((subject = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,subject,OBJ_LIST)) return;

    //尋找基準值refval
    if (refval != NULL) {
        /* Seek refval from head to tail */
        //創建一個列表的迭代器
        iter = listTypeInitIterator(subject,0,LIST_TAIL);
        //將指向當前的entry節點保存到列表類型的entry中,然後指向下一個entry節點
        while (listTypeNext(iter,&entry)) {
            //當前的entry節點的值與基準值refval是否相等
            if (listTypeEqual(&entry,refval)) {
                //如果相等,根據where插入val對象
                listTypeInsert(&entry,val,where);
                inserted = 1;   //設置插入的標識,跳出循環
                break;
            }
        }
        //事項迭代器
        listTypeReleaseIterator(iter);

        //如果插入成功,鍵值被修改,則發送信號並且發送"linsert"時間通知
        if (inserted) {
            signalModifiedKey(c->db,c->argv[1]);
            notifyKeyspaceEvent(NOTIFY_LIST,"linsert",
                                c->argv[1],c->db->id);
            server.dirty++; //更新髒鍵
        } else {
            /* Notify client of a failed insert */
            //如果沒有插入,則發送插入失敗的信息
            addReply(c,shared.cnegone);
            return;
        }

    //如果基準值爲空
    } else {
        //根據where判斷出事件名稱
        char *event = (where == LIST_HEAD) ? "lpush" : "rpush";

        //將val對象推入到列表的頭部或尾部
        listTypePush(subject,val,where);
        //當數據庫的鍵被改動,則會調用該函數發送信號
        signalModifiedKey(c->db,c->argv[1]);
        //發送事件通知
        notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
        server.dirty++; //更新髒鍵
    }

    //將插入val後的列表的元素個數發送給client
    addReplyLongLong(c,listTypeLength(subject));
}

3.2 阻塞命令

阻塞命令一共有三個,分別是:BLPOP,BRPOP,BLPOPRPUSH。命令格式如下:

 BLPOP key [key ...] timeout 
 BRPOP key [key ...] timeout
 BRPOPLPUSH source destination timeout 
 //timeout 參數表示的是一個指定阻塞的最大秒數的整型值。當 timeout 爲 0 是表示阻塞時間無限制。

首先我們介紹阻塞命令的兩種行爲。

3.2.1 非阻塞行爲

假如 BLPOP 或 BLPOP 命令被執行,當給定的所有個key內,

  • 至少有1個是非空列表,那麼就會直接將結果和信息返回給調用者。
  • 有多個非空列表,按照key的先後順序,依次檢查各個列表。

3.2.2 阻塞行爲

如果所有給定的key中不存在,或者key中包含的是空列表,那麼 BLPOP 或 BLPOP 命令將會被阻塞連接,直到另一個client對這些key中執行 [LR]PUSH 命令將一個新數據出現在任意key的列表中,那麼這個命令會解除調用BLPOP 或 BLPOP 命令的client的阻塞狀態。

3.2.3 阻塞命令的實現

其實阻塞命令實現就是在非阻塞命令的基礎上,只需要進行判斷了相應的阻塞操作即可。

  • BRPOP BLPOP 命令的底層實現
/* Blocking RPOP/LPOP */
// BRPOP BLPOP 命令的底層實現
//  BLPOP key [key ...] timeout
void blockingPopGenericCommand(client *c, int where) {
    robj *o;
    mstime_t timeout;
    int j;

    // 以秒爲單位保存timeout值
    if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS)
        != C_OK) return;

    //非阻塞行爲
    //遍歷所有的key,如果key中列表有值,則執行完這個循環一定能直接返回
    for (j = 1; j < c->argc-1; j++) {
        //以寫操作取出當前key的值
        o = lookupKeyWrite(c->db,c->argv[j]);
        // value對象不爲空
        if (o != NULL) {
            // 如果value對象的類型不是列表類型,發送類型錯誤信息,直接返回
            if (o->type != OBJ_LIST) {
                addReply(c,shared.wrongtypeerr);
                return;
            } else {
                // 列表長度不爲0
                if (listTypeLength(o) != 0) {
                    /* Non empty list, this is like a non normal [LR]POP. */
                    // 保存事件名稱
                    char *event = (where == LIST_HEAD) ? "lpop" : "rpop";
                    // 保存彈出的value對象
                    robj *value = listTypePop(o,where);
                    serverAssert(value != NULL);

                    // 發送回覆給client
                    addReplyMultiBulkLen(c,2);
                    addReplyBulk(c,c->argv[j]);
                    addReplyBulk(c,value);
                    // 釋放value
                    decrRefCount(value);
                    // 發送事件通知
                    notifyKeyspaceEvent(NOTIFY_LIST,event,
                                        c->argv[j],c->db->id);
                    //如果彈出元素後列表爲空
                    if (listTypeLength(o) == 0) {
                        //從數據庫中刪除當前的key
                        dbDelete(c->db,c->argv[j]);
                        // 發送"del"的事件通知
                        notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                                            c->argv[j],c->db->id);
                    }
                    //數據庫的鍵被修改,發送信號
                    signalModifiedKey(c->db,c->argv[j]);
                    //更新髒鍵
                    server.dirty++;

                    /* Replicate it as an [LR]POP instead of B[LR]POP. */
                    // 傳播一個[LR]POP 而不是B[LR]POP,修改client原來的命令參數
                    rewriteClientCommandVector(c,2,
                        (where == LIST_HEAD) ? shared.lpop : shared.rpop,
                        c->argv[j]);
                    return;
                }
            }
        }
    }

    //非阻塞行爲
    /* If we are inside a MULTI/EXEC and the list is empty the only thing
     * we can do is treating it as a timeout (even with timeout 0). */
    // 如果命令在一個事務中執行,則發送一個空回覆以避免死等待,因爲要執行完整個事務塊。
    if (c->flags & CLIENT_MULTI) {
        addReply(c,shared.nullmultibulk);
        return;
    }

    /* If the list is empty or the key does not exists we must block */
    // 參數中的所有鍵都不存在,則阻塞這些鍵
    blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}

我們可以看到阻塞命令其實很容易理解,但是我們需要分析阻塞的過程,也就是blockForKeys()函數。

// keys是一個key的數組,個數爲numkeys個
// timeout保存超時時間
// target保存PUSH入元素的鍵,也就是dstkey,用於BRPOPLPUSH函數
// 根據給定的key將client阻塞
void blockForKeys(client *c, robj **keys, int numkeys, mstime_t timeout, robj *target) {
    dictEntry *de;
    list *l;
    int j;

    //設置超時時間和target,該結構在下面列出
    c->bpop.timeout = timeout;
    c->bpop.target = target;

    //增加target的引用計數
    if (target != NULL) incrRefCount(target);

    //將當前client與numkeys個key關聯起來,結果也就是造成client阻塞的鍵是給定的numkeys個key
    for (j = 0; j < numkeys; j++) {
        /* If the key already exists in the dict ignore it. */
        //bpop.keys記錄所有造成client阻塞的鍵,該結構在下面列出
        //將要阻塞的鍵放入bpop.keys字典中
        if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
        //當前的key引用計數加1
        incrRefCount(keys[j]);

        /* And in the other "side", to map keys -> clients */
        //db->blocking_keys是一個字典,字典的鍵爲造成client阻塞的一個鍵,值是一個鏈表,保存着所有被該鍵阻塞的client
        //當前造成client被阻塞的鍵有沒有當前的key,如果沒有則要進行關聯
        de = dictFind(c->db->blocking_keys,keys[j]);
        if (de == NULL) {   //沒有當前的key,添加進去
            int retval;

            /* For every key we take a list of clients blocked for it */
            //創建一個列表
            l = listCreate();
            //將造成阻塞的鍵和列表添加到db->blocking_keys字典中
            retval = dictAdd(c->db->blocking_keys,keys[j],l);
            incrRefCount(keys[j]);
            serverAssertWithInfo(c,keys[j],retval == DICT_OK);
        } else {    //如果已經有了,則當前key的值保存起來,值是一個列表
            l = dictGetVal(de);
        }
        listAddNodeTail(l,c);   //將當前client加入到阻塞的client的列表
    }
    blockClient(c,BLOCKED_LIST);    //阻塞client
}

從上面的代碼中,使用了client結構的一些成員分別是c->bpop.xxxx 和 c->db->blocking_keys。我們分別查看一下其定義:

// server.h
typedef struct client {
    //client當前使用的數據庫
    redisDb *db;   /* Pointer to currently SELECTed DB. */

    //阻塞狀態
    blockingState bpop;     /* blocking state */
    //其他成員省略
} client;

我們先來分析blockingState結構。

// server.h
typedef struct blockingState {
    /* Generic fields. */
    //阻塞的時間
    mstime_t timeout;       /* Blocking operation timeout. If UNIX current time
                             * is > timeout then the operation timed out. */

    /* BLOCKED_LIST */
    //造成阻塞的鍵
    dict *keys;             /* The keys we are waiting to terminate a blocking
                             * operation such as BLPOP. Otherwise NULL. */
    //用於BRPOPLPUSH命令
    //用於保存PUSH入元素的鍵,也就是dstkey
    robj *target;           /* The key that should receive the element,
                             * for BRPOPLPUSH. */

    /* BLOCKED_WAIT */
    int numreplicas;        /* Number of replicas we are waiting for ACK. */
    long long reploffset;   /* Replication offset to reach. */
} blockingState;

再來看一下 redisDb 結構

typedef struct redisDb {
    //正處於阻塞狀態的鍵
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    //可以解除阻塞的鍵
    dict *ready_keys;           /* Blocked keys that received a PUSH */
} redisDb;

接下來,我們分析blockForKeys()代碼。剛開始將timeout和target分別讀入blockingState中。然後遍歷所有傳過來的key,這些key都是造成當前client阻塞的鍵,我們需要將這些鍵添加記錄,因此,將這些鍵加入到 c->bpop.keys 中。我們從上面的blockingState結構看出 c->bpop.keys 這個成員是一個字典結構,這個詞典記錄着所有造成客戶端阻塞的鍵。而且加入字典中的鍵爲傳入的所有key,而值則爲NULL。

如果之前 c->bpop.keys 中已經記錄當前key,則跳過本層循環。

我們不光記錄所有造成客戶端阻塞的鍵,還要這些所有鍵和造成阻塞的客戶端添加對應的映射關係。首先,我們介紹 blocking_keys 成員,這是一個字典,該字典的鍵是造成客戶端阻塞的鍵,而字典的值則爲一個鏈表,鏈表中包含着所有被阻塞的客戶端,找到當前key的節點,將節點的值,這個值保存的是被阻塞的client,將這個值添加到被阻塞客戶端鏈表中。然後調用blockClient()阻塞客戶端。

看懂了這些,對應的解阻塞的代碼就很容易看懂。解阻塞就是從記錄中刪除對應key並且解除key和client的映射。

//解阻塞一個正在阻塞中的client
void unblockClientWaitingData(client *c) {
    dictEntry *de;
    dictIterator *di;
    list *l;

    serverAssertWithInfo(c,NULL,dictSize(c->bpop.keys) != 0);
    //創建一個字典的迭代器,指向的是造成client阻塞的鍵所組成的字典
    di = dictGetIterator(c->bpop.keys);
    /* The client may wait for multiple keys, so unblock it for every key. */
    //因爲client可能被多個key所阻塞,所以要遍歷所有的鍵
    while((de = dictNext(di)) != NULL) {
        robj *key = dictGetKey(de); //獲得key對象

        /* Remove this client from the list of clients waiting for this key. */
        //根據key找到對應的列表類型值,值保存着被阻塞的client,從中找c->db->blocking_keys中尋找
        l = dictFetchValue(c->db->blocking_keys,key);
        serverAssertWithInfo(c,key,l != NULL);
        // 將阻塞的client從列表中移除
        listDelNode(l,listSearchKey(l,c));
        /* If the list is empty we need to remove it to avoid wasting memory */
        //如果當前列表爲空了,則從c->db->blocking_keys中將key刪除
        if (listLength(l) == 0)
            dictDelete(c->db->blocking_keys,key);
    }
    dictReleaseIterator(di);    //釋放迭代器

    /* Cleanup the client structure */
    //清空bpop.keys的所有節點
    dictEmpty(c->bpop.keys,NULL);
    //如果保存有新添加的元素,則應該釋放
    if (c->bpop.target) {
        decrRefCount(c->bpop.target);
        c->bpop.target = NULL;
    }
}
  • BRPOPLPUSH 命令的實現
//  BRPOPLPUSH source destination timeout
// BRPOPLPUSH命令的實現
void brpoplpushCommand(client *c) {
    mstime_t timeout;

    //以秒爲單位取出超時時間
    if (getTimeoutFromObjectOrReply(c,c->argv[3],&timeout,UNIT_SECONDS)
        != C_OK) return;
    //以寫操作讀取出 source的值
    robj *key = lookupKeyWrite(c->db, c->argv[1]);

    //如果鍵爲空,阻塞行爲
    if (key == NULL) {
        // 如果命令在一個事務中執行,則發送一個空回覆以避免死等待
        if (c->flags & CLIENT_MULTI) {
            /* Blocking against an empty list in a multi state
             * returns immediately. */
            addReply(c, shared.nullbulk);
        } else {
            /* The list is empty and the client blocks. */
            // 列表爲空,則將client阻塞
            blockForKeys(c, c->argv + 1, 1, timeout, c->argv[2]);
        }

    //非阻塞行爲
    //如果鍵不爲空,執行RPOPLPUSH
    } else {
        //判斷取出的value對象是否爲列表類型,不是的話發送類型錯誤信息
        if (key->type != OBJ_LIST) {
            addReply(c, shared.wrongtypeerr);
        } else {
            /* The list exists and has elements, so
             * the regular rpoplpushCommand is executed. */
            // value對象的列表存在且有元素,所以調用普通的rpoplpush命令
            serverAssertWithInfo(c,key,listTypeLength(key) > 0);
            rpoplpushCommand(c);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章