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 的操作以及运算
1 | SADD key member1 [member2] 向集合添加一个或多个成员 |
2 | SCARD key 获取集合的成员数 |
3 | SDIFF key1 [key2] 返回给定所有集合的差集 |
4 | SDIFFSTORE destination key1 [key2] 返回给定所有集合的差集并存储在 destination 中 |
5 | SINTER key1 [key2] 返回给定所有集合的交集 |
6 | SINTERSTORE destination key1 [key2] 返回给定所有集合的交集并存储在 destination 中 |
7 | SISMEMBER key member 判断 member 元素是否是集合 key 的成员 |
8 | SMEMBERS key 返回集合中的所有成员 |
9 | SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合 |
10 | SPOP key 移除并返回集合中的一个随机元素 |
11 | SRANDMEMBER key [count] 返回集合中一个或多个随机数 |
12 | SREM key member1 [member2] 移除集合中一个或多个成员 |
13 | SUNION key1 [key2] 返回所有给定集合的并集 |
14 | SUNIONSTORE destination key1 [key2] 所有给定集合的并集存储在 destination 集合中 |
15 | SSCAN 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字节的符号整数,等等。内部的处理都是有区别的。