魔改redis之新增命令hrandmember
正文
前言
想從redis
的hash
表獲取隨機的鍵值對,但是發現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
值是負數,意味著值可以重複。
-
如果值可以重複,那麼每次隨機取出一個成員。
-
如果
set
的size
小於請求的數量,則返回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; }
-
集合的數量沒有遠遠大於請求的數量。將
set
的值複製到dict
中,然後隨機刪除值,直到數量等於請求的值。 -
集合數量遠大請求的數量。隨機取值,加入
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
或者是dict
,ziplist
的編碼是ziplist
或者是dict
。在當前的redis版本中,還並沒有新增hrandmember
命令(6.2及之前)。
ziplist
中的字串長度超過OBJ_HASH_MAX_ZIPLIST_VALUE
(預設值為64),或者entry
的個數超過OBJ_HASH_MAX_ZIPLIST_ENTRIES
(預設值為512),則會轉化為hashtable
編碼。
ziplist
的encoding
就是嘗試將字串值解析成long
並儲存編碼。hash
型別和 set
型別最大的區別在於元素個數較少時,內部的編碼不同,hash
內部的編碼是ziplist
,而set
的內部編碼是intset
,(個人認為hash
和intset
內部編碼不統一是一處失誤,使用者對兩者有著相似的用法,也就是需求類似,然而底層實現卻不同,必然導致程式碼的重複,也確實如此,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);
}