源码版本:4.0.1
源码位置:

1. 整数集合简介

intset是Redis内存数据结构之一,和之前的 sdsskiplistdictadlist 等通用数据相比,它是Redis特有的,用来实现Redis的Set结构(当元素较小且为数字类型时),它的特点有:

  • 元素类型只能为数字。
  • 元素有三种类型:int16_t、int32_t、int64_t。
  • 元素有序,不可重复。
  • intset和sds一样,内存连续,就像数组一样。

2. 数据结构定义

  1. typedef struct intset {
  2. uint32_t encoding; // 编码类型 int16_t、int32_t、int64_t
  3. uint32_t length; // 长度 最大长度:2^32
  4. int8_t contents[]; // 柔性数组
  5. } intset;

3. 创建、插入(扩缩容)、查找(二分查找)、删除

以下面这个例子来看下intset的各种操作:

(需要自己在server.c中添加intset.h头文件,然后将main函数修改成下面代码)

  1. int main(int argc, char **argv) {
  2. uint8_t ret;
  3. uint8_t success;
  4. int64_t value;
  5. int16_t int16_a = 2 * 128;
  6. int16_t int16_b = 2 * 256;
  7. int32_t int32_c = 2 * 65536;
  8. printf("----------intset insert----------\n");
  9. intset *is = intsetNew();
  10. is = intsetAdd(is, int16_a, &success);
  11. if (success == 0) {
  12. printf("add int16_a fail\n");
  13. } else {
  14. printf("add int16_a success, ");
  15. }
  16. printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is));
  17. is = intsetAdd(is, int32_c, &success);
  18. if (success == 0) {
  19. printf("add int32_c fail\n");
  20. } else {
  21. printf("add int32_c success, ");
  22. }
  23. printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is));
  24. is = intsetAdd(is, int16_b, &success);
  25. if (success == 0) {
  26. printf("add int16_b fail\n");
  27. } else {
  28. printf("add int16_b success, ");
  29. }
  30. printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is));
  31. printf("----------intset found----------\n");
  32. ret = intsetFind(is, int16_b);
  33. if (ret == 1) {
  34. printf("int16_b is found\n");
  35. }
  36. printf("----------intset get----------\n");
  37. ret = intsetGet(is, 0, &value);
  38. if (ret != 0) {
  39. printf("int16_a get value is %lld\n", value);
  40. }
  41. printf("----------intset remove----------\n");
  42. is = intsetRemove(is, int16_b, &success);
  43. if (success == 1) {
  44. printf("int16_b is success remove\n");
  45. }
  46. printf("is encoding:%d, length:%d, bloblen:%zu\n", is->encoding, intsetLen(is), intsetBlobLen(is));
  47. zfree(is);
  48. return 0;
  49. }
  50. Out >
  51. ----------intset insert----------
  52. add int16_a success, is encoding:2, length:1, bloblen:10
  53. add int32_c success, is encoding:4, length:2, bloblen:16
  54. add int16_b success, is encoding:4, length:3, bloblen:20
  55. ----------intset found----------
  56. int16_b is found
  57. ----------intset get----------
  58. int16_a get value is 256
  59. ----------intset remove----------
  60. int16_b is success remove
  61. is encoding:4, length:2, bloblen:16

3.1 创建

  • intset *is = intsetNew(),创建了一个空的名为is的intset,代码如下:
  1. /* Create an empty intset. */
  2. intset *intsetNew(void) {
  3. intset *is = zmalloc(sizeof(intset)); // 分配空间
  4. is->encoding = intrev32ifbe(INTSET_ENC_INT16); // 初试创建默认元素大小为 2 字节
  5. is->length = 0;
  6. return is;
  7. }

3.2 插入

  • 接下来我们调用intsetAdd()连续插入了三次数据,它的代码如下:
  1. /* Insert an integer in the intset */
  2. intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
  3. uint8_t valenc = _intsetValueEncoding(value);
  4. uint32_t pos;
  5. if (success) *success = 1;
  6. /* Upgrade encoding if necessary. If we need to upgrade, we know that
  7. * this value should be either appended (if > 0) or prepended (if < 0),
  8. * because it lies outside the range of existing values. */
  9. if (valenc > intrev32ifbe(is->encoding)) {
  10. /* This always succeeds, so we don't need to curry *success. */
  11. return intsetUpgradeAndAdd(is,value);
  12. } else {
  13. /* Abort if the value is already present in the set.
  14. * This call will populate "pos" with the right position to insert
  15. * the value when it cannot be found. */
  16. if (intsetSearch(is,value,&pos)) {
  17. if (success) *success = 0;
  18. return is;
  19. }
  20. is = intsetResize(is,intrev32ifbe(is->length)+1);
  21. if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
  22. }
  23. _intsetSet(is,pos,value);
  24. is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
  25. return is;
  26. }

整个函数的流程如下:

  • uint8_t valenc = _intsetValueEncoding(value),根据value的长度获取其对应的编码,保存至valenc。
  • if (valenc > intrev32ifbe(is->encoding)),如果valenc > is->encoding,表明目前的encoding太小,需要整体提高encoding的大小。
    • 执行intsetUpgradeAndAdd()完成扩大操作。
  • 如果valenc <= is->encoding。
    • 执行查找intsetSearch(is,value,&pos),如果查找到元素,将success置为0,表示插入失败,即此元素已经存在。
    • 如果没有查找到,pos表示元素应该插入的位置,则给is扩容一个元素的大小intsetResize(is,intrev32ifbe(is->length)+1),如果需要则使用intsetMoveTail(is,pos,pos+1)将元素挪移。
  • _intsetSet(is,pos,value),将元素插入intset。
  • is->length = intrev32ifbe(intrev32ifbe(is->length)+1),更新length的值。

插入了第一个元素int16_ais如下图所示:

与输出结果相对应:

  1. add int16_a success, is encoding:2, length:1, bloblen:10
  • 接下来我们的代码添加了第二个元素,由于它的大小超过了INTSET_ENC_INT16,所以添加操作会执行intsetUpgradeAndAdd()函数扩大encoding:
  1. /* Upgrades the intset to a larger encoding and inserts the given integer. */
  2. static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
  3. uint8_t curenc = intrev32ifbe(is->encoding);
  4. uint8_t newenc = _intsetValueEncoding(value);
  5. int length = intrev32ifbe(is->length);
  6. int prepend = value < 0 ? 1 : 0;
  7. /* First set new encoding and resize */
  8. is->encoding = intrev32ifbe(newenc);
  9. is = intsetResize(is,intrev32ifbe(is->length)+1);
  10. /* Upgrade back-to-front so we don't overwrite values.
  11. * Note that the "prepend" variable is used to make sure we have an empty
  12. * space at either the beginning or the end of the intset. */
  13. while(length--)
  14. _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
  15. /* Set the value at the beginning or the end. */
  16. if (prepend)
  17. _intsetSet(is,0,value);
  18. else
  19. _intsetSet(is,intrev32ifbe(is->length),value);
  20. is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
  21. return is;
  22. }
  • 将is目前的encoding保存至curenc,将value的encoding保存至newenc。
  • int prepend = value < 0 ? 1 : 0,prepend用来确定新value的插入位置:第一个还是最后一个,因为它的encoding比is->encoding要大,所以它要么比目前所有元素都大,要么比所有元素都小,即插入位置要么第一个,要么最后一个。
  • 然后更新encoding的值,重新分配空间。
  • 挪动所有的元素到新位置。
  • 根据prepend的值判断将value插入第一个位置还是最后一个位置。
  • 更新is->length。

有一个比较生动的图解如下,参考[1]:

  1. /* Upgrade back-to-front so we don't overwrite values.
  2. * Note that the "prepend" variable is used to make sure we have an empty
  3. * space at either the beginning or the end of the intset. */
  4. // 根据集合原来的编码方式,从底层数组中取出集合元素
  5. // 然后再将元素以新编码的方式添加到集合中
  6. // 当完成了这个步骤之后,集合中所有原有的元素就完成了从旧编码到新编码的转换
  7. // 因为新分配的空间都放在数组的后端,所以程序先从后端向前端移动元素
  8. // 举个例子,假设原来有 curenc 编码的三个元素,它们在数组中排列如下:
  9. // | x | y | z |
  10. // 当程序对数组进行重分配之后,数组就被扩容了(符号 ? 表示未使用的内存):
  11. // | x | y | z | ? | ? | ? |
  12. // 这时程序从数组后端开始,重新插入元素:
  13. // | x | y | z | ? | z | ? |
  14. // | x | y | y | z | ? |
  15. // | x | y | z | ? |
  16. // 最后,程序可以将新元素添加到最后 ? 号标示的位置中:
  17. // | x | y | z | new |
  18. // 上面演示的是新元素比原来的所有元素都大的情况,也即是 prepend == 0
  19. // 当新元素比原来的所有元素都小时(prepend == 1),调整的过程如下:
  20. // | x | y | z | ? | ? | ? |
  21. // | x | y | z | ? | ? | z |
  22. // | x | y | z | ? | y | z |
  23. // | x | y | x | y | z |
  24. // 当添加新值时,原本的 | x | y | 的数据将被新值代替
  25. // | new | x | y | z |

插入了第二个元素之后is如下图所示:

输出如下所示:

  1. add int32_c success, is encoding:4, length:2, bloblen:16
  • 接下来我们插入第三个元素,此时的encoding满足int16_b的大小,所以代码分支去执行查找操作intsetSearch()函数:
  1. /* Search for the position of "value". Return 1 when the value was found and
  2. * sets "pos" to the position of the value within the intset. Return 0 when
  3. * the value is not present in the intset and sets "pos" to the position
  4. * where "value" can be inserted. */
  5. static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
  6. int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
  7. int64_t cur = -1;
  8. /* The value can never be found when the set is empty */
  9. if (intrev32ifbe(is->length) == 0) {
  10. if (pos) *pos = 0;
  11. return 0;
  12. } else {
  13. /* Check for the case where we know we cannot find the value,
  14. * but do know the insert position. */
  15. if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
  16. if (pos) *pos = intrev32ifbe(is->length);
  17. return 0;
  18. } else if (value < _intsetGet(is,0)) {
  19. if (pos) *pos = 0;
  20. return 0;
  21. }
  22. }
  23. while(max >= min) {
  24. mid = ((unsigned int)min + (unsigned int)max) >> 1; // 加法运算级别比移位高
  25. cur = _intsetGet(is,mid);
  26. if (value > cur) {
  27. min = mid+1;
  28. } else if (value < cur) {
  29. max = mid-1;
  30. } else {
  31. break;
  32. }
  33. }
  34. if (value == cur) {
  35. if (pos) *pos = mid;
  36. return 1;
  37. } else {
  38. if (pos) *pos = min;
  39. return 0;
  40. }
  41. }
  • 如果目前is->length为0,则标记pos为0,并且返回查找失败。
  • 如果value比最大值还大,或者比最小值还小,标记pos为length或者0,返回查找失败。
  • 否则使用二分法查找到元素,将pos指向应当插入的位置。

等到intsetSearch()返回之后,pos表示value应当插入的位置,此时需要挪动pos之后的元素向后一个位置,挪动函数是intsetMoveTail()

  1. static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
  2. void *src, *dst;
  3. uint32_t bytes = intrev32ifbe(is->length)-from;
  4. uint32_t encoding = intrev32ifbe(is->encoding);
  5. if (encoding == INTSET_ENC_INT64) {
  6. src = (int64_t*)is->contents+from;
  7. dst = (int64_t*)is->contents+to;
  8. bytes *= sizeof(int64_t);
  9. } else if (encoding == INTSET_ENC_INT32) {
  10. src = (int32_t*)is->contents+from;
  11. dst = (int32_t*)is->contents+to;
  12. bytes *= sizeof(int32_t);
  13. } else {
  14. src = (int16_t*)is->contents+from;
  15. dst = (int16_t*)is->contents+to;
  16. bytes *= sizeof(int16_t);
  17. }
  18. memmove(dst,src,bytes);
  19. }

实际上是把内存整体向后移动了一个元素的位置,需要注意的是 memmove 函数允许src和dst之间的内存有重叠部分。

再来一段生动的图解,同样出自参考[1]:

  1. /*
  2. * 向前或先后移动指定索引范围内的数组元素
  3. *
  4. * 函数名中的 MoveTail 其实是一个有误导性的名字,
  5. * 这个函数可以向前或向后移动元素,
  6. * 而不仅仅是向后
  7. *
  8. * 在添加新元素到数组时,就需要进行向后移动,
  9. * 如果数组表示如下(?表示一个未设置新值的空间):
  10. * | x | y | z | ? |
  11. * |<----->|
  12. * 而新元素 n 的 pos 为 1 ,那么数组将移动 y 和 z 两个元素
  13. * | x | y | y | z |
  14. * |<----->|
  15. * 接着就可以将新元素 n 设置到 pos 上了:
  16. * | x | n | y | z |
  17. *
  18. * 当从数组中删除元素时,就需要进行向前移动,
  19. * 如果数组表示如下,并且 b 为要删除的目标:
  20. * | a | b | c | d |
  21. * |<----->|
  22. * 那么程序就会移动 b 后的所有元素向前一个元素的位置,
  23. * 从而覆盖 b 的数据:
  24. * | a | c | d | d |
  25. * |<----->|
  26. * 最后,程序再从数组末尾删除一个元素的空间:
  27. * | a | c | d |
  28. * 这样就完成了删除操作。
  29. *
  30. * T = O(N)
  31. */

此时的is如下图所示:

3.3 查找

查找的逻辑在上面插入操作时候已经说到了,实际上是二分查找

3.4 删除

  1. /* Delete integer from intset */
  2. intset *intsetRemove(intset *is, int64_t value, int *success) {
  3. uint8_t valenc = _intsetValueEncoding(value);
  4. uint32_t pos;
  5. if (success) *success = 0;
  6. if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
  7. uint32_t len = intrev32ifbe(is->length);
  8. /* We know we can delete */
  9. if (success) *success = 1;
  10. /* Overwrite value with tail and update length */
  11. if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
  12. is = intsetResize(is,len-1);
  13. is->length = intrev32ifbe(len-1);
  14. }
  15. return is;
  16. }
  • 首先获取元素的encoding,如果不符合条件,success为0表示删除失败。
  • 否则调用intsetSearch()查找到相应的位置
  • 然后将pos+1的元素移动到pos位置上,相当于向前覆盖一个元素。
  • 将元素个数减一,重新分配内存。

4. 总结

本篇博客分析了intset的数据结构以及基本操作,整个数据结构还是比较简单的。
个人觉得intset实现按照元素不断增大可以扩大encoding对内存非常友好,但是它没有提供对应的减小encoding操作,即可以一直扩大encoding编码类型,但是不能缩小,这一点不太好。

参考资料:
[1] Redis源码注释3.0-黄健宏

[完]

Redis源码分析(intset)的更多相关文章

  1. Redis源码分析:serverCron - redis源码笔记

    [redis源码分析]http://blog.csdn.net/column/details/redis-source.html   Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...

  2. redis源码分析之事务Transaction(下)

    接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...

  3. Redis源码分析系列

    0.前言 Redis目前热门NoSQL内存数据库,代码量不是很大,本系列是本人阅读Redis源码时记录的笔记,由于时间仓促和水平有限,文中难免会有错误之处,欢迎读者指出,共同学习进步,本文使用的Red ...

  4. redis源码分析之发布订阅(pub/sub)

    redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点. 原文地址:htt ...

  5. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  6. redis源码分析之有序集SortedSet

    有序集SortedSet算是redis中一个很有特色的数据结构,通过这篇文章来总结一下这块知识点. 原文地址:http://www.jianshu.com/p/75ca5a359f9f 一.有序集So ...

  7. Redis源码分析(dict)

    源码版本:redis-4.0.1 源码位置: dict.h:dictEntry.dictht.dict等数据结构定义. dict.c:创建.插入.查找等功能实现. 一.dict 简介 dict (di ...

  8. redis源码分析

    我阅读的源码版本是redis-2.8.19 src目录下总共96个.h,.c文件 1. 数据结构相关源码(15个左右)字符串代码: sds.h, sds.c字典:dict.h, dict.c链表: a ...

  9. redis源码分析(一)-sds实现

    redis支持多种数据类型,sds(simple dynamic string)是最基本的一种,redis中的字符串类型大多使用sds保存,它支持动态的扩展与压缩,并提供许多工具函数.这篇文章将分析s ...

随机推荐

  1. Docker系列(26)- 发布镜像到阿里云容器服务

    1.登录阿里云 2.找到容器镜像服务 3.创建命名空间 4.创建镜像仓库 5.上传镜像

  2. Linux系列(1) - 使用Hyper-V安装Linux系统

    前言 使用工具:Hyper-V,补充:hyper-v是一项技术,而vmware是一款软件.具体区别自行百度 LINUX版本:CentOS-8.4.2105-x86_64-dvd1.iso Window ...

  3. PKI及SSL协议分析PKI及SSL协议分析

    任务一:搭建CA服务器 本任务初步了解CA服务器的原理和配置过程.操作都在CA服务器上. 1.远程桌面方式登录到CA服务器,在CMD下查看本机IP地址: 2.安装证书服务 依次点击:"开始& ...

  4. 自学 Python,视频教程和代码一看就懂,动手就废,应该这么学

    ​ 一.代码量太少了,看得多做得少,导致一做就错. 每一个测试工程师必定是在大量的时间和代码中提升的自己,如果你只是看视频的话,那永远都停留在理论上,很多问题是要实践才能发现的 我打个比方你看视频的时 ...

  5. windows下将Anaconda移位置(C盘转移至D盘)

    1.首先说明一下我的安装情况: (1)Window10系统 (2)Anaconda安装在C盘 2.由于Anaconda安装在C盘经常会涉及权限问题,在安装torch的时候让我萌发了转移磁盘的想法(拒绝 ...

  6. 【k8s】使用k8s部署一个简单的nginx服务

    名词解释 Namespace 表示命名空间 Deployment 表示pod发布 Service 表示多个pod做为一组的集合对外通过服务的表示 kubectl 是k8s的命令行操作命令,可以创建和更 ...

  7. SPA测试

    1.生产端:环境准备为了进行SPA测试,在生产数据库中创建了SPA测试专用用户,避免与其他用户相互混淆与可能产生的误操作. CREATE USER SPA IDENTIFIED BY SPA DEFA ...

  8. Sentry 监控 - Snuba 数据中台架构(Query Processing 简介)

    系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Maps Sentry For ...

  9. 打开属性页,分别在Debug和Release下将其配置类型改为:静态库(.lib);

    右键工程->属性 配置类型里面的下拉菜单选择静态库

  10. docker采用registry部署简易仓库

    解释:registry部署简易仓库,实现免密上传拉取镜像(解决不在一个容器里,也能够实现镜像拉取成功) 1.安装启动registry服务 docker pull registry docker run ...