zest集合的特点是唯一性,有序性(zset中的元素member是升序的);zset不仅会存储元素的内容,还有给每一个元素存储一个用来排序的分数score,即可查看元素对应的分数,也可通过分数查询元素(member和score是一对"pair"),所以元素和分数并不是严格意义上的键值对,键值对只是可以通过key查询到value,反过来则不行。
命令 | 时间复杂度 |
ZADD key score member [score member ...] | O(k * log(n)),k 是添加成员的个数,n 是当前有序集合的元素个数 |
ZCARD key | O(1) |
ZSCORE key member | O(1) |
ZRANK key member | O(log(n)),n 是当前有序集合的元素个数 |
ZREVRANK key member | O(log(n)),n 是当前有序集合的元素个数 |
ZREM key member [member ...] | O(k * log(n)),k 是删除成员的个数,n 是当前有序集合的元素个数 |
ZINCRBY key increment member | O(log(n)),n 是当前有序集合的元素个数 |
ZRANGE key start end [WITHSCORES] | O(k + log(n)),k 是获取成员的个数,n 是当前有序集合的元素个数 |
ZREVRANGE key start end [WITHSCORES] | O(k + log(n)),k 是获取成员的个数,n 是当前有序集合的元素个数 |
ZRANGEBYSCORE key min max [WITHSCORES] | O(k + log(n)),k 是获取成员的个数,n 是当前有序集合的元素个数 |
ZREVRANGEBYSCORE key max min [WITHSCORES] | O(k + log(n)),k 是获取成员的个数,n 是当前有序集合的元素个数 |
ZCOUNT key min max | O(log(n)),n 是当前有序集合的元素个数 |
ZREMRANGEBYRANK key start end | O(k + log(n)),k 是获取成员的个数,n 是当前有序集合的元素个数 |
ZREMRANGEBYSCORE key min max | O(k + log(n)),k 是获取成员的个数,n 是当前有序集合的元素个数 |
ZINTERSTORE destination numkeys key [key ...] | O(n * k) + O(m * log(m)),n 是输入的集合最小的元素个数,k 是集合个数,m 是目标集合元素个数 |
ZUNIONSTORE destination numkeys key [key ...] | O(n) + O(m * log(m)),n 是输入集合总元素个数,m 是目标集合元素个数 |
相关命令:
zadd:
zadd key [NX | XX] [GT | LT] [CH] [INCR] score menber [score menber ...]
zadd命令有很多可选的参数,以下是官网原文:
- XX表示如果元素member已经存在则对其进行更新,如果不存在并不添加新的元素
- NX表示如果元素member已经存在则不对其进行更新,如果不存在则创建新的元素
- LT表示less then,元素新分数小于当前分数时更新元素分数,元素不存在时并不会创建元素
- GT表示greater then,元素新分数大于当前分数时更新元素分数,元素不存在时并不会创建元素
- CH:如果不添加CH命令返回新增元素个数,如果命令新增失败可能返会0,对元素进行修改返回值的也0,容易混淆;添加CH,命令会返回被影响的元素个数,包括被修改的以及新增的,因为是添加命令,所以不会有元素被删除。
- INCR:添加INCR后zadd命令功能ZINCRBY相似,但此时只能操作一个member-score pair;
左图为INCR的演示。
注意:redis5版本并不支持[LT | GT]选项。
zadd添加元素的时间复杂度时O(log(N)),zset添加元素需要先找到添加的位置再进行添加,利用zset有序的特性,查找到对应位置的时间复杂度为O(log(N));zset元素的排序会先按照score进行排序,如果score相同咋按照字典序进行排序。
zrange:
查询有序集合指定区间的元素,因为zset是有序的,所以就可以赋予下标的概念。
zrange key start end [withscores]
//withscores:同时查询member的score
//查询结果为升序的
zcard:
查询zset中member的个数:
zcard key // 该key必须是zset的类型,否则会报错
zcount:
在一定分数范围内查找member
zcount key min max // 闭区间
zcount key (min (max // 开区间
zcount的时间复杂度为O(log(N));zset底层是跳表,我们可以根据min、max先找到临界的元素,在移动一下指针的执行就可以实现对区间内的元素删除操作,所以删除区间内元素的时间复杂度和查找元素的时间复杂度一致,为O(log(N)),N为zset中元素的规模。
min、max还支持小数。inf代表无穷大,-inf代表无穷小。
zrevrange:
zrevrange key start end [withsocres] //降序查找闭区间(下标)内的元素
zrangebyscore:
和zcount类似,时间复杂度为O(log(N)+M),N为zset中元素的个数,M为区间内元素的个数。
redis6.2版本以后已弃用。
zrangebyscore key min max [withsocre]
zpopmax:
zpopmax key count //删除分数最该的前k个元素,软着陆。
返回被删除的元素,如果存在分数相同的元素,只删除其中之一,删除字典序在大的那个元素。
时间复杂度为O(log(N)*M),N为zset中的元素规模,M为count的规模。这里删除的思路是先通用的查询方法查询到元素(max)时间复杂为O(logN),再进行删除,因此删除count个最大值的时间复杂度为O(log(N)*M),这个可以对删除最大值进行优化,删除最大值相当于尾删,可以把最后一元素记录下来,实现O(1)复杂度的尾删,不过遗憾的是redis目前并没有这么操作。
bzpopmax:
bzpopmax key [key ...] timeout
删除多个zset中某个zset的最大值元素,如果所有zset中没有元素redis客户端就阻塞,知道某一个zset中有别添加元素,客户端最多阻塞timeout时间。如果有多个zset中都有元素,就删除指令中靠前的zset的最大值。时间复杂度是O(logN)
zpopmin & bzpopmin :
zpopmin key count //删除前count个最小的元素
bzpopmin key [key ...] timeout //删除最小元素
zrank:
zrank key member //返回member的下标,只能写一个member
zset元素是升序的,元素下标由最小值开始,最小值下标为0。
zrevrank:
zrevrank key member //返回member的逆序下标,只能写一个member
和zrank相反,返回倒数的下标值
zscore:
zscore key member //查询member的score,时间复杂度为O(1)
此处redis对于这样的查询操作做了优化,付出了额外的空间代价,将时间复杂度优化到了O(1)
zrem:
zrem key member [member ...] //删除指定元素
时间复杂度为O(log(N)*M)
zremrangebyrank:
zremrangebyrank key start end // 根据下标区间删除zset中的元素
时间复杂度为O(logN+M),只需要查找一次即可
zremrangebyscore:
zremrangebyscore key start end // 根据score区间删除zset中的元素
时间复杂度为O(logN+M)
zincrby:
zincrby key increment member //member的score增加increment,同时更新排序
集合运算(交、并、差):
这一类相关的操作有很多,redis5中仅支持以下两个,高版本中可能支持zinter、zunion、zdiff等等,计算score思想和zinterstore一致:
zinterstore:
zinterstore destination numkeys key [key ...] [WEIGHTS weigth [weigth ...] [AGGREGATE <SUM | MIN | MAX>]
求集合的交集,并创建新的集合destination ,因为参数个数不确定,所以添加了numkeys指明有序集合的个数。新集合元素的score运算方法有以下几种:最小值、最大值、平均值、加权平均值。
此处的numkeys有点两HTTP请求中的content-Length,已解决粘包问题。
粘包问题是在计算机网络通信中常见的一种现象,指的是发送方在发送数据时,由于网络传输的特性,接收方可能会将多个数据包连续地接收并合并成一个大的数据块,造成数据粘在一起的现象。
粘包问题常见于使用传输控制协议(TCP)进行数据传输的场景,因为TCP是一种面向流的协议,它并不保留消息边界,而是按照字节流的方式进行数据传输。这就意味着发送方在发送数据时,数据会被拆分成TCP报文段进行传输,而接收方则需要根据自己的接收缓冲区大小和接收速度来决定如何接收和处理数据。
以下是一些常见的解决粘包问题的方法:
- 定长包:发送方在发送数据时,固定每个数据包的长度,接收方按照固定长度进行数据接收和处理。这样可以确保接收方每次接收到的数据长度是固定的,从而解决了粘包问题。但是这种方法可能会造成数据的浪费,因为如果实际数据长度小于定长包长度,会有一些空白字节被填充进去。
- 分隔符:发送方在数据包之间添加特殊的分隔符,接收方根据分隔符将接收到的数据进行拆分和处理。常见的分隔符可以是换行符、制表符等。这种方法需要保证分隔符不会出现在实际数据中,否则会造成解析错误。
- 长度字段:发送方在每个数据包的头部添加表示数据长度的字段,接收方首先读取长度字段,然后根据长度字段的值读取相应长度的数据进行处理。这种方法可以准确地知道每个数据包的长度,从而解决了粘包问题。
- 基于应用层协议的解决方案:在应用层协议中定义数据包的格式和解析规则,发送方和接收方按照协议规定的格式进行数据的打包和解析。这种方法需要在应用层进行开发和实现,但可以灵活地解决粘包问题,并且适用于不同的应用场景。
zunionstore:
zunionstore destination numkeys key [key ...] [WEIGHTS weigth [weigth ...] [AGGREGATE <SUM | MIN | MAX>]
编码方式:
有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数⼩于 zset-max-ziplist-entries 配置(默认 128 个), 同时每个元素的值都⼩于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会⽤ ziplist 来作 为有序集合的内部实现,ziplist 可以有效减少内存的使⽤。
- skiplist(跳表):当 ziplist 条件不满⾜时,有序集合会使⽤ skiplist 作为内部实现,因为此时 ziplist 的操作效率会下降。
应用场景:
排行榜系统:比如游戏天梯排行榜,每一局游戏结束,玩家分数就会改变,排名也要所知改变,zset中修改元素score之后会重新排序,又比如综合排序,根据流量排序,点赞排序,转发排序、评论排序的分数计算加权平均数。
对于member为4字节,score为8字节的有序集合来说,存储一亿个元素的空间大概是1.2GB,可见所占用的内存并不是很高。