# 书接上回

前一篇文章,我们学习的是 Redis 的数据结构之 hash, 学习了其基本的操作和使用内部数据结构是 hashtableziplist ,其中 Redis 中的 hashtable 是用 dict 表示的。如果不记得了其内部构成,就再看看看着上篇文章吧。现在我们继续学习下一个数据类型 set

# set 简介

Redisset 数据类型表示 一堆不重复值的集合。

Redisset 数据类型有两种编码方式. OBJ_ENCODING_INTSETOBJ_ENCODING_HT .

  • OBJ_ENCODING_HT 这种编码方式在上一篇文章 07-Redis 的数据类型之 hash 中已经简单的介绍过了。其实现的数据结构为 dict

  • OBJ_ENCODING_INTSET , 这种编码方式是我们要新学习的编码方式。 电梯直达

如果你看到这句话,那就说明你是一个特别认真的人。哈哈哈,我们还是先遵循惯例。先学习 set 类型相关的命令。

# set 类型的应用场景

  • 社交系统中存储关注信息,点赞信息,利用交并差运算,计算共同好友等业务中。比如 qq 的好友推荐逻辑,就可以使用差集运算。
  • 需要去重的业务逻辑中。某一时间端内系统的增长人数。
  • 统计访问网站的独立 IP

# set 的基本命令

# sadd

  • 语法

SADD key member [member ...]

  • 解释

set add

将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。

假如 key 不存在,则创建一个只包含 member 元素作成员的集合。

key 不是集合类型时,返回一个错误。

  • 演示
1
2
3
4
5
6
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 被视为空集合

  • 演示
1
2
3
4
5
6
7
8
9
# 查询元素, 注意保存是无序的.
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

  • 演示
1
2
3
4
5
6
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

移除并返回集合中的 随机一个 元素。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 的绝对值。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 不是集合类型,返回一个错误。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 元素删除。

sourcedestination 不是集合类型时,返回一个错误。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

  • 演示
1
2
3
4
5
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 被视为空集。

当给定集合当中有一个空集时,结果也为空集 (根据集合运算定律)。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 本身。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 被视为空集。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 中。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
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 被视为空集。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
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 集合中.

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 命令的游标参数, 以此来延续之前的迭代过程。

  • 演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 这个文件中。

1
2
3
4
5
robj *setTypeCreate(sds value) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}

表明, set 数据类型是由两种数据结构来实现的。

而在 createSetObject() ,指明了其编码方式是 OBJ_ENCODING_HT , 即哈希表的方式,也就是使用 dict 这种数据结构来存储的。

1
2
3
4
5
6
robj *createSetObject(void) {
dict *d = dictCreate(&setDictType, NULL);
robj *o = createObject(OBJ_SET, d);
o->encoding = OBJ_ENCODING_HT;
return o;
}

# hashtable

这里就不赘述了。直接上穿梭机吧。

# intset

createIntsetObject() 中指明了使用的编码方式是 OBJ_ENCODING_INTSET . 如下。

1
2
3
4
5
6
robj *createIntsetObject(void) {
intset *is = intsetNew();
robj *o = createObject(OBJ_SET, is);
o->encoding = OBJ_ENCODING_INTSET;
return o;
}

我们来看看 intset 到底什么何方利器.

我直接全项目搜索: intset ,就找到了 intset.h .

1
2
3
4
5
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 中的元素个数。 encodinglength 两个字段构成了 intset 的头部( header )。
  • contents : 是一个柔性数组( flexible array member ),表示 intsetheader 后面紧跟着数据元素。这个数组的总长度(即总字节数)等于 encoding * length 。柔性数组在 Redis 的很多数据结构的定义中都出现过(例如 sds , quicklist , skiplist ),用于表达一个偏移量。 contents 需要单独为其分配空间,这部分内存不包含在 intset 结构当中。

这里有个问题.

Redis 是如何决定一个 set 使用哪种编码方式的呢?

set 的编码是由第一个元素决定的。

1
2
3
4
5
6
7
8
9
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 的业务场景关键字:去重,交并差运算。但是一定是无序的。如果要求有序的话,那就 下一篇文章 zset ~
  • set15 个命令,务必熟记!!!
  • set 的内部编码方式。哈希表编码和 intset 编码。后面会有 关于 intset 数据结构的详细介绍的文章~

# 最后

期望与你一起遇见更好的自己

期望与你一起遇见更好的自己