魔改redis之新增命令hrandmember

dewxin發表於2020-12-27

魔改redis之新增命令hrandmember

正文

前言

想從redishash表獲取隨機的鍵值對,但是發現redis只支援set的隨機值SRANDMEMBER。但是如果把hash表中的資料又存一份,佔用的空間又太大。也可以通過先HLEN獲取hash表的大小,隨機出一個偏移值,再呼叫HSCAN獲得一組資料。或者直接多次隨機,多次取值。但這樣效率始終不如SRANDMEMBER(redis的開銷主要是網路的開銷)。於是想到魔改redis程式碼,使其支援對hash表的隨機鍵值對。客戶端jedis打算使用eval來呼叫redis中新加入的指令。

Set型別與srandmember命令

Set型別的編碼可以是OBJ_ENCODING_HT或者OBJ_ENCODING_INTSET,如果集合中的值全是數值,那麼Set的編碼(底層型別)為OBJ_ENCODING_INTSET, 如果加入了無法被解析為數值的字串,或者set的大小超過了OBJ_SET_MAX_INTSET_ENTRIES預設512,編碼則會變更為OBJ_ENCODING_HT

OBJ_ENCODING_INTSET就是儲存著整數的有序陣列。加入新值時新realloc新增記憶體,再使用memmove將對應位置後的資料後移,然後在對應的位置加入值。

OBJ_ENCODING_HT編碼就是dict型別,也就是字典。

srandmember命令的主要處理函式是srandmemberWithCountCommand,如果傳入的count值是負數,意味著值可以重複。

  1. 如果值可以重複,那麼每次隨機取出一個成員。

  2. 如果setsize小於請求的數量,則返回set集合中全部的值。

    //case 1
    if (!uniq) {
        addReplyMultiBulkLen(c,count);
        while(count--) {
            
            encoding = setTypeRandomElement(set,&ele,&llele);
            if (encoding == OBJ_ENCODING_INTSET) {
                addReplyBulkLongLong(c,llele);
            } else {
                addReplyBulkCBuffer(c,ele,sdslen(ele));
            }
        }
        return;
    }
    
    //case 2
    if (count >= size) {
        sunionDiffGenericCommand(c,c->argv+1,1,NULL,SET_OP_UNION);
        return;
    }
    
  3. 集合的數量沒有遠遠大於請求的數量。將set的值複製到dict中,然後隨機刪除值,直到數量等於請求的值。

  4. 集合數量遠大請求的數量。隨機取值,加入dict中,數量滿足後返回dict中的值。

        if (count*SRANDMEMBER_SUB_STRATEGY_MUL > size) {
            setTypeIterator *si;
    
            /* Add all the elements into the temporary dictionary. */
            si = setTypeInitIterator(set);
            while((encoding = setTypeNext(si,&ele,&llele)) != -1) {
                int retval = DICT_ERR;
    
                if (encoding == OBJ_ENCODING_INTSET) {
                    retval = dictAdd(d,createStringObjectFromLongLong(llele),NULL);
                } else {
                    retval = dictAdd(d,createStringObject(ele,sdslen(ele)),NULL);
                }
                serverAssert(retval == DICT_OK);
            }
            setTypeReleaseIterator(si);
            serverAssert(dictSize(d) == size);
    
            /* Remove random elements to reach the right count. */
            while(size > count) {
                dictEntry *de;
    
                de = dictGetRandomKey(d);
                dictDelete(d,dictGetKey(de));
                size--;
            }
        }
    
    
        else {
            unsigned long added = 0;
            robj *objele;
    
            while(added < count) {
                encoding = setTypeRandomElement(set,&ele,&llele);
                if (encoding == OBJ_ENCODING_INTSET) {
                    objele = createStringObjectFromLongLong(llele);
                } else {
                    objele = createStringObject(ele,sdslen(ele));
                }
                /* Try to add the object to the dictionary. If it already exists
                 * free it, otherwise increment the number of objects we have
                 * in the result dictionary. */
                if (dictAdd(d,objele,NULL) == DICT_OK)
                    added++;
                else
                    decrRefCount(objele);
            }
        }
    
        /* CASE 3 & 4: send the result to the user. */
        {
            dictIterator *di;
            dictEntry *de;
    
            addReplyMultiBulkLen(c,count);
            di = dictGetIterator(d);
            while((de = dictNext(di)) != NULL)
                addReplyBulk(c,dictGetKey(de));
            dictReleaseIterator(di);
            dictRelease(d);
        }
    

Hash型別對比Set型別

Hash型別和Set型別的關係非常密切,在java原始碼中,往往set型別就是由hash型別實現的。在redis中在資料量較大的時候也十分相似。

前文提到 Set型別的編碼可以是intset或者是dictziplist的編碼是ziplist或者是dict。在當前的redis版本中,還並沒有新增hrandmember命令(6.2及之前)。

ziplist中的字串長度超過OBJ_HASH_MAX_ZIPLIST_VALUE(預設值為64),或者entry的個數超過OBJ_HASH_MAX_ZIPLIST_ENTRIES(預設值為512),則會轉化為hashtable編碼。

ziplistencoding就是嘗試將字串值解析成long並儲存編碼。hash型別和 set型別最大的區別在於元素個數較少時,內部的編碼不同,hash內部的編碼是ziplist,而set的內部編碼是intset,(個人認為hashintset內部編碼不統一是一處失誤,使用者對兩者有著相似的用法,也就是需求類似,然而底層實現卻不同,必然導致程式碼的重複,也確實如此,redis團隊似乎因為這個原因遲遲沒有新增hrandmember命令)

hrandmember命令

因為ziplist不能被隨機訪問。對於ziplist編碼的hash表,我們採用以下演算法,來保證每個被取出來的entry的概率是一樣的。

我們從長度為m的ziplist中取出n個entry,m>=n,設剩下的長度為m left ,剩餘要取的個數為nleft,每次取球時,我們取它的概率為 nleft/m left

這樣能保證每個球被取出的概率相同,為n/m。可用數學歸納法證明。

通過使用這種方式,我們將時間複雜度從O(nm)降為O(m)。

處理hash編碼,我們複製srandmember的程式碼,並稍作修改,避免字串的複製以提高效率。

注意:當編碼為ziplist時,不支援負數的count。雖然也有返回值,但並不會重複,並且個數小於期望值。

void hrandmemberWithCountCommand(client *c, long l) {
    unsigned long entryCount, hashSize;
    int uniq = 1;
    hashTypeIterator *hi;
    robj *hash;
    dict *d;
    double randomDouble;
    double threshold;
    unsigned long index = 0;

    if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp]))
        == NULL || checkType(c,hash,OBJ_HASH)) return;

    if(l >= 0) {
        entryCount = (unsigned long) l;
    } else {
        entryCount = -l;
        uniq = 0;
    }

    hashSize = hashTypeLength(hash);
    if(entryCount > hashSize)
        entryCount = hashSize;
    addReplyMapLen(c, entryCount);
    hi = hashTypeInitIterator(hash);

    if(hash->encoding == OBJ_ENCODING_ZIPLIST) {
        while (hashTypeNext(hi) != C_ERR && entryCount != 0) {
            randomDouble = ((double)rand()) / RAND_MAX;
            threshold = ((double)entryCount) / (hashSize - index);
            if(randomDouble < threshold){
                entryCount--;
                addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
                addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);
            }
        
            index ++;
        }
    } else {       
        // copy of srandmember
        if(!uniq) {
            while(entryCount--) {
                sds key, value;
                
                dictEntry *de = dictGetRandomKey(hash->ptr);
                key = dictGetKey(de);
                value = dictGetVal(de);
                addReplyBulkCBuffer(c,key,sdslen(key));
                addReplyBulkCBuffer(c,value,sdslen(value));
            }
            return;
        }

        if(entryCount >= hashSize) {

            while (hashTypeNext(hi) != C_ERR) {
                addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
                addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);
            }
            return;
        }
        
        static dictType dt = {
            dictSdsHash,                /* hash function */
            NULL,                       /* key dup */
            NULL,                       /* val dup */
            dictSdsKeyCompare,          /* key compare */
            NULL,                       /* key destructor */
            NULL,                       /* val destructor */
            NULL                        /* allow to expand */
        };
        d = dictCreate(&dt,NULL);
        
        if(entryCount * HRANDMEMBER_SUB_STRATEGY_MUL > hashSize) {

            /* Add all the elements into the temporary dictionary. */
            while((hashTypeNext(hi)) != C_ERR) {
                int ret = DICT_ERR;
                sds key, value;

                key = hashTypeCurrentFromHashTable(hi,OBJ_HASH_KEY);
                value = hashTypeCurrentFromHashTable(hi,OBJ_HASH_VALUE);
                ret = dictAdd(d, key, value);

                serverAssert(ret == DICT_OK);
            }
            serverAssert(dictSize(d) == hashSize);

            /* Remove random elements to reach the right count. */
            while(hashSize > entryCount) {
                dictEntry *de;

                de = dictGetRandomKey(d);
                dictDelete(d,dictGetKey(de));
                hashSize--;
            }
        }

        else {
            unsigned long added = 0;
            sds sdsKey, sdsVal;

            while(added < entryCount) {
                dictEntry *de = dictGetRandomKey(hash->ptr);
                sdsKey = dictGetKey(de);
                sdsVal = dictGetVal(de);

                /* Try to add the object to the dictionary. If it already exists
                * free it, otherwise increment the number of objects we have
                * in the result dictionary. */
                if (dictAdd(d,sdsKey,sdsVal) == DICT_OK){
                    added++;
                }
            }
        }

        {
            dictIterator *di;
            dictEntry *de;
            di = dictGetIterator(d);
            while((de = dictNext(di)) != NULL) {
                sds key = dictGetKey(de);
                sds value = dictGetVal(de);
                addReplyBulkCBuffer(c,key,sdslen(key));
                addReplyBulkCBuffer(c,value,sdslen(value));
            }

            dictReleaseIterator(di);
            dictRelease(d);
        }
        
    }
    hashTypeReleaseIterator(hi);
}

參考文獻

srandmember

redis原始碼

相關文章