四.集合(Set)数据类型
集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过 索引下标获取元素,一个集合最多可以存储个元素.Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集.
(一)相关命令
下面将按照集合内和集合间两个维度对集合的常用命令进行介绍。
1.集合内操作
(1)添加元素(sadd key element [element ...])
返回结果为添加成功的元素个数,例如
127.0.0.1:6379> exists myset
(integer) 0
127.0.0.1:6379> sadd myset a b c
(integer) 3
127.0.0.1:6379> sadd myset a b
(integer) 0
(2)删除元素(srem key element [element ...])
返回结果为成功删除元素个数,例如:
127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> srem myset hello
(integer) 0
(3)计算元素个数(scard key)
scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用 Redis内部的变量
(4)判断元素是否在集合中(sismember key element)
如果给定元素element在集合内返回1,反之返回0,例如:
127.0.0.1:6379> sismember myset c
(integer) 1
(5)随机从集合返回指定个数元素(srandmember key [count])
[count]是可选参数,如果不写默认为1,例如:
127.0.0.1:6379> srandmember myset 2
1) "a"
2) "c"
127.0.0.1:6379> srandmember myset "d"
(6)从集合随机弹出元素(spop key)
spop操作可以从集合中随机弹出一个元素,例如下面代码是一次spop 后,集合元素变为"d b a":需要注意的是Redis从3.2版本开始,spop也支持[count]参数。srandmember和spop都是随机从集合选出元素,两者不同的是spop命令 执行后,元素会从集合中删除,而srandmember不会。
127.0.0.1:6379> spop myset "c"
127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "a"
(7)获取所有元素(smembers key)
下面代码获取集合myset所有元素,并且返回结果是无序的:
127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "a"
smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,这时候可以使用sscan来完成
2.集合间操作
现在有两个集合,它们分别是user:1:follow和user:2:follow:
127.0.0.1:6379> sadd user:1:follow it music his sports
(integer) 4
127.0.0.1:6379> sadd user:2:follow it news ent sports
(integer) 4
(1)求多个集合的交集(sinter key [key ...])
下面代码是求user:1:follow和user:2:follow两个集合的交集, 返回结果是sports、it:
127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "sports"
2) "it"
(2)求多个集合的并集(suinon key [key ...])
下面代码是求user:1:follow和user:2:follow两个集合的并集, 返回结果是sports、it、his、news、music、ent:
127.0.0.1:6379> sunion user:1:follow user:2:follow
1) "sports"
2) "it"
3) "his"
4) "news"
5) "music"
6) "ent"
(3)求多个集合的差集(sdiff key [key ...])
下面代码是求user:1:follow和user:2:follow两个集合的差集, 返回结果是music和his:
127.0.0.1:6379> sdiff user:1:follow user:2:follow
1) "music"
2) "his"
(4)将交集、并集、差集的结果保存(sinterstore destination key [key ...] suionstore destination key [key ...] sdiffstore destination key [key ...])
集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面 三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key中,例如下面操作将user:1:follow和user:2:follow两个集 合的交集结果保存在user:1_2:inter中,user:1_2:inter本身也是集合类
型:
127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow
(integer) 2
127.0.0.1:6379> type user:1_2:inter
set
127.0.0.1:6379> smembers user:1_2:inter
1) "it"
2) "sports"
(二)内部编码
集合类型的内部编码有两种:
·intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
·hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使 用hashtable作为集合的内部实现。
下面用示例来说明:
1)当元素个数较少且都为整数时,内部编码为intset:
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding setkey
"intset"
2)当元素个数超过512个,内部编码变为hashtable:
127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 ... 512 513
(integer) 513
127.0.0.1:6379> scard setkey
(integer) 513
127.0.0.1:6379> object encoding listkey
"hashtable"
3)当某个元素不为整数时,内部编码也会变为hashtable:
127.0.0.1:6379> sadd setkey a
(integer) 1
127.0.0.1:6379> object encoding setkey
"hashtable"
(三)使用场景
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要
下面使用集合类型实现标签功能的若干功能。
(1)给用户添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
...
(2)给标签添加用户
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...
用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致,有关如何将两个命令放在一个事务
(3)删除用户下的标签
srem user:1:tags tag1 tag5
...
(4)删除标签下的用户
srem tag1:users user:1
srem tag5:users user:1
...
(3)和(4)也是尽量放在一个事务执行。
(5)计算用户共同感兴趣的标签
可以使用sinter命令,来计算用户共同感兴趣的标签,如下代码所示
sinter user:1:tags user:2:tags
前面只是给出了使用Redis集合类型实现标签的基本思路,实际上一个标签系统远比这个要复杂得多,不过集合类型的应用场景通常为以下几种:
·sadd=Tagging(标签)
·spop/srandmember=Random item(生成随机数,比如抽奖)
·sadd+sinter=Social Graph(社交需求)
小结:
sadd:向名称为key的set中添加元素,set集合不允许重复元素。
smembers:查看set集合中的元素。
srem:删除set集合的元素
spop:随机返回删除的key
sdiff:返回两个集合的不同元素(哪个集合在前面就以哪个集合为标准)
sdiffstore:将返回的不同元素,存储到另一个集合里
sdiffstore set4 set2 set3 将set2 set3的比较结果保存到set4中
sinter:取交集
sinterstore:取交集后保存
sunion:取并集
sunionstore:取并集后保存
smove:从一个set集合移动到另一个set集合里
scard:查看集合里的元素个数
sismember:判断某个元素是否为集合中的元素,是,返回1。不是,返回0。
srandmember:随机返回一个元素
五.有序集合(Zset)数据类型
有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。如图2-24所示,该有序集合包含kris、mike、frank、tim、martin、tom, 它们的分分别是1、91、200、220、250、251,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。
下表给出了列表、集合和有序集合三者的异同点:
数据结构 | 是否允许重复 | 是否有序 | 有序实现方式 | 应用场景 |
列表(list) | 是 | 是 | 索引下标 | 时间轴,消息队列 |
集合(set) | 否 | 否 | 无 | 标签,社交等 |
有序集合(zset) | 否 | 是 | 分值(score) | 排行榜系统,社交等 |
(一)相关命令
1.集合内
(1)添加成员(zadd key score member [score member ...])
下面操作向有序集合user:ranking添加用户tom和他的分数251:
127.0.0.1:6379> zadd user:ranking 251 tom
(integer) 1
返回结果代表成功添加成员的个数:
127.0.0.1:6379> zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin
(integer) 5
有关zadd命令有两点需要注意:
·Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:
·nx:member必须不存在,才可以设置成功,用于添加。
·xx:member必须存在,才可以设置成功,用于更新。
·ch:返回此次操作后,有序集合元素和分数发生变化的个数
·incr:对score做增加,相当于后面介绍的zincrby。
有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间 复杂度为O(log(n)),sadd的时间复杂度为O(1)。
(2)计算成员个数(zcard key)
例如下面操作返回有序集合user:ranking的成员数为5,和集合类型的 scard命令一样,zcard的时间复杂度为O(1)。
127.0.0.1:6379> zcard user:ranking
(integer) 5
(3)计算某个成员的分数(zscore key member)
tom的分数为251,如果成员不存在则返回nil:
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking test
(nil)
(4)计算成员的排名(zrank key member | zrevrank key member)
zrank是从分数从低到高返回排名,zrevrank反之。例如下面操作中,tom 在zrank和zrevrank分别排名第5和第0(排名从0开始计算)。
127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0
(5)删除成员(zrem key member [member ...])
下面操作将成员mike从有序集合user:ranking中删除,返回结果为成功删除的个数。
127.0.0.1:6379> zrem user:ranking mike
(integer) 1
(6)增加成员的分数(zincrby key increment member)
下面操作给tom增加了9分,分数变为了260分:
127.0.0.1:6379> zincrby user:ranking 9 tom
"260"
(7)返回指定排名范围的成员(zrange key start end [withscores] | zrevrange key start end [withscores])
有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。 下面代码返回排名最低的是三个成员,如果加上withscores选项,同时会返回成员的分数:
127.0.0.1:6379> zrange user:ranking 0 2 withscores
1) "kris"
2) "1"
3) "frank"
4) "200"
5) "tim"
6) "220"
127.0.0.1:6379> zrevrange user:ranking 0 2 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
(8)返回指定分数范围的成员(zrangebyscore key min max [withscores] [limit offset count] | zrevrangebyscore key max min [withscores] [limit offset count])
其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。例如 下面操作从低到高返回200到221分的成员,withscores选项会同时返回每个 成员的分数。[limit offset count]选项可以限制输出的起始位置和个数:
127.0.0.1:6379> zrangebyscore user:ranking 200 tinf withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 221 200 withscores
1) "tim"
2) "220"
3) "frank"
4) "200"
同时min和max还支持开区间(小括号)和闭区间(中括号),-inf和 +inf分别代表无限小和无限大:
127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores
1) "tim"
2) "220"
3) "martin"
4) "250"
5) "tom"
6) "260"
(9)返回指定分数范围成员个数(zcount key min max)
下面操作返回200到221分的成员的个数:
127.0.0.1:6379> zcount user:ranking 200 221
(integer) 2
(10)删除指定排名内的升序元素(zremrangebyrank key start end)
下面操作删除第start到第end名的成员:
127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3
(11)删除指定分数范围的成员(zremrangebyscore key min max)
下面操作将250分以上的成员全部删除,返回结果为成功删除的个数:
127.0.0.1:6379> zremrangebyscore user:ranking (250 +inf
(integer) 2
2.集合间的操作
将下图的两个有序集合导入到Redis中。
127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
(integer) 6
127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom
(integer) 4
(1)交集
zinterstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
这个命令参数较多,下面分别进行说明:
·destination:交集计算结果保存到这个键。
·numkeys:需要做交集计算键的个数。
·key[key...]:需要做交集计算的键
·weights weight[weight...]:每个键的权重,在做交集计算时,每个键中 的每个member会将自己分数乘以这个权重,每个键的权重默认是1。
·aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、 min(最小值)、max(最大值)做汇总,默认值是sum。
下面操作对user:ranking:1和user:ranking:2做交集,weights和 aggregate使用了默认配置,可以看到目标键user:ranking:1_inter_2对分值 做了sum操作:
127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "168"
3) "martin"
4) "875"
5) "tom"
6) "1139"
如果想让user:ranking:2的权重变为0.5,并且聚合效果使用max,可以执行如下操作:
127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2 weights 1 0.5 aggregate max
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "91"
3) "martin"
4) "312.5"
5) "tom"
6) "444"
(2)并集
zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
该命令的所有参数和zinterstore是一致的,只不过是做并集计算,例如 下面操作是计算user:ranking:1和user:ranking:2的并集,weights和 aggregate使用了默认配置,可以看到目标键user:ranking:1_union_2对分值 做了sum操作:
127.0.0.1:6379> zunionstore user:ranking:1_union_2 2 user:ranking:1 user:ranking:2
(integer) 7
127.0.0.1:6379> zrange user:ranking:1_union_2 0 -1 withscores
1) "kris"
2) "1"
3) "james"
4) "8"
5) "mike"
6) "168"
7) "frank"
8) "200"
9) "tim"
10) "220"
11) "martin"
12) "875"
13) "tom"
14) "1139"
(二)内部编码
有序集合类型的内部编码有两种:
·ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplistentries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
·skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
下面用示例来说明:
1)当元素个数较少且每个元素较小时,内部编码为ziplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3
(integer) 3
127.0.0.1:6379> object encoding zsetkey
"ziplist"
2)当元素个数超过128个,内部编码变为skiplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 12 e4 ...忽略... 84 e129
(integer) 129
127.0.0.1:6379> object encoding zsetkey
"skiplist"
3)当某个元素大于64字节时,内部编码也会变为skiplist:
127.0.0.1:6379> zadd zsetkey 20 "one string is bigger than 64 byte............. ..................."
(integer) 1
127.0.0.1:6379> object encoding zsetkey
"skiplist"
(三)使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。下面使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能。
(1)添加用户赞数
例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的 zadd和zincrby功能:
zadd user:ranking:2016_03_15 mike 3
如果之后再获得一个赞,可以使用zincrby:
zincrby user:ranking:2016_03_15 mike 1
(2)取消用户赞数
由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要 将用户从榜单中删除掉,可以使用zrem。例如删除成员tom:
zrem user:ranking:2016_03_15 mike
(3)展示获取赞数最多的十个用户
此功能使用zrevrange命令实现:
zrevrangebyrank user:ranking:2016_03_15 0 9
(4)展示用户信息以及用户分数
此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户 的分数和排名可以使用zscore和zrank两个功能:
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike
小结:
zadd:向有序集合中添加一个元素,该元素如果存在则更新顺序,如果分值相同元素不同会同时存在两个元素。
zrem :删除zset名称key中的member元素
zrank 返回有序集合中指定成员的索引(从小到大排序)
zrevrank 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
zcard 返回集合里所有元素的个数
zcount 返回集合中score在给定区间中的数量
zincrby key increment member: 有序集合中对指定成员的分数加上增量 increment
zrangebyscore key min max [WITHSCORES] [LIMIT] :通过分数返回有序集合指定区间内的成员
zremrangebyrank key start stop :移除有序集合中给定的排名区间的所有成员
zremrangebyscore key min max:移除有序集合中给定的分数区间的所有成员