redis的数据类型:SET与ZSET

SET类型

set类型介绍

谈到一个术语,这个术语很可能有多种含义~~比如Set,set可以是一种类型叫做集合,也可以是一条命令—— (和 get 相对应),本节我们主要就是要研究set作为一个类型,它有哪些性质呢?

  1. 集合就是把一些有关联的数据放到一起~~
  2. set集合中的元素是无序的!

如何理解set的无序性?

此处说的无序和 前面 list 这里的有序 是对应的.
有序:顺序很重要。变换一下顺序,就是不同的 list 了.
无序:顺序不重要。变化一下顺序,集合还是那个集合.
list: [1, 2, 3] 和 [2, 1, 3] 两个不同的 list
set: [1, 2, 3] 和 [2, 1, 3] 是同一个集合.

set中元素的类型

我们知道,set类型就是一些元素的集合,请问Redis对集合中每个元素的类型有什么限制要求吗?
和list类似,集合set中的每个元素也都必须是string类型.(可以使用json这样的格式存储 结构化 数据)

set类型的命令

SADD

作用:将一个或者多个元素添加到set中。
语法:SADD key member [member ...]
时间复杂度:O(1)
返回值:本次操作添加成功的元素个数。

Redis中如何创建一个set类型的集合?

在这里插入图片描述

请问相同的元素可以重复添加到set中吗?

不能

smembers

如何获取一个set中的所有元素呢?
输入指令:smembers key
其中key对应的value是一个set集合

SISMEMBER

如何查找一个元素在不在set中?
SISMEMBER key member
其中key对应一个set,member就是你要查找的那个元素
如果查到了返回1,没查到返回0
集合的操作都是带有S前缀,判定当前的元素是否在集合中.

SPOP

作用:从集合中删一个元素
语法:SPOP key [count]

参数count
  • count 为正数且小于等于集合元素数量
    返回包含 count 个随机元素的数组 ,数组中的元素各不相同 ,同时这些元素会从集合中被移除 。比如集合有 5 个元素 ,执行 SPOP myset 3 ,会返回 3 个不同的随机元素组成的数组 ,且这 3 个元素从集合中移除。
  • count 为正数且大于集合元素数量
    返回整个集合内容 ,之后集合变为空集
  • count 为负数
    返回包含 count 绝对值数量的随机元素数组 ,元素可能重复 ,集合中的元素不会被移除 。 例如 SPOP myset -3 ,可能返回包含 3 个元素(元素可能重复 )的数组 ,集合本身不变
返回值:
  • 正常情况:
    • 如果SPOP 语句中不带count,说明就随机删除一个元素,我们直接返回那个刚刚被删除的元素
    • 如果SPOP 语句中带count,说明要删除的元素不止一个,我们就返回一个数组
  • 特殊情况
    • 集合不存在:若指定的 key 不存在 ,Redis 会将其视为空集合,此时 SPOP 命令返回 nil 。
    • 集合为空:当 key 对应的集合为空集时 ,SPOP 命令同样返回 nil 。
我们都知道SPOP应该是从SET里面删除一个元素,那因为SET是无序的呀,所以SET没有所谓的队头元素队尾元素这种说法,那请问SPOP删的是哪一个元素呢?
  • 如果你的命令是SPOP key,也就是说后面的count没写,那就是从set中随机删除一个元素
  • 如果你的命令是SPOP key count,那就是从set中随机删除count个元素

在这里插入图片描述

看图片中的例子,如果我再次构造一个1 2 3 4这样的集合,此时还是通过4次spop进行删除,删除顺序是否还是1432呢,如果每次删除,都是1432,这还能叫随机嘛??

  • 通过实验我们发现,再次构造一个1 2 3 4这样的集合,此时还是通过4次spop进行删除,删除顺序已经不是1432了,即当前的spop每次确实是随机删除了一个元素!!!
  • 并且官方文档中也承诺了,确实是随机删除的
  • 随机删除的实现方式,其实在SPOP实现的时候生产了一个随机数,根据随机数选取要删除的元素

SMOVE

作用:将一个元素从一个集合中移动到另一个集合中
语法:SMOVE source destination member
效果:将member元素从source集合中删除,然后插入到destination集合中
返回值:MOVE成功返回1,失败返回0,当 source 或 destination 不是集合类型时,会返回nil

假如说source中的member=1,然后我们的destination中已经有一个元素1了,请问我再执行SMOVE source destination "1",结果会怎样呢?

结果就是source中的member=1会被删除,然后destination中并不会多一个元素1,因为之前已经有过了

如果SMOVE source destination member中,我们要move的member并不在souce中,执行结果会怎样?

返回0表示移动失败

SREM

作用:删除key对应集合中的一个或者多个member
语法:SREM key member [member ...]
返回值:返回成功删除的元素个数
不同操作的返回值,含义差别还挺大的, 很多很乱,记不住咋办?俺也一样。原则,不要刻意去背,而是多翻文档.经常用的操作,自然就记住了.

SCARD

作用是求一个set中元素的数量,也就是集合的基数(集合中不重复的元素的个数)
命令语法:SCARD key
返回值

  • 若键存在且为set类型,SCARD 会返回set中元素的数量。
  • 若键不存在,SCARD 会返回 0

求两个集合的交集、并集、差集

交集、并集、差集概念介绍

交集和并集想必不用我多介绍,集合A与集合B的差集定义是,A中有且B中没有的所有元素组成的集合,就叫做A与B的差集,记作A-B

如何求两个集合的交集?(SINTER)

SINTER key1 key2 ...

  • 此处每个key都对应一个集合
  • 执行指令之后的返回值就是 所有集合取交集之后的所有元素
  • 时间复杂度: O(N * M) 其中,N 为给定集合当中基数(集合中元素的个数)最小的集合 ,M 为给定集合的个数 。这意味着,给定集合中最小集合的元素个数越少、集合的数量越少,执行该命令的耗时通常就越少

SINTERSTORE destination key [key ...]
SINTERSTORE可以直接把算好的交集,存储到destination对应的集合中(destination可以先前不存在)
返回值就是交集中的元素个数。要想知道交集的内容,直接按照集合的方式smembers访问destination这个key即可~

如何求两个集合的并集?(SUNION)

SUNION key [key ...]
sunion 返回的就是并集的结果数据.
时间复杂度,O (N) N 指的是总的元素个数.

SUNIONSTORE destination key [key ...]
直接把并集的结果存储到 destination 对应的 集合 中.
返回值并集的元素个数.

如何求两个集合的差集?(SDIFF)

SDIFF key [key ...]
返回值是差集的所有元素
时间复杂度: O (N)

SDIFFSTORE destination key [key ...]
SDIFFSTORE直接把算好的差集,存储到destination对应的集合中了.(destination可以不存在)
返回值就是差集中的元素个数.要想知道差集的内容,直接按照集合的方式smembers访问destination这个key即可~
时间复杂度: O (N)

上述这几个命令,对应的时间复杂度,是怎么计算出来的呢?
这就涉及到 集合 内部的源码实现了,自己去看

命令小结

命令时间复杂度
sadd key element [element …]O(k),k 是元素个数
srem key element [element …]O(k),k 是元素个数
scard keyO(1)
sismember key elementO(1)
srandmember key [count]O(n),n 是 count
spop key [count]O(n),n 是 count
smembers keyO(k),k 是元素个数
sinter key [key …] sinterstoreO(m * k),k 是几个集合中元素最小的个数,m 是
sunion key [key …] sunionstoreO(k),k 是多个集合的元素个数总和
sdiff key [key …] sdiffstoreO(k),k 是多个集合的元素个数总和

set类型的内部编码方式

主要就是intset和Hashtable两种

  • 正常情况下我们用Hashtable存,
  • 当元素均为整数,并且元素个数不是很多的时候, 我们就用intset来存,intset主要是为了节省空间,做出的特定优化~

在这里插入图片描述C++ 中的 std::set 背后的数据结构是 红黑树Java 中的 Set 本身是一个接口。这个接口后面的实现,可以是 TreeSet,也可以是 HashSet

set类型的应用场景

1. 使用 Set 来保存用户的 “标签”(用户画像)

首先企业通过自身搜集以及企业间数据的交换,获得了很多用户的标签,然后我们要将标签转化成字符串,存储在Redis中。redis中存数据,也可以用string、hash,想想为什么我们最终要用set来存呢?
主要是set可以过滤掉双方的冗余信息
比如PDD和腾讯交换信息,PDD这边通过自己软件内的信息搜集,得出结论,手机号为153xxxxxxxx的用户是一名在校男大学生,腾讯这边也通过自己的信息搜集,得出了同样的结论,那在他们共享手机号为153xxxxxxxx的用户信息时,用户是一名在校男大学生”这条信息就是冗余信息,根据set的性质,重复插入相同元素,set中只会保留一个值,这样就过滤掉了冗余信息。
而如果不用set,公司在给用户打标签时,就会有很多重复的标签,这样既占空间,又影响判断。

上述玩法,抖音玩的是最好的.其他互联网大厂一看这么搞真好,都纷纷跟进,最终导致了我们现在的信息茧房——你看到的东西,都是你愿意看到的,你不愿意看的,就很难被看到
=> 你看到的内容始终就是一个小圈子,到处都是传递焦虑的
=> 当你很认真的看了一个焦虑视频之后,人家服务器就判定你,你非常爱看这种类型的视频,接下来就给你狂推送

2. 使用Set来计算用户之间的共同好友

Set 非常方便计算交集,可以很容易的找到两个用户之间的公共标签~~基于这样的标签,衍生出一些 “用户关系”
比如QQ, 我这边加了很多好友. 你这边也加了很多好友,基于set就可以做一些好友推荐:A和B是好友. A和C是好友. B和C和D都是好友. 系统就会把D推荐给A

3. 使用Set统计UV

一个互联网产品,如何衡量用户量,用户规模?主要的指标,是两方面:

  1. PV page view
    用户每次访问该服务器,每次访问都会产生一个pv.
    网站的PV就是这个网站的累计访问次数

  2. UV ——user view
    每个用户,访问服务器,都会产生一个uv. 但是同一个用户多次访问,不会使uv增加~~ uv需要按照用户进行去重~~ 上述的去重过程,就可以使用set来实现.

网站的UV就表示这个网站一共被多少用户访问过(在累计访问次数的基础上加了一个限制,就是同一个用户无论访问多少次,我统计UV的时候只算一次)

之所以用set来统计UV,就是因为set自带去重属性,统计起来非常方便

ZSET类型

ZSET类型介绍

Zset全称是有序集合,Z就表示有序,如何理解这里的有序呢?
这里的有序是指,集合中的元素,按照一定的排序规则,在集合中升序或者降序排列(特别地,对于 zset这种有序集合,内部就是按照升序方式来排列的!!!)

ZSET排序的规则是啥呢?

给zset中的member同时引入了一个属性分数(score),浮点类型.每个member都会安排一个分数.进行排序的时候,就是依照此处的 score大小来进行升序/降序排序.

Zset中member可以重复吗?score可以重复吗?

member不可以,但是score可以

ZSET、set、List三种类型,在重复性上对元素的要求是什么?区分元素的依据是什么?

  1. List中的元素是可以重复的,set和zset中的元素member是不可以重复的,zset中的score是可以重复的
  2. set和zset都是靠member的唯一性来区分元素,List靠元素的数组下标来区分元素

如果ZSET中多个member具有相同的score,那这些Member之间会怎么排序呢?

分数相同,按照元素自身字符串的字典序来排列
分数不同,按照分数来排。

Redis中的ZSET类型与C++中的std::set的对比

C++中的std::set存储的元素也是有序的,但是不支持对已插入set中的值进行修改
而Redis中的ZSET类型不仅是有序的,还支持对有序集合中的元素score再进行修改,修改之后还会自动帮我们重新排好序

ZSET命令介绍

ZADD

作用是向有序集合中添加元素

ZADD语法

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

  • key:对应一个有序集合,代表你要操作的有序集合

  • score与member(ZADD添加ZSET中元素的时候,既要添加元素member,又要添加对应的分数score)

  • score:为有序集合中的成员指定的分数,属于浮点数类型。有序集合会依据分数对成员进行排序。

  • member:即有序集合中的成员,每个成员在有序集合里是唯一的,但不同成员可以有相同的分数。

    • member和score称为是一个 “pair”
      这个东西类似于C++里谈到的std::pair,但是不要把member和score理解成 “键值对”(key - value pair)(为什么呢?)
    • 键值对中,是有明确的 "角色区分"谁是键,谁是值,是明确的.一定是根据键 -> 值 ~~
      但是对于有序集合来说,是既可以通过member找到对应的score,又可以通过score找到匹配的member.
  • 可选参数[NX|XX]

    • []说明这俩可有可无,中间的NX|XX说明这俩只能选一个,不能同时设置

    • NX:
      表示仅在成员Member不存在时才进行添加操作(如果ZADD命令中加了NX选项,说明该指令只能添加新元素,不能修改老member的score)

    • XX:
      意味着只对已存在的member进行score的更新操作,不会添加新member。(如果ZADD命令中加了XX选项,说明该指令只能修改老member的score,不能添加新元素)

    • 如果既不加NX,也不加XX,那么该ZADD指令既可以添加新元素,也可以修改老member的score

  • [LT|GT]
    LT的全称是Less than,GT的全称是Great than

    • LT选项设置之后,假如说现在我要更新分数了,只有我发现更新后的分数,比之前的分数小,才会更新成功,否则就不更新~~
    • GT选项设置之后,假如说现在我要更新分数了,只有我发现更新后的分数,比之前的分数大,才会更新成功,否则就不更新~~
  • [CH]:
    本来你ZADD返回的是新添加的元素数量,加了CH选项之后,返回值就变成了你这个ZSET中所有被修改过的元素数量,包括新添加的元素以及分数被更新的元素(如果不加CH,更新元素是不会被记数的)
    下面就是加不加CH的一个对比
    在这里插入图片描述
    在这里插入图片描述

  • INCR

    • 当指定 INCR 选项时,ZADD 的行为与 ZINCRBY 命令相同 , ZINCRBY 命令用于对有序集合中指定成员的分数进行递增操作 ,此时 ZADD 也实现对成员分数的递增,表示增加某个member的score
    • ZADD+INCR后面只能再加一个score和一个member,不能再加其他选项了,返回值返回的是修改之后的score
    • 假设有序集合 myset 存在成员 member1 ,其分数为 10 ,执行 ZADD INCR myset 5 member1 ,等同于 ZINCRBY myset 5 member1 ,会将 member1 的分数在原来 10 的基础上增加 5 ,操作后分数变为 15 ,命令返回值为增加后的分数 15 。
zadd操作执行完毕之后的返回值为0是否代表插入失败呢?

答案是不一定,zadd返回值表示的是这条指令执行完毕之后新增的元素个数,返回值为0,表示这条指令执行过程中并没有添加新元素,并不能说明zadd失败了,举个反例:如果zadd用来修改现有member的score,即使成功,也会返回0

ZADD返回值

ZADD 命令返回新添加到有序集合中的成员数量,不包含那些被更新分数的成员。要是使用了 CH 选项,返回的就是被修改的成员数量(新增或更新分数的Member数量)。

ZADD 命令的时间复杂度

ZADD 命令的时间复杂度为 O(log(N)),其中 N 是有序集合中的元素数量。以前我们说的大部分命令,时间复杂度都近似可以看做是O(1),但这个ZADD的时间复杂度没办法看做O(1),真的是与问题规模N有关,N是有序集合中的元素数量
之所以时间复杂度那么高,原因是 Redis 采用跳跃表(Skip List)和哈希表来实现有序集合,在插入或更新成员时,需要对跳跃表进行调整以维持有序性。

zrange

zrange类似于lrange,可以指定一对下标构成的区间,查看zset指定区间内的元素。ZRANGE 指令中的下标本质是排序后列表的位置索引
zset会将其中的元素按照从小到大的顺序进行排序,zrange key 0 5的意思就是列出排序之后下标为0~5之间的所有元素

如何查看一个ZSET中的所有元素呢?

我们可以通过zrange key start stop查看key对应有序集合中下标区间[start, stop]内的所有元素,当start=0,stop=-1时,我们就可以通过zrange指令查看key对应ZSET中的所有元素

为什么ZSET中有下标这样的概念,而SET中就没有?

有序集合,本身元素就是有先后顺序的,谁在前,谁在后,都是很明确的!因此也就可以给这个有序集合赋予下标这样的概念了~~
SET是无序集合,所以没有下标这样的概念

zrange的时间复杂度

O(logN + M),其中找到start和stop对应下标的元素时间复杂度是logN,遍历[start, stop]区间内M个元素,时间复杂度O(M)

如何将输出数据的格式从二进制输出转化成汉字输出?

在这里插入图片描述

redis内部存储数据的时候,是按照二进制的方式来存储的,这就意味着,redis服务器不负责 “字符编码” 。要把二进制字节对回到汉字,还需要客户端来支持
我们先退出客户端,再次进来时,加上一个–raw选项,进来之后,客户端就会自动尝试将二进制字符翻译成汉字了

在zrange时,我不仅想看到member,我还想看到每个Member的score,我该怎么做?

在zrange指令最后加一个 withscores选项

我现在想修改zset中member="赵云"的分数,把他的分数改成97 ,我该怎么做?

也是用zadd指令:zadd key 97 赵云

如果修改一个人的分数之后,导致zset中各Member的次序发生了改变,请问我再次zrange查询时,输出的结果会按照修改后的顺序输出,还是按照修改之前的次序输出?

按照修改过后的升序输出

我们都知道zrange默认是按照升序输出结果的,有没有办法让它按照降序输出结果呢?

使用指令zrevrange key start stop,多出来的rev就表示reverse翻转,原本是升序,翻转输出就是降序

zcard

如何查看一个ZSET中有多少个元素?
zcard key

zcount

我想求出zset中分数位于[min, max]这个区间里的元素的个数,我该怎么做?
zcount key min max

zcount 的时间复杂度: O(logN),请问这个 O(logN)是咋求的?

下面我们简单分析一下求zcount的时间复杂度的思路
zcount 要指定 min 和 max 分数区间~~

  1. 先找到Min下标对应的元素.再找到max 下标对应的元素,这两次找的过程,时间复杂度都是O(logN)
  2. 这时候如果进行一个遍历,是不是就知道这里的元素个数了呢??
    如果区间中的元素比较多,此时要进行遍历,复杂度就成了 O(logN + M) ,M 是区间中元素个数,N 是整个有序集合的元素个数.

实际上我们可以进一步优化,因为 Zset 内部,会记录每个元素当前的 “排行”/“次序”。查询到元素,就直接知道了元素所在的 “次序”(相当于执行了一次ZRANK操作),我们可以直接把 max 对应的元素次序和 min 对应的元素次序做减法,中间的差值不就是中间的元素个数嘛,这个操作的时间复杂度是O(1)

我想查看zset中分数位于(min, max]这个左闭右开区间里的元素的个数,该怎么做?

zcount key (min max

我想查看zset中分数位于(min, max)这个开区间里的元素的个数,该怎么做?

zcount key (min (max
这个设计比较反人类,这种反人类的设计,后面改了不行吗?

  • 不好改的,因为要考虑兼容性!你只要改了,那前面很多人写好的代码可能就编不过了
  • 既然已经这么设定了,就只能将错就错了,后面想改,难!!因为要考虑兼容性!!广泛使用的软件,一旦在新版本中,引入和之前不兼容的特性,成本是非常高的!!
  • 考虑兼容性的案例: C++ (兼容 C)
  • 不考虑兼容性的案例: IPv6

一般来说,确实需要做出这种不兼容的修改,可以先把这个要修改的内容,标记成 “弃用” (给程序猿打个预防针,意思说这玩意后面几个版本还会支持,但是再过几个版本,我就不用了),同时推出新版本的方案,隔若干个版本之后,再逐渐的把这样的功能完成修改

指令zcount key min max中的min和max只能是整数嘛?可不可以是浮点数?

min 和 max 可以写成浮点数 (zset 分数本身就是浮点数)

在浮点数中,存在两个特殊的数值:inf: 无穷大.-inf: 负无穷大。zset 中分数也支持将max 和 min 的值取做inf 和 -inf
注意 !!! -inf是负无穷大,不是无穷小!!

我想查看zset中分数位于[min, max]这个区间里的元素有哪些,我该怎么做?

ZRANGEBYSCORE=ZRANGE + BYSCORE,意思就是根据分数找元素
使用格式:ZRANGEBYSCORE key min max [WITHSCORES]
时间复杂度:O(logN + M),与zcount的时间复杂度是一个道理

ZPOPMAX

ZPOPMAX key:删除key对应ZSET中最大的元素,并将这个元素返回到屏幕上
ZPOPMAX key count:删除key对应ZSET中降序排序前count位的元素(删除从最大到前count大之间的所有元素)

如果存在多个元素,分数相同,同时为最大值,zpopmax 删的时候,应该怎么删?

这个要分情况讨论,主要是看参数count的取值

  • 如果count默认缺省了,那么count就视为默认值1
    • 如果存在多个元素,分数相同,同时为最大值,zpopmax 删的时候,仍然只删除其中一个元素!!分数相同会按照 member 字符串的字典序决定先后!!
  • 如果count有明确的取值,那count等于几,就删几个,删除的顺序按照score和字母序来排
ZPOPMAX的时间复杂度是多少?

O(log N * M)
在这里插入图片描述

既然是尾删,为什么我们不把这个最后一个元素的位置特殊记录下来,这样就省去了查找的过程, 后续删除不就可以O(logN) => O(1) 了嘛?

这个事情是有可能的!!但是很遗憾,目前 redis 并没有这么做~事实上,redis 的源码中,针对有序集合,确实是记录了 尾部 这样的特定位置。但是在实际删除的时候,并没有用上这个特性,而是直接调用了一个 “通用的删除函数”(给定一个 member 的值,进行查找找到位置之后再删除)

此处我认为是存在优化空间的,虽然存在这样的优化空间,但是未来真的会这么优化吗?

也不好说,因为当前的这个 logN 的速度其实是不慢的!!如果 N 不是非常夸张的大,基本是可以近似看做 O(1) 的。优化这种活,要优化到刀刃上,优化一般是要先找到性能瓶颈,再针对性的优化,比如下图中,第4步就是性能瓶颈,第一步并不是性能瓶颈,所以使劲儿优化没啥用(木桶的短板效应)
在这里插入图片描述

ZPOPMIN

和ZPOPMAX非常相似,唯一区别就是,删除的不是有序集合中score最大的member,而是score最小的member
语法:ZPOPMIN key [count]
时间复杂度: O (logN * M)
此处的 zpopmin 和上面的 zpopmax 的逻辑是一致的(同一个函数实现的),只不过zpopmax是尾删(升序排列,尾部最大),zpopmin是头删(升序排列,尾部最小)。也同样存在优化空间

BZPOPMAX

B+ZPOPMAX,就是ZPOPMAX的阻塞(BLOCK)版本,即阻塞式POP有序集合中最大的元素

语法:BZPOPMAX key [key...] timeout

  • 每个 key 都是一个 有序集合.
  • timeout 表示超时时间,即最多等多久,单位是 s,支持小数形式. 写作 0.1 就是 100ms~~

时间复杂度:O(log N)
这个O(log N)主要是在zset(跳表)中查找最大值的时间复杂度

BZPOPMAX作用

  • 当参数中的所有key对应的有序集合不全为空时,BZPOPMAX指令会直接从第一个非空的有序集合中弹出分数最大的元素 ,然后退出返回
  • 当参数中的所有key对应的有序集合都为空时,BZPOPMAX指令会被阻塞timeout指定的时间;
    • 当在timeout阻塞时间内,参数中的所有key对应的有序集合不全为空时,BZPOPMAX指令会解除阻塞状态,会从第一个非空的有序集合中弹出分数最大的元素 ,然后退出返回
    • 如果在timeout阻塞时间内,参数中的所有key对应的有序集合一直全为空,那么时间一过,指令直接退出结束
为什么要引入阻塞版本的ZPOPMAX?

即引入阻塞版本的ZPOPMAX,主要是用于构建一个带有 “阻塞功能” 的优先级队列

我们前面说List的时候,讲过BLPOP与BRPOP,这俩阻塞版本的插入操作,主要用于构建消息队列。咱们这里的 “有序集合” 也可以视为是一个 “优先级队列”。有的时候,也需要一个带有 “阻塞功能” 的优先级队列,

如果BZPOPMAX指令中的key不止一个,请问时间复杂度会不会变成O(Mlog N)(M是指令中key的个数)?

答案是不会,原因主要就是,无论你参数中的key有多少,BZPOP实际操作的时候,只会删一个key对应有序结合中的最大值,删完就结束了,后面的就不会删了

BZPOPMIN

BZPOPMIN key [key ...] timeout
用法和 BZPOPMAX 一样一样的~~唯一区别就是,BZPOPMIN拿的是最小的元素(头删)
时间复杂度,也是 O(logN)

ZRANK

如何查看一个member的score在zset中的排名?
ZRANK key member(返回从0开始的升序排名)
ZRANK指令返回值:

  • 如果成员存在于有序集合中,返回该成员的排名(注意:默认返回的排名是该成员成绩的升序排名,因为zset内部排序就是升序排的)。
  • 如果成员不存在于有序集合中,返回 nil。
  • 如果zset的key不存在,返回 nil

在这里插入图片描述

ZSET的时间复杂度:ZRANK key member最主要是有一个 查询位置 的过程,即要先在跳表中根据member的分数查到member这个元素,时间复杂度 O(logN)
zcount 在计算的时候,就是先根据分数找到元素,再根据元素获取到排名,再把排名一减,得到了元素个数。

注意:zrank 返回的排名,是从0开始的升序排名,这个排名其实你就可以理解成,将集合中的元素按照升序排序之后依次放到一个数组里,你zrank返回的值就是member在这个数据里的下标

ZREVRANK

ZREVRANK key member(返回从0开始的降序排名)

ZSCORE

如何查看有序集合中一个member的score?
ZSCORE key member
时间复杂度:O(1)

前面根据 member在zset中的下标 找member,时间复杂度都是 logN ,这里也是要先找元素呀,为啥时间复杂度是O(1)呢?

在Redis中,有序集合(Sorted Set,ZSet) 是通过跳表(Skip List)哈希表 两种数据结构共同实现的,其中:

  • 跳表的每个节点都会一个存member的信息(包括Member的score),并以<score,member>键值对为排序依据,在排序时首先比较score,score相同时再以字母顺序比较member
  • 哈希表以集合中的成员(member)作为key,可以直接索引到跳表中对应的member节点

到这里你肯定就明白为啥根据member拿score时间复杂度是O(1)了,主要原因就是哈希表以member为索引,可以直接找到该member在跳表中的节点,而member的score就存在这个节点中,所以时间复杂度肯定是O(1)

那为啥前面我们用下标查score,时间复杂度是O(log n)呢?
member的score存在跳表的某个节点中,你想要根据下标在跳表中找到该节点,其本质是类似于二分查找的,所以时间复杂度是O(log n)

ZREM

如何删除有序集合中的一个member?
ZREM key member1 member2 ...

ZREM时间复杂度:O(log N * M)(为什么?)
  1. 哈希表定位:首先通过哈希表快速找到要删除的成员对应的跳表节点,这一步是 O (1)(单元素情况下)。
  2. 跳表节点删除:从跳表中删除节点时,需要更新该节点前后所有层级的指针和跨度(span)信息。由于跳表的层级数平均为 O (log N),因此删除单个节点的操作复杂度为 O (log N)。
  3. 多元素处理:如果一次删除 M 个成员,整体复杂度就是 M * log N。

ZREMRANGEBYRANK

如何一次性删除有序集合中排名前三的元素?
ZREMRANGEBYRANK=ZREM + RANGEBYRANK(按照排名)
使用ZREMRANGEBYRANK key start stop指令可以移除有序集合中[start, stop]范围内的所有元素 。例如,若 start 为 0,stop 为 2 ,则会移除有序集合中下标 为0 1 2 的元素。(下标从0开始)
语法:ZREMRANGEBYRANK key start stop
ZREMRANGEBYRANK时间复杂度:O((M + log N) * log N),其中 N 是有序集合中的元素数量,M 是被移除的元素数量 。为什么呢?

  1. 定位范围边界:需要先找到排名范围内的起始和结束节点。由于跳表按分数排序,通过多层索引和跨度计算定位边界节点的时间复杂度为 O(log N)

  2. 删除范围内元素

    • 对于范围内的 M 个元素,每个元素的删除都需要更新跳表中相关节点的指针和跨度信息。
    • 单个元素的删除操作复杂度为 O(log N)(因跳表层级平均为 log N),因此 M 个元素的删除总复杂度为 O(M * log N)
  3. 整体复杂度:定位边界的 O(log N) 加上删除 M 个元素的 O(M * log N),合并后就是 O((M + 1) * log N),进一步简化就是 O((M + log N) * log N)

ZREMRANGEBYSCORE

注意:ZREMRANGEBYSCORE与ZREMRANGEBYRANK的核心区别,这俩一个是按分数(跳表比较依据)划范围,一个是按排名(有序数组下标)划范围
我想把有序集合中分数位于[min, max]这个区间里的元素全删了,我该怎么做?
ZREMRANGEBYSCORE=ZREM + RANGEBYSCORE(按照分数)
ZREMRANGEBYSCORE key min max
作用:它会移除有序集合中分数在 min(包含)到 max(包含)范围内的所有元素 。比如设置 min 为 10,max 为 20,那么有序集合里分数在 10 到 20 之间的元素都会被移除。

返回值:返回成功移除的元素数量。
时间复杂度:O (log (N)+M) ,其中 N 是有序集合中的元素数量,M 是被移除的元素数量 。

ZINCRBY

我想修改一个Member的score,比如把它的score加10分,我该怎么做?
ZINCRBY key increment member
其中increment就代表要增加的分数值,可以是正数,也可以是负数。如果是正数,成员的分数会增加;如果是负数,成员的分数会减少。

修改分数之后,该member在集合中的排序大概会发生改变,请问我下次执行zrange操作时,该元素输出的次序是修改之前的,还是修改之后的?

修改之后的次序

如何求几个有序集合之间的交集、并集、差集?

5版本仅提供了两个接口zinterstore、zunionstore,将集合运算的结果存入另一个key指向的有序集合中

ZINTERSTORE介绍(求交集)

这个函数的功能是计算多个有序集合的交集,并把交集结果存储到一个新的有序集合里
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM | MIN | MAX]

  • destination:是一个键名key,用于指向存储交集结果的目标zset有序集合

  • numkeys:参与交集计算的有序集合的数量。

    • 前面介绍的很多命令也是支持多个 key 的呀~~为啥它们不用指定numkeys呢?
      • 主要原因就是ZINTERSTORE的后面选项有点多,你要是不指定key选项的个数,万一redis把后面的其他选项比如说weights或者sum啥的看成是一个键名咋办呢?
      • 有了numkeys 描述出 key 的个数之后,就可以明确的知道,后面的 “选项” 是从哪里开始了,从而有效避免选项和 keys 混淆
      • 此处的设定,特别像之前学过的HTTP 协议中的一个知识点:请求报头中 Content-Length 描述了 正文的长度!!如果没有这个字段,数据错了,就容易产生 粘包问题
    • 什么是粘包问题?如何解决粘包问题?
      • HTTP 在传输层,是基于 TCP。TCP 是面向字节流的!!粘包问题,就是在解析报文的时候,分不清这个报文的边界,导致报文读取出错的问题。粘包问题是面向字节流这种 IO 方式中的一个普遍存在的问题~~
        除了TCP之外,文件读写也是面向字节流的!也需要考虑粘包问题!那如何解决粘包问题呢?
      • 就是在报头中通过类似 Content-Length 这样的字段提前明确包的长度和包的边界
  • key [key …]:参与交集计算的有序集合的键名,可以有多个。

  • [WEIGHTS weight [weight …]] (可选):为每个参与计算的有序集合设置权重,默认为 1 。

    • 此处指定的权重,相当于一个系数,计算交集元素分数时会根据此权重进行运算。例如,若有两个有序集合,分别设置权重为 2 和 3 ,则交集元素(两个集合中都有这个元素,但是两个集合中该元素的score不相同)的分数计算会考虑对应权重,即最终的score可能是score1 * 2 + score2 * 3
      注意:这里的权重也可以传小数,即 ZINTERSTORE命令后面加上weights 0.4 0.6 ,这样的话最终并集中member的score可能是score1 * 0.4 + score2 * 0.6
    • 有序集合中,member 才是元素的本体。score 只是辅助排序的工具人,因此,在进行比较“相同”的时候,只要 member 相同即可。score 不一样,就不一样呗~~如果 member 相同,score 不同,进行交集合并之后的,最终分数,咋算??如果前面weignts选项不为空,则后面的聚合方式一般默认选SUM,按照 加权运算规则,算出最终的score
  • [AGGREGATE SUM | MIN | MAX] (可选):聚合方式。

    • SUM 表示将交集元素(各个集合中都有的member)在各有序集合中的分数相加作为结果集合中元素的分数;
    • MIN 表示取交集元素(各个集合中都有的member)在各有序集合中分数的最小值作为结果集合中元素的分数;
    • MAX 表示取交集元素(各个集合中都有的member)在各有序集合中分数的最大值作为结果集合中元素的分数 ,默认是 SUM 。
ZINTERSTORE时间复杂度

ZINTERSTORE 的时间复杂度为 O(N * K) + O(M * log(M))

  • O(N * K)部分
    • 其中 N 表示参与交集计算的有序集合中,成员数量最少的那个有序集合的元素个数 ;
    • K 是参与交集计算的有序集合的数量。
    • 这部分时间复杂度主要来自于对各个有序集合的遍历操作,因为要找出多个有序集合的交集元素,需要对每个集合进行一定程度的扫描,集合数量越多、最小集合元素越多,这部分耗时就越长。
  • O(M * log(M))部分
    • 这里的 M 是指计算得到的结果有序集合中的元素数量 。这部分时间复杂度主要和对结果集的处理有关,通常涉及到对结果集中元素进行排序等操作(比如为了保证有序集合的有序性 ),采用一些比较高效的排序算法(如堆排序等 )时,时间复杂度通常为 O(M * log(M))。

ZUNIONSTORE介绍(求并集)

功能:计算多个有序集合的并集,并将结果存储到一个新的有序集合中。
格式:ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM | MIN | MAX]
ZUNIONSTORE的参数解释以及时间复杂度都是和ZINTERSTORE一样一样的,故不再赘述

ZSET类型的内部编码方式

ZSET类型的内部编码方式主要有两种

  • 如果有序集合中的元素个数较少,或者单个元素体积较小。就可以使用 ziplist 压缩列表来存储~~ 节省内存空间.
  • 如果当前元素个数比较多,或者单个元素体积非常大~~就必须使用 skiplist 来存储了

在 Redis 中,对于有序集合(Sorted Set )的底层存储结构选择有一定策略:

  • zset-max-ziplist-entries:它是一个配置参数,用于设定当有序集合使用压缩列表(ziplist )存储时,**元素个数的最大阈值 **
    • 如果有序集合的元素个数未超过这个值,且单个元素体积较小,就会采用压缩列表存储以节省内存。
    • 当元素个数达到或超过该阈值,且满足其他条件(如单个元素体积较大 )时,可能会转换为跳表(skiplist )存储。
  • zset-max-ziplist-value:也是一个配置参数,用于设定压缩列表中单个元素所占空间的最大阈值 。
    • 如果有序集合中的元素体积都未超过这个值,且元素个数也满足条件,就会用压缩列表存储。
    • 一旦元素体积超过该值,即便元素个数未达 zset-max-ziplist-entries 限制,也可能促使存储结构转换为跳表。

重点在于理解 Redis 根据元素数量和元素体积等限制,动态选择存储结构以平衡内存占用和操作性能的策略思想,而非死记这两个参数的具体数值。

什么是跳表skiplist?

简单来说,跳表是一个 “复杂链表”。查询元素时间复杂度 logN(和平衡二叉树是一个量级的)
相比于树形结构,更适合范围查找~~ (B+树也是比较适合范围查找)

ZSET的应用场景

最关键的应用场景就是——排行榜系统,比如:

  • 微博热搜
  • 游戏天梯排行
  • 成绩排行…

为什么排行榜系统适合用ZSET类型呢?

有人的地方,就有江湖,有了江湖,就得排个座次
关键点: 排行榜用来排行的“分数”是实时变化的,这就要求我们要高效地对排行榜进行实时更新,而这其实就是ZSET的最大特点!
使用 zset 来完成上述操作,就非常简单!比如游戏天梯排行,只需要把玩家信息和对应的分数给放到有序集合中,就自动形成了一个排行榜,随时可以按照 排行(下标),按照分数 进行范围查询,随着分数发生改变,也可以比较方便地用zincrby 修改分数,排行顺序也能自动调整(logN)

游戏玩家这么多,这个zset能存下嘛??

答案是能存下!存储1亿用户的数据,可以也只需要一个G的内存空间,你的计算机内存都能存下!更别说Redis存储服务器了

像现在的排行榜比如微博热度,往往是要考虑多方面因素(比如浏览量、点赞量、转发量、收藏量、评论量等等),我们要根据每个维度,计算得到综合得分,最终计算出热度!!请问这个过程用redis如何实现呢?

很简单,直接用 zinterstore / zunionstore 按照加权方式处理
我们可以把上述每个维度的数值都放到一个有序集合中. member 就是 微博的 id,score 就是各自维度的数值,通过 zinterstore 或者 zunionstore 把上述有序集合按照约定好的权重,进行集合间运算即可,得到的结果集合,的分数就是热度!(排行榜也就顺带出来了)

具体设置多少个维度,每个维度权重怎么分配,以及怎么设定是最优呢?
公司一般都有专门的团队做这块的事情~~(通过一些 人工智能 的方式来进行计算)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值