前一篇文章,我们学习的是 Redis的数据结构之hash, 学习了其基本的操作和使用内部数据结构是hashtable
和ziplist
,其中Redis
中的hash
是用 dict
表示的。如果不记得了其内部构成, 就再看看看着上篇文章吧。现在我们继续学习下一个数据类型 set
。
set
简介Redis
的 set
数据类型表示 一堆不重复值的集合。
Redis
的set
数据类型有两种编码方式. OBJ_ENCODING_INTSET
和 OBJ_ENCODING_HT
.
OBJ_ENCODING_HT
这种编码方式在上一篇文章 07-Redis的数据类型之hash.md中已经简单的介绍过了。其实现的数据结构为 dict
。更加深入的分享, 穿梭机.
OBJ_ENCODING_INTSET
,这种编码方式是我们要新学习的编码方式。 电梯直达
如果你看到这句话, 那就说明你是一个特别认真的人。哈哈哈,我们还是先遵循惯例。先学习set
类型相关的命令。
set
类型的应用场景qq
的好友推荐逻辑,就可以使用差集运算。IP
。set
的基本命令sadd
SADD key member [member ...]
set add
将一个或多个 member
元素加入到集合 key
当中,已经存在于集合的 member
元素将被忽略。
假如 key
不存在,则创建一个只包含 member
元素作成员的集合。
当 key
不是集合类型时,返回一个错误。
127.0.0.1:6379> sadd k52 mem1 mem2
(integer) 2
127.0.0.1:6379> sadd k52 mem1
(integer) 0
127.0.0.1:6379> sadd k52 mem1 mem3
(integer) 1
smembers
SMEMBERS key
set members
返回集合 key
中的所有成员。
不存在的 key
被视为空集合
# 查询元素, 注意保存是无序的.
127.0.0.1:6379> SADD k53 m1 m2 m3 m4 m5
(integer) 5
127.0.0.1:6379> SMEMBERS k53
1) "m4"
2) "m3"
3) "m2"
4) "m1"
5) "m5"
sismember
SISMEMBER key member
set is members
判断 member
元素是否集合 key
的成员。
如果 member
元素是集合的成员,返回 1
。 如果 member
元素不是集合的成员,或 key
不存在,返回 0
127.0.0.1:6379> SADD k54 m1 m2 m3 m4
(integer) 4
127.0.0.1:6379> SISMEMBER k54 m2
(integer) 1
127.0.0.1:6379> SISMEMBER k54 m5
(integer) 0
spop
SPOP key [count]
set pop
移除并返回集合中的 随机一个 元素。
127.0.0.1:6379> SADD k55 m1 m2 m3 m4 m5 m6 m7 m8 m9 m10
(integer) 10
# 随机移除一个元素
127.0.0.1:6379> spop k55
"m9"
127.0.0.1:6379> spop k55
"m2"
127.0.0.1:6379> spop k55
"m3"
# 随机移除3个元素
127.0.0.1:6379> spop k55 3
1) "m5"
2) "m4"
3) "m10"
# 查看所有元素
127.0.0.1:6379> SMEMBERS k55
1) "m1"
2) "m7"
3) "m8"
4) "m6"
srandmemeber
SRANDMEMBER key [count]
set rand member
返回集合中的 随机 count
个元素(不会删除元素)
如果 count
为正数,且小于集合基数,那么命令返回一个包含 count
个元素的数组,数组中的元素各不相同。如果 count
大于等于集合基数,那么返回整个集合。
如果 count
为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count
的绝对值。
127.0.0.1:6379> SADD k56 m1 m2 m3 m4 m5 m6 m7 m8 m9 m10
(integer) 10
# 随机返回一个元素
127.0.0.1:6379> SRANDMEMBER k56
"m1"
127.0.0.1:6379> SRANDMEMBER k56
"m9"
127.0.0.1:6379> SRANDMEMBER k56
"m5"
# count是正数,小于集合的元素数,返回count个元素,无重复元素
127.0.0.1:6379> SRANDMEMBER k56 5
1) "m7"
2) "m1"
3) "m5"
4) "m6"
5) "m3"
# count是正数,大于集合的元素数,返回整个集合
127.0.0.1:6379> SRANDMEMBER k56 20
1) "m5"
2) "m6"
3) "m4"
4) "m8"
5) "m7"
6) "m1"
7) "m10"
8) "m3"
9) "m2"
10) "m9"
# count为负数, 返回20个集合中的元素,元素会重复
127.0.0.1:6379> SRANDMEMBER k56 -20
1) "m6"
2) "m6"
3) "m9"
4) "m7"
5) "m6"
6) "m5"
7) "m6"
8) "m9"
9) "m1"
10) "m6"
11) "m1"
12) "m9"
13) "m2"
14) "m1"
15) "m2"
16) "m5"
17) "m9"
18) "m2"
19) "m4"
20) "m6"
srem
SREM key member [member ...]
set remove
移除集合 key
中的一个或多个 member
元素,不存在的 member
元素会被忽略。
当 key
不是集合类型,返回一个错误。
127.0.0.1:6379> SADD k57 m1 m2 m3 m4 m5 m6 m7 m8 m9 m10
(integer) 10
127.0.0.1:6379> SREM k57 m1 m2 m3
(integer) 3
127.0.0.1:6379> SMEMBERS k57
1) "m7"
2) "m8"
3) "m5"
4) "m6"
5) "m4"
6) "m10"
7) "m9"
127.0.0.1:6379> SREM k57 m11 m12
(integer) 0
smove
SMOVE source destination member
set move
将 member
元素从 source
集合移动到 destination
集合。
SMOVE
是原子性操作。
如果 source
集合不存在或不包含指定的 member
元素,则 SMOVE
命令不执行任何操作,仅返回 0
。否则, member
元素从 source
集合中被移除,并添加到 destination
集合中去。
当 destination
集合已经包含 member
元素时, SMOVE
命令只是简单地将 source
集合中的 member
元素删除。
当 source
或 destination
不是集合类型时,返回一个错误。
127.0.0.1:6379> SADD k58 m1 m2 m3 m4 m5 m6 m7 m8 m9 m10
(integer) 10
# 移动两个元素到k58_dis
127.0.0.1:6379> SMOVE k58 k58_dis m1
(integer) 1
127.0.0.1:6379> SMOVE k58 k58_dis m2
(integer) 1
127.0.0.1:6379> SMOVE k58 k58_dis m1
(integer) 0
# k58中的m1,m2 已被移除。
127.0.0.1:6379> SMEMBERS k58
1) "m7"
2) "m8"
3) "m5"
4) "m6"
5) "m4"
6) "m10"
7) "m3"
8) "m9"
# k58_dis中的m1,m2
127.0.0.1:6379> SMEMBERS k58_dis
1) "m2"
2) "m1"
scard
SCARD key
返回集合 key
的基数(集合中元素的数量)。
集合的基数。 当 key
不存在时,返回 0
127.0.0.1:6379> SADD k59 m1 m2 m3 m4 m5 m6 m7 m8 m9 m10
(integer) 10
# 获取元素个数
127.0.0.1:6379> SCARD k59
(integer) 10
sinter
SINTER key [key ...]
set intersection
: set
的交集
返回一个集合的全部成员,该集合是所有给定集合的交集。
不存在的 key
被视为空集。
当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。
127.0.0.1:6379> SADD k60_1 m1 m2 m3 m4 m5
(integer) 5
127.0.0.1:6379> SADD k60_2 m2 m3 m4 m5 m6
(integer) 5
127.0.0.1:6379> SADD k60_3 m4 m5 m6 m7 m8
(integer) 5
# 指定了一个key,返回集合的所有元素
127.0.0.1:6379> SINTER k60_1
1) "m4"
2) "m3"
3) "m2"
4) "m1"
5) "m5"
# 多个key的时候,返回集合的交集。
127.0.0.1:6379> SINTER k60_1 k60_2
1) "m4"
2) "m3"
3) "m2"
4) "m5"
# 多个key的时候,返回集合的交集。
127.0.0.1:6379> SINTER k60_1 k60_2 k60_3
1) "m4"
2) "m5"
# k60_4不存在,为空集
127.0.0.1:6379> SINTER k60_1 k60_4
(empty list or set)
sinterstore
SINTERSTORE destination key [key ...]
set intersection and store
这个命令类似于 SINTER key [key …] 命令,返回集合的交集。但它将结果保存到 destination 集合,而不是简单地返回结果集。
如果 destination 集合已经存在,则将其覆盖。
destination 可以是 key 本身。
127.0.0.1:6379> SADD k61_1 m1 m2 m3 m4 m5
(integer) 5
127.0.0.1:6379> SADD k61_2 m4 m5 m6 m7 m8
(integer) 5
# 将k61_1 和 k61_2 集合的交集存储到k61_dis中
127.0.0.1:6379> SINTERSTORE k61_dis k61_1 k61_2
(integer) 2
# 查看 k61_dis
127.0.0.1:6379> SMEMBERS k61_dis
1) "m4"
2) "m5"
# k61_1 和 k61_2 没有变化
127.0.0.1:6379> SMEMBERS k61_1
1) "m4"
2) "m3"
3) "m2"
4) "m1"
5) "m5"
127.0.0.1:6379> SMEMBERS k61_2
1) "m7"
2) "m8"
3) "m5"
4) "m6"
5) "m4"
# 如果目标集合(k61_dis)存在,元素会被覆盖掉。
127.0.0.1:6379> SADD k61_3 m1 m2 m3
(integer) 3
127.0.0.1:6379> SINTERSTORE k61_dis k61_1 k61_3
(integer) 3
127.0.0.1:6379> SMEMBERS k61_dis
1) "m1"
2) "m2"
3) "m3"
sunion
SUNION key [key ...]
set union
返回一个集合的全部成员,如果是多个集合(key),返回所有给定集合的并集。
不存在的 key 被视为空集。
127.0.0.1:6379> SADD k62_1 m1 m2 m3
(integer) 3
127.0.0.1:6379> SADD k62_2 m2 m3 m4 m5 m6
(integer) 5
# 一个key,返回整个集合。
127.0.0.1:6379> SUNION k62_1
1) "m1"
2) "m2"
3) "m3"
# 多个key,返回并集
127.0.0.1:6379> SUNION k62_1 k62_2
1) "m3"
2) "m1"
3) "m5"
4) "m6"
5) "m2"
6) "m4"
sunionstore
SUNIONSTORE destination key [key ...]
set union and store
同 SINTERSTORE , 只不过存储的是并集的结果。 将多个集合的并集存储到 distination 中。
127.0.0.1:6379> SADD k63_1 m1 m2 m3
(integer) 3
127.0.0.1:6379> SADD k63_2 m2 m3 m4 m5 m6
(integer) 5
127.0.0.1:6379> SUNIONSTORE k63_dis k62_1 k62_2
(integer) 6
127.0.0.1:6379> SMEMBERS k63_dis
1) "m3"
2) "m1"
3) "m5"
4) "m6"
5) "m2"
6) "m4"
sdiff
SDIFF key [key ...]
set difference
如果指定一个集合,key
,返回一个集合的全部成员,
如果指定了多个集合(key
),则返回 所有给定集合之间的差集。
不存在的 key
被视为空集。
127.0.0.1:6379> SADD k64_1 m1 m2 m3
(integer) 3
127.0.0.1:6379> SADD k64_2 m2 m3 m4 m5 m6
(integer) 5
# 返回 k64_1 - k64_2
127.0.0.1:6379> SDIFF k64_1 k64_2
1) "m1"
# 返回 k64_2 - k64_1
127.0.0.1:6379> SDIFF k64_2 k64_1
1) "m6"
2) "m4"
3) "m5"
sdiffstore
SDIFFSTORE destination key [key ...]
将集合的差集存储到 destination
集合中.
127.0.0.1:6379> SADD k65_1 m1 m2 m3
(integer) 3
127.0.0.1:6379> SADD k65_2 m2 m3 m4 m5 m6
(integer) 5
127.0.0.1:6379> SDIFFSTORE k65_dis_1 k65_1 k65_2
(integer) 1
127.0.0.1:6379> SMEMBERS k65_dis_1
1) "m1"
127.0.0.1:6379> SDIFFSTORE k65_dis_2 k65_2 k65_1
(integer) 3
127.0.0.1:6379> SMEMBERS k65_dis_2
1) "m6"
2) "m4"
3) "m5"
sscan
SSCAN key cursor [MATCH pattern] [COUNT count]
set scan
这是一个查询命令。 同 SCAN
命令. 可以参考这篇文章 010-其他命令
SCAN
命令是一个基于游标的迭代器(cursor based iterator
): SCAN
命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN
命令的游标参数, 以此来延续之前的迭代过程。
127.0.0.1:6379> SSCAN k66 1
1) "0"
2) 1) "m1"
2) "m3"
3) "m2"
4) "m4"
5) "m5"
127.0.0.1:6379> SSCAN k66 0
1) "0"
2) 1) "m6"
2) "m1"
3) "m3"
4) "m2"
5) "m4"
6) "m5"
127.0.0.1:6379> SSCAN k66 1 MATCH m2 Count 10
1) "0"
2) 1) "m2"
127.0.0.1:6379>
set
的内部结构在 t_set.c 这个文件中。
robj *setTypeCreate(sds value) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
表明,set
数据类型是由两种数据结构来实现的。
而在 createSetObject()
,指明了其编码方式是 OBJ_ENCODING_HT
,即哈希表的方式, 也就是使用 dict 这种数据结构来存储的。
robj *createSetObject(void) {
dict *d = dictCreate(&setDictType, NULL);
robj *o = createObject(OBJ_SET, d);
o->encoding = OBJ_ENCODING_HT;
return o;
}
hashtable
这里就不赘述了。直接上穿梭机吧。
在createIntsetObject()
中指明了使用的编码方式是 OBJ_ENCODING_INTSET
. 如下。
robj *createIntsetObject(void) {
intset *is = intsetNew();
robj *o = createObject(OBJ_SET, is);
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
我们来看看 intset
到底什么何方利器.
我直接全项目搜索: intset
,就找到了 intset.h
.
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
encoding
: 数据编码,表示intset
中的每个数据元素用几个字节来存储。它有三种可能的取值:INTSET_ENC_INT16
表示每个元素用2
个字节存储,INTSET_ENC_INT32
表示每个元素用4
个字节存储,INTSET_ENC_INT64
表示每个元素用8
个字节存储。因此,intset
中存储的整数最多只能占用64bit
。length
: 表示intset
中的元素个数。encoding
和length
两个字段构成了intset
的头部(header
)。contents
: 是一个柔性数组(flexible array member
),表示intset
的header
后面紧跟着数据元素。这个数组的总长度(即总字节数)等于encoding * length
。柔性数组在Redis
的很多数据结构的定义中都出现过(例如sds
, quicklist
, skiplist
),用于表达一个偏移量。contents
需要单独为其分配空间,这部分内存不包含在intset
结构当中。这里有个问题.
Redis 是如何决定一个set 使用哪种编码方式的呢?
set 的编码是由第一个元素决定的。
robj *setTypeCreate(sds value) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
int isSdsRepresentableAsLongLong(sds s, long long *llval) {
return string2ll(s, sdslen(s), llval) ? C_OK : C_ERR;
}
如果 value
可以转换成 long long
类型的话,就使用 inset
编码方式。
通过看源码发现:
当intset
的元素个数超过 set_max_intset_entries
这个配置的时候,就会从intset
编码(OBJ_ENCODING_INTSET
)转换成 ht
编码(OBJ_ENCODING_HT
)。
这个我们会在后续文章中说明这里的方案。
好了,关于 set 类型的介绍就到这里了。
set
这种类型是一种无重复元素的集合。set
的业务场景关键字: 去重, 交并差运算。但是一定是无序的。如果要求有序的话,那就请等待下一篇文章吧。set
的 15
个命令, 务必熟记!!!set
的内部编码方式。哈希表编码和intset
编码。关于 intset 数据结构的详细介绍, 敬请期待吧。最新更新在公众号里哦。