redis数据结构---intset

        这两天看过《redis设计与实现》中的intset一章,配合着redis 4.0.1中的源码,感觉自己对redis中的intset有了新的认识。在这里写下自己对于intset的理解,以及redis源码中的实现技巧。

        intset作为redis中集合的底层实现之一,当集合中的元素数量不太多且都是整数值的时候,set使用intset作为其底层实现。个人认为,redis中intset的重要部分在于intset对于存储不同类型的整数使用不同的底层数组,巧妙的节约了内存的空间、提供了较高的灵活性。redis源码中对于intset的实现也比较简单,下面对自己在阅读源码的时候,当时不太熟悉的部分做一个简要的摘录。从redis中实现intset的基本数据结构和redis中数据类型的升级两方面进行介绍。

1、redis中intset数据结构
  typedef struct intset {
      uint32_t encoding;
      uint32_t length;
      /* 虽然这里声明的是int8_t类型的数组,但是真正存储的元素的类型还是要依赖于encoding中的说明来确定 */
      int8_t contents[];
  } intset;

        这里对struct intset需要说明的一点是,虽然contents的类型定义的是int8_t,但是其实际存储的数据的类型需要靠encoding指明,encoding的值为contents中存储的最大类型的值的类型。在redis的实现代码中定义了三种类型,分别如下。

  #define INTSET_ENC_INT16 (sizeof(int16_t))
  #define INTSET_ENC_INT32 (sizeof(int32_t))
  #define INTSET_ENC_INT64 (sizeof(int64_t))
        从上面三种宏定义中可以看出redis为底层的contents提供了三种可选的类型,分别为INTSET_ENC_INT16、INTSET_ENC_INT32和INTSET_ENC_INT64,三种类型的值分别以对应的整数类型的值所占用的字节数。
2、redis中数据类型的升级

        首先,创建一个空的redis中的intset集合使用的函数intsetNew中,创建的类型为INTSET_ENC_INT16类型,这种类型在放入的整数处于-32768~32767的时候,都是没有任何问题的。但是,假如要放入一个65535的整数值,那么int16_t类型将会溢出,所以此时intset要执行数据类型的升级操作。

        在redis中只有执行插入value的时候才会执行升级操作。根据升级操作的条件---当前集合的数据类型无法表示value的大小,因此新插入的value的类型存在两种情况导致了intset的升级。

        ·value小于当前encoding表示的类型能够表示的最小值;

        ·value大于当前encoding表示的类型能够表示的最大值;

        有且只有以上两种情况才会导致intset的升级操作。那么如果是第一种情况的时候,要插入的value应该处于的位置(pos)应该是contents中下标为0的地方;如果是第二种情况,要插入的value应该位于contents中下标为当前的length的位置上。总之,如果由于插入value导致intset中数据类型的升级,那么value要么位于intset中contents中的最开始的位置,要么位于最后的位置。

        当然,还需要说明的是,redis不会由于删除元素而降级。比如在上面插入了65535值之后,intset的encoding肯定是INTSET_ENC_INT32。但是,如果删除了65535,intset的encoding不会降级为INTSET_ENC_INT16。

        下面,我们看redis中源码是如何实现升级和插入操作的。

  /* 升级之后元素的摆放位置:之所以要进行intset的升级,完全是因为当前intset中的类型不能满足要添加的元素的范围类型。所以,升级之后新的元素要不是
  添加到intset的索引为0的位置(value < 0),要不就是添加到最后一个位置 */
  /* Upgrades the intset to a larger encoding and inserts the given integer. */
  static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
      /* 当前编码类型 */
      uint8_t curenc = intrev32ifbe(is->encoding);
      /* 要转换成的编码类型 */
      uint8_t newenc = _intsetValueEncoding(value);
      /* intset 原来的长度 */
      int length = intrev32ifbe(is->length);
      /* 如果是小于0,证明新要插入的值是整个intset中最小的值,那么在转移数据的过程中原来数据所在的pos+1才是新数据应该所在的pos; 如果 value > 0,   那么说明要插入的value是整个intset中最大的值,应该位于length的位置,其余元素在转移的过程中,保持其在原来intset中的pos即可 */
      /* 根据intset的升级方案: 只有当前数据的编码类型无法满足新插入的数据要求的时候才会考虑进行升级,所以新插入的数据,要么是在已有的intset的最
  前端,要么是在已有的intset的最末端 */
      int prepend = value < 0 ? 1 : 0;
  
      /* First set new encoding and resize */
      is->encoding = intrev32ifbe(newenc);
      is = intsetResize(is,intrev32ifbe(is->length)+1);
  
      /* Upgrade back-to-front so we don't overwrite values.
       * Note that the "prepend" variable is used to make sure we have an empty
       * space at either the beginning or the end of the intset. */
      /* 不同的encoding会改变数组内部读取值的开始位置和读取的长度 */
      /* _intsetGetEncode(is, length, curenc)通过传递的length和curenc确定旧的encoding中intset元素的位置和编码,按照编码和位置确定旧的length位置
  的值,并返回;_intsetSet(is, length+prepend, _intsetGetEncoded(is, length, curenc))中根据传入的值和当前的intset->encoding的值确定传入的值应该
  位于从开头算起的哪一bit */
      while(length--)
          _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
  
      /* Set the value at the beginning or the end. */
      if (prepend)
          _intsetSet(is,0,value);
      else
          _intsetSet(is,intrev32ifbe(is->length),value);
      is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
      return is;
}

        升级整数集合的步骤为:

        ·根据需要升级成的类型的长度或许需要的底层空间,并重新分配底层需要的内存空间;

        ·将已有的数据转换成升级成的类型的长度,并转移到合适的位置,转移的过程中保持intset集合的有序性不变;

        ·将新元素添加到新分配的整数集合中;

        在redis中intset中的元素都是按照“小端字节序"进行存储的。struct intset中的encoding和length以及contents中的每一个元素在实际的物理空间中都是转变成小端字节序,这是因为大多数的系统的在本地的存储方式都是”小端字节序"。如果系统本身是“大端字节序”的方式存储数据的,那么redis会将其转换成小端字节序进行存储(通过intrev32(16,64)ifbe函数)。如果系统本身是大端字节序存储,那么intrev16(32,64)ifbe的作用是翻转一个整数的所有字节;如果系统本身是小端字节序,那么intrev16(32,64)ifbe不做任何事情。你可以看到在redis的intset.c文件中,所有的获取当前的intset的encoding和length值的时候,使用intrev32ifbe对其进行操作,这是因为每次存储这些元素时候,都是通过intrev32ifbe进行转换过的。取用的时候不过是将其还原成原来的整数而已,否则可能会因为字节序的不同导致出现问题。

        整体而言,redis中关于整数集合的实现比较简单,这里只是记录了自己当时不太熟悉的地方,欢迎吐槽。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值