【Redis 初阶】Redis 常见数据类型(Set、Zset、渐进式遍历、数据库管理)

一、Set 集合

集合类型也是保存多个字符串类型的元素的(可以使用 json 格式让 string 也能存储结构化数据),但和列表类型不同的是,集合中:

  1. 元素之间是无序的。(此处的 “无序” 是和 list 的有序相对应的)
  2. 元素不允许重复,如下图所示。

集合类型:

一个集合中最多可以存储个元素。Redis 除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多问题。

  • list:[1, 2, 3] 和 [2, 1, 3] 是两个不同的 list
  • set:[1, 2, 3] 和 [2, 1, 3] 是同一个集合

1、普通命令

(1)SADD

将⼀个或者多个元素添加到 set 中。

注意:重复的元素无法添加到 set 中。

语法:

SADD key member [member ...]

命令有效版本:

1.0.0 之后

时间复杂度:

O(1)

返回值:

本次添加成功的元素个数。

示例:


(2)SMEMBERS

获取一个 set 中的所有元素,注意,元素间的顺序是无序的。

语法:

SMEMBERS key

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 是集合中的元素个数。

返回值:

所有元素的列表。

示例:


(3)SISMEMBER

判断一个元素在不在 set 中。

语法:

SISMEMBER key member

命令有效版本:

1.0.0 之后

时间复杂度:

O(1)

返回值:

1 表示元素在 set 中。0 表示元素不在 set 中或者 key 不存在。

示例:


(4)SCARD

获取一个 set 的基数(cardinality),即 set 中的元素个数。

语法:

SCARD key

命令有效版本:

1.0.0 之后

时间复杂度:

O(1)

返回值:

set 内的元素个数。

示例:


(5)SPOP

从 set 中删除并返回⼀个或者多个元素。

注意:由于 set 内的元素是无序的,所以取出哪个元素实际是未定义行为,即可以看作随机的。

语法:

SPOP key [count] 

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 是 count

返回值:

取出的元素。

示例:


(6)SMOVE

将一个元素从源 set 取出并放入目标 set 中。

语法:

SMOVE source destination member 

命令有效版本:

1.0.0 之后

时间复杂度:

O(1)

返回值:

1 表示移动成功,0 表示失败。

示例:

针对上述情况,smove 不会视为出错,也会按照删除、插入来执行。


(7)SREM

将指定的元素从 set 中删除。

语法:

SREM key member [member ...]  

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 是要删除的元素个数.

返回值:

本次操作删除的元素个数。

示例:


2、集合间操作

交集(inter)、并集(union)、差集(diff)的概念如下图所示:

集合求交集、并集、差集:


(1)SINTER

获取给定 set 的交集中的元素。

语法:

SINTER key [key ...]

命令有效版本:

1.0.0 之后

时间复杂度:

O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。

返回值:

交集的元素。

示例:


(2)SINTERSTORE

获取给定 set 的交集中的元素并保存到目标 set 中。

要想知道交集的内容,直接按照集合的方式访问目标 set 这个 key 即可。

语法:

SINTERSTORE destination key [key ...] 

命令有效版本:

1.0.0 之后

时间复杂度:

O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。

返回值:

交集的元素个数。

示例:


(3)SUNION

获取给定 set 的并集中的元素。

语法:

SUNION key [key ...]  

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 给定的所有集合的总的元素个数。

返回值:

并集的元素。

示例:


(4)SUNIONSTORE

获取给定 set 的并集中的元素并保存到目标 set 中。

语法:

SUNIONSTORE destination key [key ...]

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 给定的所有集合的总的元素个数。

返回值:

并集的元素个数。

示例:


(5)SDIFF

获取给定 set 的差集中的元素。

语法:

SDIFF key [key ...]

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 给定的所有集合的总的元素个数。

返回值:

差集的元素。

示例:


(6)SDIFFSTORE

获取给定 set 的差集中的元素并保存到⽬标 set 中。

语法:

SDIFFSTORE destination key [key ...] 

命令有效版本:

1.0.0 之后

时间复杂度:

O(N),N 给定的所有集合的总的元素个数.

返回值:

差集的元素个数。

示例:


3、命令小结

下表总结了集合类型的常见命令:

集合类型命令:


4、内部编码

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数并且元素的个数小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使⽤。
  • hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。

(1)当元素个数较少并且都为整数时,内部编码为 intset

(2)当元素个数超过 512 个,内部编码为 hashtable

(3)当存在元素不是整数时,内部编码为 hashtable

 


5、使用场景

场景一:集合类型比较典型的使用场景是标签(tag)。例如 A 用户对娱乐、体育板块比较感兴趣,B 用户对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于增强用户体验和用户黏度都非常有帮助。 例如一个电子商务网站会对不同标签的用户做不同的产品推荐。

下面的演示通过集合类型来实现标签的若干功能。

(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:4 user:9 user:28

(3)删除用户下的标签

srem user:1:tags tag1 tag5
...

(4)删除标签下的用户

srem tag1:users user:1
srem tag5:users user:1
...

(5)计算用户的共同兴趣标签

sinter user:1:tags user:2:tags

场景二:还可以使用 Set 来计算用户之间的共同好友(基于 “集合求交集”),基于此还可以做一些好友推荐。 

场景三:

使用 Set 还能统计 UV(去重)。一个互联网产品如何衡量用户量,用户规模呢?

主要的指标是以下两个方面:

  1. PV(Page View),用户每次访问该服务器都会产生一个 pv。
  2. UV(User View),每个用户访问服务器都会产生一个 uv,但是同一个用户多次访问并不会使 uv 增加。uv 需要按照用户进行去重,去重的过程就可以使用 Set 来实现。

二、Zset 有序集合

有序集合相对于字符串、列表、哈希、集合来说会有一些陌生。它保留了集合不能有重复成员的特点,但与集合不同的是,有序集合中的每个元素都有一个唯一的浮点类型的分数(score)与之关联,着使得有序集合中的元素是可以维护有序的,但这个有序不是用下标作为排序依据而是用这个分数。

Zset 的内部数据结构是跳表。

如下图所示,该有序集合显示了三国中的武将的武力。

有序集合:

有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利用有序集合,可以帮助我们在实际开发中解决很多问题。

有序集合中的元素是不能重复的,但分数允许重复。类比于一次考试之后,每个人一定有一个唯一的分数,但分数允许相同。

列表、集合、有序集合三者的异同点: 


1、普通命令

(1)ZADD

添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负极限也是合法的。注意:负无穷大不是无穷小,负无穷大的绝对值和无穷大是一样的。

ZADD 的相关选项:

  • XX:仅仅用于更新已经存在的元素(member),不会添加新元素。

  • NX:仅用于添加新元素(member),不会更新已经存在的元素。

  • LT:仅当新分数小于当前分数时才更新现有元素,不会阻止添加新元素。

  • GT:仅当新分数大于当前分数时才更新现有元素,不会阻止添加新元素。

  • CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。

  • INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和分数。

语法:

ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]

member 和 score 称为是一个 "pair",类似于 C++ 里的 std::pair。不要把它们理解成 “键值对”(key - value pair),键值对中是有明确的 “角色区分”,一定是根据键 -> 值。而对于有序集合来说,既可以通过 member 找到对应的 score,也可以通过 score 找到匹配的 member。

命令有效版本:

1.2.0 之后

时间复杂度:

O(log(N))

返回值:

返回新增成功的元素个数。

示例:

如果修改的分数影响到了之前的顺序,就会自动的移动元素位置,保持原有的升序顺序不变:


(2)ZCARD

获取一个 zset 的基数(cardinality),即 zset 中的元素个数。

语法:

ZCARD key 

命令有效版本:

1.2.0 之后

时间复杂度:

O(1)

返回值:

zset 内的元素个数。

示例:


(3)ZCOUNT

返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,如果不想要边界值,可以通过在边界值前加上 '(' 来排除。

语法:

ZCOUNT key min max

命令有效版本:

2.0.0 之后

时间复杂度:

O(log(N))

先根据 min 找到对应的元素,再根据 max 找到对应的元素,两次都是 O(log(N))。实际上,Zset 内部会记录每个元素当前的 “排行” / “次序”,查询到元素就直接知道了元素所在的 “次序”(下标),就可以直接把 max 对应的元素次序和 min 对应的元素次序做减法即可。

返回值:

满足条件的元素列表个数。

示例:


(4)ZRANGE

返回指定区间里的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。

语法:

ZRANGE key start stop [WITHSCORES]  

此处的 [start, stop] 为下标构成的区间,从 0 开始,支持负数。

命令有效版本:

1.2.0 之后

时间复杂度:

O(log(N)+M),N 是整个有序集合的元素个数,M 是 start - stop 区间内的元素个数。

返回值:

区间内的元素列表。

示例:

Redis 内部存储数据是按照二进制的方式存储的,意味着 Redis 服务器是不负责字符编码的,所以要把二进制对回到汉字需要客户端支持:


(5)ZREVRANGE

返回指定区间里的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。

备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。

语法:

ZREVRANGE key start stop [WITHSCORES] 

命令有效版本:

1.2.0 之后

时间复杂度:

O(log(N)+M)

返回值:

区间内的元素列表。

示例:


(6)ZRANGEBYSCORE(弃用)

返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 '(' 排除。

备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。

语法:

ZRANGEBYSCORE key min max [WITHSCORES]  

命令有效版本:

1.0.5 之后

时间复杂度:

O(log(N)+M)

返回值:

区间内的元素列表。

示例:


(7)ZPOPMAX

删除并返回分数最高的 count 个元素。

语法:

ZPOPMAX key [count]

命令有效版本:

5.0.0 之后

时间复杂度:

O(log(N) * M),N 是有序集合的元素个数,M 表示 count,要删除的元素个数。

既然是尾删,为什么不把最后一个元素的位置特殊标记一下,后续删除不久省却了查找过程,直接 O(1) 了吗?

这个是有可能的,但是目前 Redis 并没有这么做。事实上,Redis 的源码中针对有序集合确实是记录了尾部的特定位置,但是在实际删除的时候并没有用上这个特性,而是直接调用了一个 “通用的删除函数”(给定一个 member 的值,进行查找,找到位置之后再删除)。(个人认为此处是存在优化空间的)

返回值:

分数和元素列表。

示例:

如果存在多个元素分数相同(分数是主要因素,相同的情况下会按照 member 字符串的字典序来决定先后顺序),同时为最大值,那么 zpopmax 删除最大元素时,仍然只会删除其中一个元素。


(8)BZPOPMAX

ZPOPMAX 的阻塞版本。可以同时读多个有序集合。

语法:

BZPOPMAX key [key ...] timeout

timeout 单位是 s,支持小数形式。

命令有效版本:

5.0.0 之后

时间复杂度:

O(log(N)),删除最大值花费的时间。

如果当前 BZPOPMAX 同时监听多个 key,假设 key 是 M 个,那么此时时间复杂度是 O(log(N) * M) 吗?

每个这样的 key 上面都删除一次元素才需要 * M,而这里是从这若干个 key 中只删除一次。

返回值:

元素列表。

示例:


(9)ZPOPMIN

删除并返回分数最低的 count 个元素。

语法:

ZPOPMIN key [count] 

命令有效版本:

5.0.0 之后

时间复杂度:

O(log(N) * M)

返回值:

分数和元素列表。

示例:


(10)BZPOPMIN

ZPOPMIN 的阻塞版本。

语法:

BZPOPMIN key [key ...] timeout

命令有效版本:

5.0.0 之后

时间复杂度:

O(log(N))

返回值:

元素列表。

示例:


(11)ZRANK

返回指定元素的排名,升序。

语法:

ZRANK key member

命令有效版本:

2.0.0 之后

时间复杂度:

O(log(N))

ZRANK 查找元素的过程和 ZCOUNT 是一样的。

返回值:

排名。

示例:


(12)ZREVRANK

返回指定元素的排名,降序

语法:

ZREVRANK key member 

命令有效版本:

2.0.0 之后

时间复杂度:

O(log(N))

返回值:

排名。

示例:


(13)ZSCORE

返回指定元素的分数。

语法:

ZSCORE key member 

命令有效版本:

1.2.0 之后

时间复杂度:

O(1)

此处相当于 Redis 对于这样的查询操作做了特殊优化,付出了额外的空间代价。

返回值:

分数。

示例:


(14)ZREM

删除指定的元素。

语法:

ZREM key member [member ...] 

命令有效版本:

1.2.0 之后

时间复杂度:

O(M*log(N))

返回值:

本次操作删除的元素个数。

示例:


(15)ZREMRANGEBYRANK

按照排序,升序删除指定范围的元素,左闭右闭。

语法:

ZREMRANGEBYRANK key start stop  

命令有效版本:

2.0.0 之后

时间复杂度:

O(log(N)+M)

返回值:

本次操作删除的元素个数。

示例:


(16)ZREMRANGEBYSCORE

按照分数删除指定范围的元素,左闭右闭,也可以使用 '(' 来排除边界值。

语法:

ZREMRANGEBYSCORE key min max 

命令有效版本:

1.2.0 之后

时间复杂度:

O(log(N)+M)

返回值:

本次操作删除的元素个数。

示例:


(17) ZINCRBY

为指定的元素的关联分数添加指定的分数值。

语法:

ZINCRBY key increment member

命令有效版本:

1.2.0 之后

时间复杂度:

O(log(N))

返回值:

增加后元素的分数。

示例:

ZINCRBY 不光会修改分数内容,也能同时移动元素位置,保证整个有序集合仍然是升序的。


2、集合间操作

有序集合的交集操作:


(1)ZINTERSTORE

求出给定有序集合中元素的交集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。

在有序集合中,member 是元素的本体,score 只是辅助排序的工具人。因此,在进行比较 “相同” 时,只要 member 相同即可。如果 member 相同,score 不同,进行交集合并之后的最终分数看 AGGREGATE 后面的属性。

语法:

ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
numkeys 是一个整数,用来描述后续有几个 key 参与交集运算。

命令有效版本:

2.0.0 之后

时间复杂度:

O(N*K)+O(M*log(M)),N 是输入的有序集合中,最小的有序集合的元素个数;K 是输入了几个有序集合;M 是最终结果的有序集合的元素个数。

返回值:

目标集合中的元素个数。

示例:


有序集合的并集操作:


(2)ZUNIONSTORE

求出给定有序集合中元素的并集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。

语法:

ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]

命令有效版本:

2.0.0 之后

时间复杂度:

O(N)+O(M*log(M)) N 是输入的有序集合总的元素个数,M 是最终结果的有序集合的元素个数。

返回值:

目标集合中的元素个数

示例:


3、命令小结

有序集合命令:


4、内部编码

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
  • skiplist(跳表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时 ziplist 的操作效率会下降。

简单来说,跳表是一个 “复杂链表”,查询元素的时间复杂度是 O(logN)。相比于树形结构,更适合按照范围获取元素。

(1)当元素个数较少且每个元素较小时,内部编码为 ziplist

(2)当元素个数超过 128 个,内部编码 skiplist

(3)当某个元素大于 64 字节时,内部编码 skiplist


5、使用场景

有序集合比较典型的使用场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热榜:

(1)添加用户赞数

例如用户 james 发布了一篇文章,并获得 3 个赞,可以使用有序集合的 zadd 和 zincrby 功能:

zadd user:ranking:2022-03-15 3 james

之后如果再获得赞,可以使用 zincrby:

zincrby user:ranking:2022-03-15 1 james

(2)取消用户赞数

由于各种原因(例如用户注销、用户作弊等)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用 zrem。例如删除成员 tom:

zrem user:ranking:2022-03-15 tom


(3)展示获取赞数最多的 10 个用户

此功能使用 zrevrange 命令实现:

zrevrangebyrank user:ranking:2022-03-15 0 9

(4)展示用户信息以及用户分数

次功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用 zscore 和 zrank 来实现。

hgetall user:info:tom
zscore user:ranking:2022-03-15 mike
zrank user:ranking:2022-03-15 mike

三、渐进式遍历

Redis 使用 scan 命令进行渐进式遍历键,进而解决直接使用 keys 获取键时可能出现的阻塞问题。不是一个命令将所有的 key 都拿到,而是每执行一次命令,只获取到其中的一小部分,这样就可以保证当前这一次操作不会太卡。每次 scan 命令的时间复杂度是 O(1),但是要完整地完成所有键的遍历,需要执行多次 scan。渐进式遍历其实是一组命令,这一组命令的使用方法是一样的。整个过程如下图所示:

scan 命令渐进式遍历

  • 首次 scan 从 0 开始。
  • 当 scan 返回的下次位置为 0 时,遍历结束。

返回值的前半部分1)是告诉我们,下次继续遍历的光标(当作一个字符串即可)要从哪里开始。第二部分2)是我们真正遍历到的 key 的内容。


1、SCAN

以渐进式的方式进行键的遍历。

渐进式遍历再遍历过程中不会在服务器这边存储任何的状态信息,此处的遍历是随时可以终止的,不会对服务器产生任何的副作用。

语法:

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

这里的 count 是限制这一次遍历能够获取到多少个元素,默认是 10。

注意:此处的 count(给 Redis 服务器一个 “提示” / “建议”,写入的 count 和实际返回的 key 的个数不一定完全相同,但是不会差很多)和 MySQL 的 limit(精确的)不一样。

命令有效版本:

2.8.0 之后

时间复杂度:

O(1)

返回值:

下一次 scan 的游标(cursor)以及本次得到的键。

示例:

除了 scan 以外,Redis 面向哈希类型、集合类型、有序集合类型分别提供了 hscan、sscan、zscan 命令,它们的用法和 scan 基本类似。

注意:渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏。


四、数据库管理

Redis 提供了几个面向 Redis 数据库的操作,分别是命令:dbsize、select、flushdb、flushall。


1、切换数据库

select dbIndex  

许多关系型数据库,例如 MySQL 支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis 只是用数字作为多个数据库的实现。Redis 默认配置中是有 16 个数据库,我们不能创建新的数据库,也不能删除已有的数据库,这 16 个数据库中的数据是隔离的(相互之间不会有影响)。select 0 操作会切换到第⼀个数据库,select 15 会切换到最后⼀个数据库。

0 号数据库和 15 号数据库保存的数据是完全不冲突的,如下图所示,即各种有各自的键值对。默认情况下,我们处于数据库 0。

Redis 管理的数据库:

Redis 中虽然支持多数据库,但随着版本的升级,其实不是特别建议使用多数据库特性。如果真的需要完全隔离的两套键值对,更好的做法是维护多个 Redis 实例,而不是在一个 Redis 实例中维护多数据库。这是因为本身 Redis 并没有为多数据库提供太多的特性,其次无论是否有多个数据库,Redis 都是使用单线程模型,所以彼此之间还是需要排队等待命令的执行。同时多数据库还会让开发、调试和运维工作变得复杂。所以实践中,始终使用数据库 0 其实是⼀个很好的选择


2、清除数据库

flushdb / flushall 命令用于清除数据库,区别在于 flushdb 只清除当前数据库,flushall 会清楚所有数据库。

永远不要在线上环境执行清除数据的操作,除非你想体验一把 “从删库到跑路” 的操作。 

语法:

FLUSHDB [ASYNC | SYNC] 

FLUSHALL [ASYNC | SYNC] 

  • ASYNC:异步
  • SYNC:同步
  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

炫酷的伊莉娜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值