魔改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集合中全部的值。

    1. //case 1
    2. if (!uniq) {
    3. addReplyMultiBulkLen(c,count);
    4. while(count--) {
    5. encoding = setTypeRandomElement(set,&ele,&llele);
    6. if (encoding == OBJ_ENCODING_INTSET) {
    7. addReplyBulkLongLong(c,llele);
    8. } else {
    9. addReplyBulkCBuffer(c,ele,sdslen(ele));
    10. }
    11. }
    12. return;
    13. }
    14. //case 2
    15. if (count >= size) {
    16. sunionDiffGenericCommand(c,c->argv+1,1,NULL,SET_OP_UNION);
    17. return;
    18. }
  3. 集合的数量没有远远大于请求的数量。将set的值复制到dict中,然后随机删除值,直到数量等于请求的值。

  4. 集合数量远大请求的数量。随机取值,加入dict中,数量满足后返回dict中的值。

    1. if (count*SRANDMEMBER_SUB_STRATEGY_MUL > size) {
    2. setTypeIterator *si;
    3. /* Add all the elements into the temporary dictionary. */
    4. si = setTypeInitIterator(set);
    5. while((encoding = setTypeNext(si,&ele,&llele)) != -1) {
    6. int retval = DICT_ERR;
    7. if (encoding == OBJ_ENCODING_INTSET) {
    8. retval = dictAdd(d,createStringObjectFromLongLong(llele),NULL);
    9. } else {
    10. retval = dictAdd(d,createStringObject(ele,sdslen(ele)),NULL);
    11. }
    12. serverAssert(retval == DICT_OK);
    13. }
    14. setTypeReleaseIterator(si);
    15. serverAssert(dictSize(d) == size);
    16. /* Remove random elements to reach the right count. */
    17. while(size > count) {
    18. dictEntry *de;
    19. de = dictGetRandomKey(d);
    20. dictDelete(d,dictGetKey(de));
    21. size--;
    22. }
    23. }
    24. else {
    25. unsigned long added = 0;
    26. robj *objele;
    27. while(added < count) {
    28. encoding = setTypeRandomElement(set,&ele,&llele);
    29. if (encoding == OBJ_ENCODING_INTSET) {
    30. objele = createStringObjectFromLongLong(llele);
    31. } else {
    32. objele = createStringObject(ele,sdslen(ele));
    33. }
    34. /* Try to add the object to the dictionary. If it already exists
    35. * free it, otherwise increment the number of objects we have
    36. * in the result dictionary. */
    37. if (dictAdd(d,objele,NULL) == DICT_OK)
    38. added++;
    39. else
    40. decrRefCount(objele);
    41. }
    42. }
    43. /* CASE 3 & 4: send the result to the user. */
    44. {
    45. dictIterator *di;
    46. dictEntry *de;
    47. addReplyMultiBulkLen(c,count);
    48. di = dictGetIterator(d);
    49. while((de = dictNext(di)) != NULL)
    50. addReplyBulk(c,dictGetKey(de));
    51. dictReleaseIterator(di);
    52. dictRelease(d);
    53. }

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。虽然也有返回值,但并不会重复,并且个数小于期望值。

  1. void hrandmemberWithCountCommand(client *c, long l) {
  2. unsigned long entryCount, hashSize;
  3. int uniq = 1;
  4. hashTypeIterator *hi;
  5. robj *hash;
  6. dict *d;
  7. double randomDouble;
  8. double threshold;
  9. unsigned long index = 0;
  10. if ((hash = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp]))
  11. == NULL || checkType(c,hash,OBJ_HASH)) return;
  12. if(l >= 0) {
  13. entryCount = (unsigned long) l;
  14. } else {
  15. entryCount = -l;
  16. uniq = 0;
  17. }
  18. hashSize = hashTypeLength(hash);
  19. if(entryCount > hashSize)
  20. entryCount = hashSize;
  21. addReplyMapLen(c, entryCount);
  22. hi = hashTypeInitIterator(hash);
  23. if(hash->encoding == OBJ_ENCODING_ZIPLIST) {
  24. while (hashTypeNext(hi) != C_ERR && entryCount != 0) {
  25. randomDouble = ((double)rand()) / RAND_MAX;
  26. threshold = ((double)entryCount) / (hashSize - index);
  27. if(randomDouble < threshold){
  28. entryCount--;
  29. addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
  30. addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);
  31. }
  32. index ++;
  33. }
  34. } else {
  35. // copy of srandmember
  36. if(!uniq) {
  37. while(entryCount--) {
  38. sds key, value;
  39. dictEntry *de = dictGetRandomKey(hash->ptr);
  40. key = dictGetKey(de);
  41. value = dictGetVal(de);
  42. addReplyBulkCBuffer(c,key,sdslen(key));
  43. addReplyBulkCBuffer(c,value,sdslen(value));
  44. }
  45. return;
  46. }
  47. if(entryCount >= hashSize) {
  48. while (hashTypeNext(hi) != C_ERR) {
  49. addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
  50. addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);
  51. }
  52. return;
  53. }
  54. static dictType dt = {
  55. dictSdsHash, /* hash function */
  56. NULL, /* key dup */
  57. NULL, /* val dup */
  58. dictSdsKeyCompare, /* key compare */
  59. NULL, /* key destructor */
  60. NULL, /* val destructor */
  61. NULL /* allow to expand */
  62. };
  63. d = dictCreate(&dt,NULL);
  64. if(entryCount * HRANDMEMBER_SUB_STRATEGY_MUL > hashSize) {
  65. /* Add all the elements into the temporary dictionary. */
  66. while((hashTypeNext(hi)) != C_ERR) {
  67. int ret = DICT_ERR;
  68. sds key, value;
  69. key = hashTypeCurrentFromHashTable(hi,OBJ_HASH_KEY);
  70. value = hashTypeCurrentFromHashTable(hi,OBJ_HASH_VALUE);
  71. ret = dictAdd(d, key, value);
  72. serverAssert(ret == DICT_OK);
  73. }
  74. serverAssert(dictSize(d) == hashSize);
  75. /* Remove random elements to reach the right count. */
  76. while(hashSize > entryCount) {
  77. dictEntry *de;
  78. de = dictGetRandomKey(d);
  79. dictDelete(d,dictGetKey(de));
  80. hashSize--;
  81. }
  82. }
  83. else {
  84. unsigned long added = 0;
  85. sds sdsKey, sdsVal;
  86. while(added < entryCount) {
  87. dictEntry *de = dictGetRandomKey(hash->ptr);
  88. sdsKey = dictGetKey(de);
  89. sdsVal = dictGetVal(de);
  90. /* Try to add the object to the dictionary. If it already exists
  91. * free it, otherwise increment the number of objects we have
  92. * in the result dictionary. */
  93. if (dictAdd(d,sdsKey,sdsVal) == DICT_OK){
  94. added++;
  95. }
  96. }
  97. }
  98. {
  99. dictIterator *di;
  100. dictEntry *de;
  101. di = dictGetIterator(d);
  102. while((de = dictNext(di)) != NULL) {
  103. sds key = dictGetKey(de);
  104. sds value = dictGetVal(de);
  105. addReplyBulkCBuffer(c,key,sdslen(key));
  106. addReplyBulkCBuffer(c,value,sdslen(value));
  107. }
  108. dictReleaseIterator(di);
  109. dictRelease(d);
  110. }
  111. }
  112. hashTypeReleaseIterator(hi);
  113. }

参考文献

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. Free-Form Image Inpainting with Gated Convolution

    Free-Form Image Inpainting with Gated Convolution pytorch 引言 和Generative Image Inpainting with Conte ...

  2. 【linux】i2c使用分析&源码实战

    目录 前言 1. 设备检查命令 1.1 查看I2C驱动 1.2 i2c-tools 1.2.1 I2C-detect安装 1.2.2 i2cdetect 命令 1.2.3 i2cget 命令 1.2. ...

  3. 测试:ADB

    配置 JAVA: 1.安装jdk的按抓包(傻瓜式安装不需要更改路径) 2.我的电脑右击属性--高级系统设置--环境变量--用户变量--新建:JAVA_HOME C:\Program Files\Jav ...

  4. Linux无法新增用户

    1.查看当前用户是否有权限创建用户 2.磁盘空间不足,vi打开/etc/passwd 报: E297: Write error in swap file"adduser.sh" 1 ...

  5. Spring Boot 2.4发布了,但Spring Cloud用户不推荐着急升级

    前段时间Spring Boot发布了本年度最后一个重要更新版本:Spring Boot 2.4.0. 最近在社群里也开始有讨论关于Spring Boot 2.4的一些使用问题.我发现有很多Spring ...

  6. 探究 | 如何捕获一个Activity页面上所有的点击行为

    前言 最近逛wanAndroid论坛,发现一个有趣的问题:如何捕获一个Activity页面上所有的点击行为. 一起研究下吧,不想看源码的小伙伴可以直接看文末总结- 准备工作 先得罗列出页面上的一些点击 ...

  7. Spring Cloud 学习 (十) Spring Security, OAuth2, JWT

    通过 Spring Security + OAuth2 认证和鉴权,每次请求都需要经过 OAuth Server 验证当前 token 的合法性,并且需要查询该 token 对应的用户权限,在高并发场 ...

  8. day1(Django路径问题)

    1.python中的三种路径 1.1 操作系统文件绝对路径 django 静态文件查找, 模板查找(第一种) # 去配置好的 文件夹 中查找指定的文件 BASE_DIR = os.path.dirna ...

  9. moviepy音视频剪辑:AudioClip的max_volume方法报TypeError: bad operand type for abs(): ‘list‘错

    ☞ ░ 前往老猿Python博文目录 ░ 一.环境 操作系统:win7 64位 moviepy:1.0.3 numpy:1.19.0 Python:3.7.2 二.应用代码及报错信息 应用代码 imp ...

  10. 第8.22节 Python案例详解:重写 “富比较”方法控制比较逻辑

    一. 案例说明 本节定义一个小汽车的类Car,类中包括车名carname.百公里油耗oilcostper100km.价格price三个属性.然后实现__lt__.__gt__.__le__.__ge_ ...