目录

数据结构回顾

常用命令

添加

删除

获取元素个数

获取区间内元素个数

获取元素索引

获取区间内元素

获取所有元素

查看或增加分数

交集和并集

pop命令

使用场景

1.阅读量排行榜

2.销售量排行榜

3.手机号幸运抽奖

总结


本文介绍一下redis中zset的使用。首先说一下我本地的实验环境:

redis版本:6.0.7
springboot-redis版本:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.1.6.RELEASE</version>
</dependency>

里面使用到的spring-data-redis版本:2.1.9.RELEASE
里面使用到的lettuce连接池版本:5.1.7.RELEASE

数据结构回顾

之前在文章《redis灵魂拷问:聊一聊redis底层数据结构》中讲过redis的数据结构了,zset用到了3种数据结构,压缩列表、跳表,并且使用哈希表来保存value:score键值对。

当同时满足下面2个条件时会用到压缩列表,否则会用跳表:

  • 集合中元素都小于64字节
  • 集合中元素个数小于128个

当然这个也是可以配置的,在redis.conf文件中:

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

因为使用哈希表保存分数,所以zset查找分数的命令时间复杂度是o(1)。

跳表的数据结构我们再回顾一下,看下图;

跳表中的元素是按照分数有序排列的,每个元素都有指向后一个元素的指针,所以跳表可以很方便地进行范围查询,查找一个元素的复杂度是O(log(N)),从这个元素通过指针就可以找到后面的M个元素,所以复杂度是O(log(N)+M)。

常用命令

注意:下面的命令我用java代码来实现,注解中写了每个命令的原生命令和时间复杂度,使用的时候大家可以根据每个命令的复杂度来进行取舍。

添加

/*** ZADD* 时间复杂度 O(log(N)),n是sorted set中元素个数*/public void add(){//批量添加Set<DefaultTypedTuple> zset1 = new HashSet<>();zset1.add(new DefaultTypedTuple("v1",30.0));zset1.add(new DefaultTypedTuple("v2",20.0));zset1.add(new DefaultTypedTuple("v3",40.0));zset1.add(new DefaultTypedTuple("v4",10.0));//单个添加redisTemplate.opsForZSet().add("zset1", zset1);//加入后排序[v5, v4, v2, v1, v3]redisTemplate.opsForZSet().add("zset1", "v5", 5);}

删除

/*** ZREM* 复杂度O(M*log(N))* @return 删除元素个数*/public Long remove(){Set<DefaultTypedTuple> zset6 = new HashSet<>();zset6.add(new DefaultTypedTuple("v1",30.0));zset6.add(new DefaultTypedTuple("v2",20.0));zset6.add(new DefaultTypedTuple("v3",40.0));zset6.add(new DefaultTypedTuple("v4",10.0));zset6.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset6", zset6);//返回2return redisTemplate.opsForZSet().remove("zset6", "v4", "v5");}/*** ZREMRANGEBYRANK* 复杂度O(log(N)+M)* @return 删除元素个数*/public Long removeRange(){Set<DefaultTypedTuple> zset7 = new HashSet<>();zset7.add(new DefaultTypedTuple("v1",10.0));zset7.add(new DefaultTypedTuple("v2",20.0));zset7.add(new DefaultTypedTuple("v3",30.0));zset7.add(new DefaultTypedTuple("v4",40.0));zset7.add(new DefaultTypedTuple("v5",50.0));redisTemplate.opsForZSet().add("zset7", zset7);//返回3,此时zset中只剩[v1,v5]return redisTemplate.opsForZSet().removeRange("zset7", 1, 3);}/*** ZREMRANGEBYSCORE* 复杂度O(log(N)+M)* @return 删除元素个数*/public Long removeRangeByScore(){Set<DefaultTypedTuple> zset8 = new HashSet<>();zset8.add(new DefaultTypedTuple("v1",10.0));zset8.add(new DefaultTypedTuple("v2",20.0));zset8.add(new DefaultTypedTuple("v3",30.0));zset8.add(new DefaultTypedTuple("v4",40.0));zset8.add(new DefaultTypedTuple("v5",50.0));redisTemplate.opsForZSet().add("zset8", zset8);//返回3,此时zset中只剩[v4,v5]return redisTemplate.opsForZSet().removeRangeByScore("zset8", 10, 30);}

获取元素个数

/*** ZCARD* 返回元素个数* 复杂度 O(1)** @return*/public Long zCard(){Set<DefaultTypedTuple> zset1 = new HashSet<>();zset1.add(new DefaultTypedTuple("v1",30.0));zset1.add(new DefaultTypedTuple("v2",20.0));zset1.add(new DefaultTypedTuple("v3",40.0));zset1.add(new DefaultTypedTuple("v4",10.0));zset1.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset1", zset1);//[v5, v4, v2, v1, v3],返回5return redisTemplate.opsForZSet().zCard("zset1");}

获取区间内元素个数

/*** ZCOUNT* 时间复杂度O(log(N))* 返回分数是min~max之间的元素个数, 闭区间* @return*/public Long count(){Set<DefaultTypedTuple> zset1 = new HashSet<>();zset1.add(new DefaultTypedTuple("v1",30.0));zset1.add(new DefaultTypedTuple("v2",20.0));zset1.add(new DefaultTypedTuple("v3",40.0));zset1.add(new DefaultTypedTuple("v4",10.0));zset1.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset1", zset1);//["v1",30.0, "v2",20.0, "v3",40.0, "v4",10.0, "v5",5.0],返回2return redisTemplate.opsForZSet().count("zset1", 20.0, 30.0);}

获取元素索引

/*** ZRANK* 时间复杂度O(log(N))** @return 返回元素的正序索引位置*/public Long rank(){Set<DefaultTypedTuple> zset1 = new HashSet<>();zset1.add(new DefaultTypedTuple("v1",30.0));zset1.add(new DefaultTypedTuple("v2",20.0));zset1.add(new DefaultTypedTuple("v3",40.0));zset1.add(new DefaultTypedTuple("v4",10.0));zset1.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset1", zset1);//[v5, v4, v2, v1, v3],这里输出0return redisTemplate.opsForZSet().rank("zset1", "v5");}/*** ZREVRANK* 时间复杂度O(log(N))* 跟rank相反,返回元素逆序的位置** @return 返回元素的逆序索引位置*/public Long reverseRank(){Set<DefaultTypedTuple> zset1 = new HashSet<>();zset1.add(new DefaultTypedTuple("v1",30.0));zset1.add(new DefaultTypedTuple("v2",20.0));zset1.add(new DefaultTypedTuple("v3",40.0));zset1.add(new DefaultTypedTuple("v4",10.0));zset1.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset1", zset1);//[v5, v4, v2, v1, v3],这里输出4return redisTemplate.opsForZSet().reverseRank("zset1", "v5");}

获取区间内元素


/*** ZRANGE/ZREVRANGE命令* 复杂度O(log(N)+M),N是有序集合中的元素,M是返回的元素个数*注意:* 1.索引下标从0开始* 2.ZREVRANGE对应逆序输出,这里不给出示例** @return 返回指定索引范围内的元素,注意,这里是闭区间, 如果end传入-1,就是从start到最后一个元素*/public Set range(){Set<DefaultTypedTuple> zset1 = new HashSet<>();zset1.add(new DefaultTypedTuple("v1",30.0));zset1.add(new DefaultTypedTuple("v2",20.0));zset1.add(new DefaultTypedTuple("v3",40.0));zset1.add(new DefaultTypedTuple("v4",10.0));zset1.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset1", zset1);//输出[v4, v2, v1, v3],总共5个元素,索引从1开始return redisTemplate.opsForZSet().range("zset1", 1, -1);}/*** ZRANGEBYLE/ZRANGEBYLEX命令* 复杂度O(log(N)+M),N是有序集合中的元素,M是返回的元素个数*注意:* 1.这个命令用于元素分数相同的有序集合* 2.spring的RedisZSetCommands.Range不生效* 3.ZRANGEBYLEX对应逆序输出,当前客户端不支持这个命令** @return 返回指定索引范围内的元素*/public Set rangeByLex(){Set<DefaultTypedTuple> zset3 = new HashSet<>();zset3.add(new DefaultTypedTuple("a",0d));zset3.add(new DefaultTypedTuple("b",0d));zset3.add(new DefaultTypedTuple("c",0d));zset3.add(new DefaultTypedTuple("d",0d));zset3.add(new DefaultTypedTuple("e",0d));zset3.add(new DefaultTypedTuple("f",0d));zset3.add(new DefaultTypedTuple("g",0d));redisTemplate.opsForZSet().add("zset3", zset3);RedisZSetCommands.Range range = RedisZSetCommands.Range.range();//下面range赋值不生效,给lt赋值后返回空//range.lt("f");range.gt("c");RedisZSetCommands.Limit limit = new RedisZSetCommands.Limit();limit.offset(0);limit.count(5);//返回[a, b, c, d, e]return redisTemplate.opsForZSet().rangeByLex("zset3", range, limit);}/*** ZRANGEBYSCORE/ZRANGEBYSCORE命令* 复杂度O(log(N)+M),N是有序集合中的元素,M是返回的元素个数*注意:* 这个命令是闭区间* ZRANGEBYSCORE命令对应逆序输出** @return 返回指定索引范围内的元素*/public Set rangeByScore(){Set<DefaultTypedTuple> zset4 = new HashSet<>();zset4.add(new DefaultTypedTuple("v1",30.0));zset4.add(new DefaultTypedTuple("v2",20.0));zset4.add(new DefaultTypedTuple("v3",40.0));zset4.add(new DefaultTypedTuple("v4",10.0));zset4.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset4", zset4);//返回[v4, v2, v1]return redisTemplate.opsForZSet().rangeByScore("zset4", 10, 30);}

获取所有元素


/*** ZSCAN命令* 复杂度O(1)* 注意:*** @return 返回指定索引范围内的元素*/public void scan(){Set<DefaultTypedTuple> zset9 = new HashSet<>();for (int i = 1; i <= 1000; i ++){zset9.add(new DefaultTypedTuple("v" + i,i * 10.0));}redisTemplate.opsForZSet().add("zset9", zset9);ScanOptions.ScanOptionsBuilder scanOptionsBuilder = new ScanOptions.ScanOptionsBuilder();//这个count参数其实也不起作用,数据量小,比如20个,我们设置了10,会全部输出;数据量10000个,我们输入2000,也是输出1000个scanOptionsBuilder.count(2000);//这里使用v*竟然匹配不到scanOptionsBuilder.match("*");Cursor<ZSetOperations.TypedTuple> cursor = redisTemplate.opsForZSet().scan("zset9", scanOptionsBuilder.build());System.out.println("======================");cursor.forEachRemaining(r -> System.out.println(r.getValue() + ":" + r.getScore()));/*** 下面是输出1000行中的前5行:* v490:4900.0* v573:5730.0* v643:6430.0* v733:7330.0* v408:4080.0*/}

查看或增加分数


/*** ZINCRBY命令* 增加元素分数* 复杂度 O(log(N)),n是zset中元素个数** @return 增加分数后的元素值*/public Double incrementScore(){Set<DefaultTypedTuple> zset5 = new HashSet<>();zset5.add(new DefaultTypedTuple("v1",30.0));zset5.add(new DefaultTypedTuple("v2",20.0));zset5.add(new DefaultTypedTuple("v3",40.0));zset5.add(new DefaultTypedTuple("v4",10.0));zset5.add(new DefaultTypedTuple("v5",5.0));redisTemplate.opsForZSet().add("zset5", zset5);//返回15.0return redisTemplate.opsForZSet().incrementScore("zset5", "v5", 10d);}/*** ZSCAN命令* 复杂度O(1) * @return 查找元素的分数*/public Double score(){Set<DefaultTypedTuple> zset14 = new HashSet<>();zset14.add(new DefaultTypedTuple("v1",10.0));zset14.add(new DefaultTypedTuple("v2",20.0));zset14.add(new DefaultTypedTuple("v3",30.0));zset14.add(new DefaultTypedTuple("v4",40.0));zset14.add(new DefaultTypedTuple("v5",50.0));redisTemplate.opsForZSet().add("zset14", zset14);//返回30.0return redisTemplate.opsForZSet().score("zset14", "v3");}

交集和并集


/*** ZINTER/ZINTERSTORE** 复杂度O(N*K)+O(M*log(M)) ,N是元素少的zset的元素数量,K是2个zset的元素总数,M是返回结果*/public void intersectAndStore(){Set<DefaultTypedTuple> zset10 = new HashSet<>();zset10.add(new DefaultTypedTuple("v1",10.0));zset10.add(new DefaultTypedTuple("v3",30.0));zset10.add(new DefaultTypedTuple("v5",50.0));zset10.add(new DefaultTypedTuple("v6",60.0));redisTemplate.opsForZSet().add("zset10", zset10);Set<DefaultTypedTuple> zset11 = new HashSet<>();zset11.add(new DefaultTypedTuple("v1",10.0));zset11.add(new DefaultTypedTuple("v2",20.0));zset11.add(new DefaultTypedTuple("v3",30.0));zset11.add(new DefaultTypedTuple("v4",40.0));zset11.add(new DefaultTypedTuple("v5",50.0));redisTemplate.opsForZSet().add("zset11", zset11);redisTemplate.opsForZSet().intersectAndStore("zset10", "zset11", "zsetinter9and11");ScanOptions scanOptions = ScanOptions.NONE;Cursor<ZSetOperations.TypedTuple> cursor = redisTemplate.opsForZSet().scan("zsetinter9and11", scanOptions);System.out.println("======================");cursor.forEachRemaining(r -> System.out.println(r.getValue() + ":" + r.getScore()));/*** 输出结果如下* v1:20.0* v3:60.0* v5:100.0*/}/*** ZUNIONSTORE* * 复杂度:O(N)+O(M log(M)),其中N是2个zset的元素总数,M是返回的元素个数*/public void unionAndStore(){Set<DefaultTypedTuple> zset12 = new HashSet<>();zset12.add(new DefaultTypedTuple("v1",10.0));zset12.add(new DefaultTypedTuple("v2",20.0));zset12.add(new DefaultTypedTuple("v3",30.0));redisTemplate.opsForZSet().add("zset12", zset12);Set<DefaultTypedTuple> zset13 = new HashSet<>();zset13.add(new DefaultTypedTuple("v4",40.0));zset13.add(new DefaultTypedTuple("v5",50.0));zset13.add(new DefaultTypedTuple("v6",60.0));redisTemplate.opsForZSet().add("zset13", zset13);redisTemplate.opsForZSet().unionAndStore("zset12", "zset13", "zsetinter12and13");ScanOptions scanOptions = ScanOptions.NONE;Cursor<ZSetOperations.TypedTuple> cursor = redisTemplate.opsForZSet().scan("zsetinter12and13", scanOptions);System.out.println("======================");cursor.forEachRemaining(r -> System.out.println(r.getValue() + ":" + r.getScore()));/*** 输出结果如下* v1:10.0* v2:20.0* v3:30.0* v4:40.0* v5:50.0* v6:60.0*/}

pop命令

作为队列2个命令:ZPOPMAX/ZPOPMIN,让当前分数最高/最低的元素出队,复杂度O(log(N)*M) ,当前spring版本客户端不支持。

使用场景

zset保存了分数值,所以对于阅读量、点击量排行等场景可以很方便的使用。

1.阅读量排行榜

假如一个博客网站上有10篇文章,我们要统计今天阅读量排名前2位的文章,我们可以先初始化一个10篇文章的zset,代码如下:

Set<DefaultTypedTuple> articles = new HashSet<>();
articles.add(new DefaultTypedTuple("article1",0d));
articles.add(new DefaultTypedTuple("article2",0d));
articles.add(new DefaultTypedTuple("article3",0d));
articles.add(new DefaultTypedTuple("article4",0d));
articles.add(new DefaultTypedTuple("article5",0d));
articles.add(new DefaultTypedTuple("article6",0d));
articles.add(new DefaultTypedTuple("article7",0d));
articles.add(new DefaultTypedTuple("article8",0d));
articles.add(new DefaultTypedTuple("article9",0d));
articles.add(new DefaultTypedTuple("article10",0d));
redisTemplate.opsForZSet().add("articles", articles);

每当有1篇文章被阅读时,我们就把分数加1,比如第一篇:

redisTemplate.opsForZSet().incrementScore("articles", "article1", 1d);

日终时,我们找出排名前2位的文章:

redisTemplate.opsForZSet().range("articles", 0, 1);

2.销售量排行榜 

跟上面的场景类似,假如我们要找出销售量前2位的商品,我们也可以初始化一个商品zset,分数就是销售量,每次售出一件商品时分数值加1,最后range命令去除前2个商品。

3.手机号幸运抽奖

比如我们要对1万个手机号排名,我们可以把姓名作为key,把手机号score存入zset中,代码如下:

Set<DefaultTypedTuple> phones = new HashSet<>();
phones.add(new DefaultTypedTuple("张三",18605556899));
redisTemplate.opsForZSet().add("phones", phones);

我们可以随便找出一个幸运手机号,比如6000

redisTemplate.opsForZSet().range("articles", 5999, 5999);

总结

zset使用了压缩列表、跳表的数据结构,并且使用哈希表来保存value:score键值对。

range命令得益于底层使用了跳表,复杂度并不高,但是会随着返回元素的数量而增加。zscan命令复杂度很低,但是spring提供的api不友好,超过1000需要分页的时候,就不好用了。元素个数少于1000时使用zscan命令一次取出是最快的。

交集并集的复杂度很高,如果有bigkey的情况,会严重阻塞主线程,建议一般不要使用。可以把2个zset的元素取出来,在应用内存中进行交集并集运算,这样不会阻塞redis主线程。

由于api和版本限制,本文并没有列出zset的所有命令,大家可以查看官网:

https://redis.io/commands/zunionstore

往期文章回顾:

《redis灵魂拷问:为什么响应变慢了》

《redis灵魂拷问:聊一聊主从复制缓冲区》

《redis灵魂拷问:AOF文件可以保存RDB格式吗》

《redis灵魂拷问:聊一聊AOF日志重写》

《redis灵魂拷问:聊一聊redis底层数据结构》

《redis灵魂拷问:怎么搭建一个哨兵主从集群》

《springboot研究九:lettuce连接池很香,撸撸它的源代码》


欢迎关注个人公众号

redis灵魂拷问:聊一聊zset使用相关推荐

  1. redis灵魂拷问:如何使用stream实现消息队列

    redis在很早之前就支持消息队列了,使用的是PUB/SUB功能来实现的.PUB/SUB有一个缺点就是消息不能持久化,如果redis发生宕机,或者客户端发生网络断开,历史消息就丢失了. redis5. ...

  2. redis灵魂拷问:19图+11题带你面试通关

    又到了金三银四跳槽季,好多同学已经开始行动了.今天我来助力一把,送出这套redis面试题,助力大家通关. 1 redis为什么响应快 1.1数据保存在内存中 redis数据保存在内存中,读写操作只要访 ...

  3. Redis灵魂拷问:36题带你面试通关

    Redis是什么? Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库.与传统数据库不同的是,Redis 的数据是存在内存中的,所以 ...

  4. 泪目跳槽太不容易,蚂蚁金服三轮面试,四个小时灵魂拷问

    本人是双非院校科班研究生,Java开发3年工作经验,以下是最近的面试总结: 先说下我的面试准备经历,为了保证自己简历有较大一定的概率通过筛选,我在2018毕业后面试了多家公司,去了一家上海一家小公司一 ...

  5. 88道BAT Java面试题 助你跳槽BAT,轻松应对面试官的灵魂拷问

    88道BAT Java面试题 助你跳槽BAT,轻松应对面试官的灵魂拷问 前言: 备战金九银十逃脱不了面试官的灵魂拷问,笔者整理了88道Java面试,由于面试题太多文章没有包含答案,需要领取这些面试题答 ...

  6. 字节跳动,三轮面试,四个小时,灵魂拷问,结局我哭了但下次还敢...

    写在开篇 去年的秋招对于我来说,那是非常的不顺利,所以今年的春招其实我也没有抱太大的希望,令我惊讶的是第一家给我面试机会的公司竟然是宇宙条.一开始接到面试通知时,心情特别复杂,紧张又兴奋,字节跳动是出 ...

  7. 灵魂拷问:我们该如何写一个适合自己的状态管理库?

    作者|李骏(涅尘) 来源|尔达Erda公众号 ​ 引言 大家好,这里是 Erda 开源项目前端技术团队,今天聊一聊前端的状态管理. 说到状态管理库,想必前端同学随口都能说出好几个来,社区里的轮子一个接 ...

  8. 灵魂拷问:你看过Xgboost原文吗?

    Datawhale 作者:小雨姑娘,Datawhale成员 事情的源头是这样的,某日我分享了一篇阿里机器学习工程师面试失败经历,其中提到了我回答关于Xgboost的部分,评论区的老哥就开始了灵魂拷问: ...

  9. 旷视唐文斌:你到底给谁创造了什么样的价值?AI产品灵魂拷问

    落地,是2019年AI行业的共同话题,创造价值.降本增效,成为行业共识. 作为AI头雁公司.也即将成为AI创业第一股的旷视,又是怎样看待落地这个话题的? 而作为一位技术领袖,旷视联合创始人兼CTO唐文 ...

  10. 旷视唐文斌:你到底给谁创造了什么样的价值?这是AI产品的灵魂拷问丨MEET2020...

    郭一璞 整理自 MEET2020智能未来大会  量子位 报道 | 公众号 QbitAI 落地,是2019年AI行业的共同话题,创造价值.降本增效,成为行业共识. 作为AI头雁公司.也即将成为AI创业第 ...

最新文章

  1. 前端开发之retina屏幕
  2. 网络流24题 飞行员配对方案问题
  3. web自动化之鼠标事件
  4. python如何打开txt文件、并算词频_python TF-IDF词频算法实现文本关键词提取代码...
  5. 【Linux】gdb常用的调试命令
  6. 用 Python 分析今年考研形势
  7. 简洁jQuery滑动门插件
  8. linux下 添加一个新账户tom,linux 账户管理命令 useradd、groupadd使用方法
  9. 数组去重和两个数组求交集
  10. DSP编程 单片机编程 开关电源设计
  11. 各种电子面单-Api接口(顺丰、快递鸟、菜鸟)
  12. Java基础(一)之公共基础
  13. 多个图片合成PDF文件
  14. ionic3硬件检测、请求权限插件 Diagnostic 的用法
  15. 【转】常用邮箱的 IMAP/POP3/SMTP 设置
  16. 最详细的VI编辑器指南
  17. Python自动化办公:word文件操作教程
  18. 麦克风声音太小别人听不到怎么办
  19. 2022国自然中标至少1篇1区代表作?没中接下来怎么办?
  20. 战地之王服务器维护启动失败,《战地之王》战地之王韩服官方各种问题攻略

热门文章

  1. 第三章(第一部分) 月夜猫の魅 友谊的决裂
  2. flutter pdf 插件使用
  3. 高等数学(第七版)同济大学 总习题七 (前4题)个人解答
  4. html文本框的margin,HTML DOM Style marginTop 属性 | 菜鸟教程
  5. View margin/marginTop 注意点
  6. 【debug】Support for password authentication was removed on August 13, 2021.解决
  7. windows10专业版安装应用商店方法
  8. 红米Note4X开发者选项
  9. 安装docker遇到的坑
  10. 网络安全先进技术与应用发展系列报告 用户实体行为分析技术(UEBA)