魔改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源码

魔改redis之添加命令hrandmember的更多相关文章

  1. 魔改——MFC MDI程序 定制 文档模板 运行时全部打开 禁用关闭按钮

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  2. [7b2美化]柒比贰 魔改系列|7B2-分类封面添加波浪效果&每日诗词

    本文转载自:钻芒博客 https://www.zmki.cn/5105.html 效果如图: 代码: 首先在style.css样式表里添加波浪样式 /*浪来了*/ .lang { overflow: ...

  3. layui 魔改:富文本编辑器添加上传视频功能

    甲方又整新需求了:富文本编辑器需要可以传视频. layui本身的富文本编辑器没有传视频的功能,所以,又到了咱们魔改的时候了. 友情提醒,富文本编辑器 layedit 只有layui的V1版有,V2版没 ...

  4. 魔改——MFC SDI程序 转换为 MDI程序

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  5. Jedis对Redis的常用命令操作

    本篇主要总结一些Jedis对Redis的常用命令操作: 1.对key操作命令 2.对String操作命令 3.对List操作命令 4.对Set操作命令 5.对Hash操作命令 6.排序操作指令 一.项 ...

  6. Linux下安装redis以及常用命令

    https://blog.csdn.net/zgf19930504/article/details/51850594 安装: 1.获取redis资源 wget http://download.redi ...

  7. 魔改——MDI多视图模板Tab/标签页 初始化/操作控件

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  8. 分布式系列十: Redis安装和命令

    redis是一个开源的, 内存数据结构存储, 一般用来作为数据库,缓存和消息代理. Redis的优势 多种数据结构 字符类型String 散列类型Hash 列表类型List 集合类型Set 有序集合类 ...

  9. Redis安装、命令以及设置密码遇到的问题

    一.下载Redis 如果没有 安装wget先安装wget和gcc(使用make的时候会用上) wget http://download.redis.io/releases/redis-4.0.8.ta ...

随机推荐

  1. std::unique_ptr使用incomplete type的报错分析和解决

    Pimpl(Pointer to implementation)很多同学都不陌生,但是从原始指针升级到C++11的独占指针std::unique_ptr时,会遇到一个incomplete type的报 ...

  2. 关于热力图的loss的一点感想

    网络的输出的热力图和gt相减的差矩阵,求其最大特征值的平方作为loss. 若图像h w不相等,可以使用奇异值代替特征值.奇异值往往对应着矩阵中隐含的重要信息,且重要性和奇异值大小正相关.每个矩阵A都可 ...

  3. harbor私有仓库部署

    Harbor 简介     Harbor是构建企业级私有docker镜像的仓库的开源解决方案,它是Docker Registry的更高级封装,它除了提供友好的Web UI界面,角色和用户权限管理,用户 ...

  4. Verilog之阻塞赋值非阻塞赋值

    verilog设计进阶 时间:2014年5月6日星期二 主要收获: 1. 阻塞赋值与非阻塞赋值: 2. 代码测试: 3. 组合逻辑电路和时序逻辑电路. 阻塞赋值与非阻塞赋值: 1. 阻塞赋值" ...

  5. 图像分割必备知识点 | Unet++超详解+注解

    文章来自周纵苇大佬的知乎,是Unet++模型的一作大佬,其在2019年底详细剖析了Unet++模型,讲解的非常好.所以在此做一个搬运+个人的理解. 文中加粗部分为个人做的注解.需要讨论交流的朋友可以加 ...

  6. golang拾遗:嵌入类型

    这里是golang拾遗系列的第三篇,前两篇可以点击此处链接跳转: golang拾遗:为什么我们需要泛型 golang拾遗:指针和接口 今天我们要讨论的是golang中的嵌入类型(embedding t ...

  7. Python怎么控制将一个整数输出成指定长的十六进制数?

    使用format方法,在格式控制中进行控制,具体控制参数为: {:#016X} 其中: 大括号表示该处从后面的format的参数中取值 冒号表示格式控制开始 0表示长度不足16位补0 16表示长度 X ...

  8. 第11.13节 Python正则表达式的转义符”\”功能介绍

    为了支持特殊元字符在特定场景下能表示自身而不会被当成元字符进行匹配出来,可以通过字符集或转义符表示方法来表示,字符集表示方法前面在<第11.4节 Python正则表达式搜索字符集匹配功能及元字符 ...

  9. PyQt(Python+Qt)学习随笔:QListView的itemAlignment属性

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QListView的itemAlignment属性用于控制每个数据项的对齐方式,其类型为枚举类Qt. ...

  10. PyQt(Python+Qt)学习随笔:Qt Designer中部件的调色板palette属性和字体font属性设置

    一.调色板 在Qt Designer的部件属性中,有个部件调色板(palette)的属性,进入后,如下图所示: 1.调色板palette Qt中提供的调色板palette用于管理控件的外观显示,对应P ...