Redis Source Code Read Log( 9 set )

Redis内部提供了set数据类型

介绍

set 数据类型是一个集合(没有排序,不重复),可以对 set 类型的数据进行添加、删除、判断是否存在等操作

set 集合不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份。

set 类型提供了多个 set 之间的聚合运算,如求交集、并集、补集,这些操作在 redis 内部完成,效率很高。

set 对数据类型要求是string类型,但是针对可以转换成 整型 的string,set内部对其有特殊的处理。此时,无序的set不再无序,内部实际是有序的(升序)。

特殊处理,纯整型元素的 set, intset

Redis set内部对于纯整型元素的set进行了特殊处理,以提升内存利用率。

声明

说明

intset 是一段连续的空间,编码方式确定了之后,entry取值方式固定。其中有8个字节的固定头部,后面紧跟整型数字向量(采用int8_t类型数据向量存储)

头部含两个域:

编码方式域,encoding,标记内部实际采用的编码方式,一共三种:

#define INTSET_ENC_INT16 (sizeof(int16_t))  // 2

#define INTSET_ENC_INT32 (sizeof(int32_t))  // 4

#define INTSET_ENC_INT64 (sizeof(int64_t))  // 8

种编码方式,实际使用有优先级,INT16 > INT32 > INT64.

当新增的数超过当前的编码范围,内部编码方式切换,便采用次优先级的编码方案,并触发整个 intset 内部的数据 re-encoding ,注意,编码方式的转换小小范围逐步按需到大范围进行的,且不可逆

entry 数量域,length,标记内部实际存储的entry(整型数)个数。

payload 存储contents 向量中。

intset 一直是有序的,内部一直保持着升序排列

空间占用:

编码方式 * 元素个数 + 8个字节header

e.g. 内部存储的全是 signed short 类型的数据 int16_t,那么采用 INT16_T 编码,共计40个元素,空间占用 2 * 40 + 8 = 88 字节。

时间复杂度:

查询:内部采用二分查找方式,查询复杂度为 O(logN)

增加:二分查找位置,即 O(logN) 外加 realloc 内存 + memove 的操作

删除:二分查找位置,即 O(logN) 外加 memove 操作

修改:无修改功能

增加触发的 re-encoding:

       整体复杂度 O(N) 因为内部所有元素需要逐一进行迁移,而无法直接采用整体 memove

dict 类型 set

dict type

注意,以dict形式作为存储结构的set,是只有key而无value类型的字典。

以dict形式存储的dict,相关操作的时间复杂度,都在 O(1)  级别。

切换

1. 当某侧 SADD* 的操作,插入的新值,不再是 整型,触发 type convert

2. 当元素数量超过配置值,set-max-intset-entries ,默认配置是512.

以上条件,任何一条满足,都会触发切换操作。切换会将 intset 中的整型转换成string形式,存入新建的dict中。

此时的转换操作

第一. dict在创建之初,就会有一次 expend 操作,取一个 大于或等于 当前元素个数的  2的次方数,作为此时dict初始化的 SLOT size。

第二. 此时将以同步的方式直接将 intset 中的全量元素遍历出来,依次插入到新建的 dict 中

SET 的操作以及运算

1SADD key member1 [member2]
向集合添加一个或多个成员
2SCARD key
获取集合的成员数
3SDIFF key1 [key2]
返回给定所有集合的差集
4SDIFFSTORE destination key1 [key2]
返回给定所有集合的差集并存储在 destination 中
5SINTER key1 [key2]
返回给定所有集合的交集
6SINTERSTORE destination key1 [key2]
返回给定所有集合的交集并存储在 destination 中
7SISMEMBER key member
判断 member 元素是否是集合 key 的成员
8SMEMBERS key
返回集合中的所有成员
9SMOVE source destination member
将 member 元素从 source 集合移动到 destination 集合
10SPOP key
移除并返回集合中的一个随机元素
11SRANDMEMBER key [count]
返回集合中一个或多个随机数
12SREM key member1 [member2]
移除集合中一个或多个成员
13SUNION key1 [key2]
返回所有给定集合的并集
14SUNIONSTORE destination key1 [key2]
所有给定集合的并集存储在 destination 集合中
15SSCAN key cursor [MATCH pattern] [COUNT count]
迭代集合中的元素

内部算法实现

1. 交集

SINTER sk1 sk2 sk3 ...

SINTERSTORE dst sk1 sk2 sk3 ...

Redis 允许内部多个集合同时求 交集.

内部算法实现:

第一步:逐步取各个 key 值对应的 set 对象指针,并且同步校验有效性。

对于查询key值返回空,视为空集处理。此时直接返回,因为多个集合与空集交集即为空集。

对于查询key值返回的结果为非 set类型的,返回类型错误。

第二步:针对各个集合,按照其内部的元素个数,进行快速排序,按照升序排列,排列之后,进行第三步的两层循环效率更高。

第三步:

外层循环,第一个,也就是个数最小的set的中进行元素遍历,逐个取 x。

    内层循环,将外层循环的元素,依次去与后面的set进行判断:

        if  x  ismember return false, break 内层循环。

        如果内层循环正常full循环结束退出,该元素插入结果集。

两层循环结束之后,查看结果集。

结果集默认选择 intset 类型,当src的set中有intset,那么,结果集必然是intset。

如果匹配的元素是 string 类型,那么元素插入到结果集便会触发结果集 set 的 type convert。

为什么各个set按照size升序排列,效率更高?

1. 外层循环,直接遍历最小size集合的元素,那么,循环次数最少。

2. 内存循环,ismember的判断,遇到size较小的集合,更容易返回false,以便更快的进入到外层循环进行下一个元素的判断。

第四步:处理结果集

第一种情况:不带目标 dstkey,如SINTER 命令,返回最终结果。

第二种情况:带目标 dstkey,那么 dstkey 不论是什么类型,都会从数据库中被删除,然后以 dstkey为key 以结果集set为value,将插入进数据库中。注意,原先已经存在的 dstkey会从数据库中删除掉。所以 STORE 命令要注意对原有数据的影响。这是一种忽略类型的覆盖操作。

2. 差集

SDIFF sk1 sk2 ...

注意差集的定义,之前用某度,里面许多都是说错了的,并且,多个集合的差集,用这种错误的观点,根本无法解释。

A B 的 差集,定义是  {x 属于 A 且 x 不属于 B},记为 A - B。

A - B - C 就是 A - B 的结果 再 - C。

如上图, A B 差集 A - B = 1 + 4,记为 R

A - B - C 也就是 R - C = 1.

所以,A B 的差集,跟 B A 的差集,不是一回事。被减集合 与 减集合 位置是不能交换的

所以,差集的方式就好办了,参数中,第一个集合,被减集合。后面的都是减集合。

无论如果,结果中的元素,必然都是 被减集合 中的元素,必然都不是其他任意 减集合 中的元素

这句话,其实就是 Redis 内部 SDIFF 命令计算差集的一种算法。

Redis 计算差集的算法

算法一:

外层循环 遍历 被减集合 A,取元素 x

      内层循环,减集合B, C, D逐个判断

          if  x  isMember B/C/D return true, break;

      如果内层循环中正常结束(不是break), 那么 x 添加入 结果集。

算法二:

第一步,duplacte 一份 被减集合 A, aux集合(辅助集合),遍历,取每一个元素。

第二步,遍历 减集合  B/C/D,依次取其中的元素 x

if  x isMember of aux,  remove x from aux.

结束之后,aux 便是最终的结果集。

算法一复杂度: A size N,减集合个数 M, 那么 总体复杂度 O(N * M)

算法二复杂度:A 与 B,C,D.. 集合中的元素总数为 T, 那么总体复杂度 O(T)

两种算法,Redis是不允许客户端指定,而是 Redis server内部自行进行选择.

选择条件:

if N * M < T, 使用算法一. 否则,选择 算法二.

按照道理,应该是这样的。但是 由于 算法一 时间更加稳定,操作相对较少,所以 Give it some advantage。条件变成了:

if  N * M  <=  2 T, 使用算法一,否则使用 算法 二。

另外,针对第一种算法,Redis 按照 各个 减集合 的size 进行降序排列,以提升循环效率

因为,元素多的 减集合 (注意是减集合,不包含被减集合) 在前面,内层循环在做 ismember 判断时,有着更大的概率返回true,打断内层循环,从而更快的进入外层循环进行下一个元素的判断

其他集合操作,算法相对简单,此处略。

使用

set内部为了提高内存使用效率,对纯整型采用了 intset 的方式存储。

所以业务层面,纯整型元素的场景,对于纯整型,还有范围的概念。-32768 到 + 32767 以及 4字节的符号整数,等等。内部的处理都是有区别的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值