zset有序集合介绍
Question:这里的有序单纯就是升序/降序,那么排序的规则是啥
Answer:给zset中的member同时引入了一个属性,分数(score),浮点类型,每个member都会安排一个分数,进行排序的时候,就是按照此处的分数大小来进行升序/降序来排序
- zset中的member仍然要求是唯一的(score则可以重复)
- zset主要还是用来存member的,score只是辅助
zset常用命令
zadd
- 添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负 极限也是合法的。
- 时间复杂度为O(logN),N表示有序集合中的元素个数,之前的Hash,Set,List很多时候,添加元素,都是O(1),此处,zset则是logN:由于zset是有序结构,要求新增的元素,要放到合适的位置上,同样logN也是利用了有序性(跳表)
ZADD 的相关选项:
- XX:仅仅⽤于更新已经存在的元素,不会添加新元素(member)。
- NX:仅⽤于添加新元素,不会更新已经存在的元素(member);如果当前member不存在,此时就会达到"添加新member"的效果,如果当前member已经存在,此时就会更新分数
- CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。
- INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和 分数
- LT:只更新已经存在的元素,如果这个元素(要插入的元素)比现在的元素要小,那么就不会添加新的元素
- GT:只更新已经存在的元素,如果这个元素比现在的元素要大,那么就不会添加新的元素
添加的时候,既要添加元素,又要添加分数:member和score称为是一个"pair"
不要把member和score理解成"键值对",可以理解为属性:对于有序集合来说,既可以通过member来匹配score,也可以通过score来找到member
Question:如果有重复的score,如何解决?
Answer:通过分数来排序,如果分数相同,再按照元素自身字符串的字典序来排序
有序集合排序规则,zset内部就是按照升序方式来排序的
zrange 查看有序集合中的元素详情,类似于lrange,可以指定一对下标构成的区间
这里的有序集合本身就是有先后顺序的,谁在前谁在后,都是很明确的
redis内部存储数据的时候,是按照二进制的方式来存储的,这就意味着redis服务器不负责"字符编码",要把二进制字节对应汉字,还需要客户端来支持,即在启动客户端时使用 --raw命令即可
Question:为什么这样排序?
Answer:因为是按照升序的结构来排序的
修改操作
如果修改的分数,影响到了之前的顺序,就会自动的移动元素位置,保持原有的升序不变
zadd在默认情况下就是返回新增元素的个数
如果已经存在了就不支持这种行为
如果是xx就支持这种存在修改行为
如果不存在就不支持这种行为
ch操作
incr
zcard
获取⼀个 zset 的基数(cardinality),即 zset 中的元素个数
时间复杂度:O(1) 返回值:zset 内的元素个数(member)。
zcount
- 返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
- 时间复杂度:O(log(N)) :先根据min找到对应的元素,再根据max找到对应的元素,如果进行一个便利,是不是就知道这里的元素个数了呢?如果元素较多,此时要进行遍历,那么时间复杂度为O(N),zset内部会记录每个元素当前的次序,查询到元素就直接知道元素所在的"次序",就可以直接把max对应的元素次序和min对应的元素次序,减法即可
- min和max是可以写成浮点数的(zset本身就是浮点数),在浮点数中,存在两个特殊的数值:inf(无穷大),-inf(负无穷大)
返回值:满⾜条件的元素列表个数。
zset中的member和score的关系,不能理解成"键值对"
包含97,但不包含95
这里并不符合直觉既然已经这么设定了,只能遵守这样的规则,既然已经这么设定了,就只能将错就错了!而且还要考虑兼容性,一般来说,如果想要做出这种不兼容的修改,可以先把这个修改的内容标记成弃用,隔若干个版本在逐渐的把功能完成修改
- 考虑兼容性的案例:C++(兼容C)
- 不考虑兼容性的案例:IPv6
zrange
- 返回指定区间⾥的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。
- 时间复杂度:O(log(N)+M) :此处要根据下标找到边界值,start对应的位置,接下来就需要遍历了,start-stop的元素个数为M
- 返回值:区间内的元素列表。
zrevrange
- 返回指定区间⾥的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。
- 备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
- rev-reverse逆序
- 时间复杂度:O(log(N)+M) 返回值:区间内的元素列表。
- 此处的区间0 -1两个参数顺序是不需要变的,也就是降序
zrangebyscore
- 返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
- 备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
- 时间复杂度:O(log(N)+M) 返回值:区间内的元素列表。
zpopmax
- 删除并返回分数最⾼的 count 个元素
- 时间复杂度:O(log(N) * M) ,N为有序集合的元素个数,M表示count,此处删除的是最大值!!!有序集合,最大值就相当于最后一个元素(尾删),既然是尾删,为什么不把最后一个元素特殊记录下来,后续删除不就可以O(1)了吗,省去了查找的过程?Redis目前并没有这么做,Redis源码中针对有序集合确实是记录了尾部这样的特定位置,但是在实际删除的时候,并没有用上这个特性,而是直接调用了一个"通用的删除函数",此处是存在优化空间的(当然了,优化要优化在刀刃上,一般先找到性能瓶颈,再针对性的优化)
- 返回值:分数和元素列表。
- 联想到使用优先级队列来解决topK问题,这里类似
如果存在多个元素分数相同并且同时为最大值,仍然只删除其中一个元素,分数虽然是主要因素,如果分数相同会按照member字典序来排序
zpopmin
bzpopmax
- ZPOPMAX 的阻塞版本。(联想到针对list的blpop,brpop实现类似于阻塞队列的效果)
- 这里的有序集合也可以视为"优先级队列",有的时候,也需要一个带有"阻塞功能"的优先级队列,每个key都是一个有序集合,阻塞也是在有序集合为空的时候触发阻塞,阻塞到有其他客户端插入元素,timeout表示超时时间(单位为秒,支持小数形式),最多阻塞多久
- 时间复杂度:O(log(N)),删除最大值花的时间,如果当前bzpopmax同时监听了多个key,假设key是M个,此时时间复杂度是O(logN*M)?
- 并不是,是从这若干个key中只删除一次,并不是每个key的max都删除
- 返回值:元素列表。
zset为空,左侧进行阻塞操作,右侧添加元素时,直接删除,若不为空,直接删除,不论count为多少,有三个属性:key标识了有序集合,以及member和score
zpopmin
删除并返回分数最低的 count 个元素。
时间复杂度:O(log(N) * M),M为count值, 返回值:分数和元素列表。
bzpopmin
ZPOPMIN 的阻塞版本,用法和bzpopmax一致
时间复杂度:O(log(N)),多个有序集合只删除一次,返回值:元素列表。
zrank
返回指定元素的排名,升序。
时间复杂度:O(log(N)),zcount算的时候就是先根据分数找到元素,再根据元素获取到排名,再把排名意见,就得到了元素个数,最主要是有一个查询位置的过程,返回值:排名。
zrank得到的下标,是从前往后算的
zrevrank
返回指定元素的排名,降序。
时间复杂度:O(log(N)) 返回值:排名。
zscore
- 返回指定元素的分数。
- 时间复杂度:O(1),前面,根据member找元素,都是logN,这里也是要先找元素?
- 此处相当于redis对于这样的查询操作做了特殊优化,付出了额外的空间代价,针对这里进行了优化到O(1)实现
- 返回值:分数。
zrem
- 删除指定的元素。
- 时间复杂度:O(M*log(N)),N是整个有序集合中元素个数,M是参数中member的个数
- 返回值:本次操作删除的元素个数。
zremrangebyrank
- 按照排序下标,升序删除指定范围的元素,左闭右闭。
- 时间复杂度:O(log(N)+M),N是整个有序集合的元素个数,M是start-stop之间的元素个数,此处查找位置,只需要进行一次(不需要重复进行)
- 返回值:本次操作删除的元素个数。
zremrangebyscore
按照分数删除指定范围的元素,左闭右闭。
时间复杂度:O(log(N)+M) 返回值:本次操作删除的元素个数。
zincrby
- 为指定的元素的关联分数添加指定的分数值
- 时间复杂度:O(log(N))
- 返回值:增加后元素的分数。
不光会修改分数内容,也能同时移动元素位置,保证集合有序
同时也可以支持减法和小数
zinterstore
- 求出给定有序集合中元素的交集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元 素对应的分数按照不同的聚合⽅式和权重得到新的分数。
选项
- numkeys:几个交集,同时也是防止混淆后面的选项和key
- 此处比较像http协议中的请求头(heade)中的Content-Length,描述了正文的长度,如果这个没有描述好,就可能产生粘包问题:HTTP在传输层是基于TCp,TCp是面向字节流的,粘包问题,是面向字节流这种IO方式中的一个普遍存在的问题
- destination:要把结果存储到哪个key对应的zset中
- weights:权重,此处指定的权重,相当于一个系数,会乘以当前的分数
- 有序集合中,member才是元素的主体,score注释辅助排序的工具,因此在比较"相同"的时候,只要member相同即可,如果member相同,score不同,那么最终结果是多少呢!?
- aggregate:sum | min | max,如果是max,那么最终结果就是最大的score,以此类推
- 时间复杂度:O(N*K)+O(M*log(M)),化简得到近似值为N*logN(k不会很多,近似看作1,把N和M看作是近似的),N 是输⼊的有序集合中, N是最⼩的有序集合的元素个数; K 是输⼊了几个有序集合; M 是最终结果的有序集合的元素个数,取决于redis源码咋写的
- 返回值:目标集合中的元素个数
zunionstore
- 求出给定有序集合中元素的并集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元 素对应的分数按照不同的聚合⽅式和权重得到新的分数,参数和上一个交集命令的参数一致
- 时间复杂度:O(N)+O(M*log(M)) N 是输⼊的有序集合总的元素个数; M 是最终结果的有序集合的元素个数,近似为O(M*logM)
- 返回值:⽬标集合中的元素个数
zunionstore用法和zinterstore用法一致
zset命令小结
zset编码方式
- ziplist(压缩列表):当有序集合的元素个数⼩于 zset-max-ziplist-entries 配置(默认 128 个), 同时每个元素的值都⼩于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会⽤ ziplist 来作 为有序集合的内部实现,ziplist 可以有效减少内存的使⽤。
- skiplist(跳表):当 ziplist 条件不满⾜时,有序集合会使⽤ skiplist 作为内部实现,因为此时 ziplist 的操作效率会下降。
- 当元素个数较少且每个元素较⼩时,内部编码为 ziplist:
- 当元素个数超过 XXX 个,内部编码 skiplist:
- 当某个元素⼤于 XXXX 字节时,内部编码 skiplist:
zset的应用场景
最关键的应用场景:排行榜系统
- 微博热搜
- 游戏天梯排行
- 成绩排行
...
- 关键要点:用来排行的"分数"是实时变化的,虽然是实时变化的,也能够高效的更新排行
- 使用zset来完成上述操作,就非常简单,比如游戏天梯排行,只要把玩家信息和对应的分数给放到有序集合中即可,自动就形成了一个排行榜,随时可以按照排行(下标),按照分数进行范围查询,随着分数发生改变,也可以比较方便的,zincrby修改分数,排行顺序也能自动调整(logN)
- 游戏玩家这么多,此时都用这个zset来存(内存),能存下嘛?
- 在王者农药中,往多了算,就按一亿玩家,userId 4个字节来理解,score 8个字节来理解,表示一个玩家,大概是12个字节,那么一亿玩家就有12亿字节,大约是1.2GB,单位换算,一定要张口就来:thousand kb,million mb,billion gb
- 对于游戏排行榜,这里的前后顺序非常容易确定,但是有的排行榜就要复杂一些,微博热榜,需要考虑综合数值,浏览量,点赞量,转发量,评论量,这里就是权重位,具体有多少个维度,每个维度怎么分配的,以及怎么设定是最优的...
- 根据每个维度,计算得到综合得分也就是热度,此时借助 zinterstore/zunionstore按照加权方式处理,此时就可以把上述每个维度的数值都放到有序集合中,member就是微博的Id,score就是个维度的数值,通过zinterstore/zunionstore把上述集合进行加权处理
注意事项:zset是一个选择,但并不是一定要用redis中的zset,要综合考虑