zset中的score_redis灵魂拷问:聊一聊zset使用

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

redis版本:6.0.7

springboot-redis版本:

<dependency>    <groupId>org.springframework.bootgroupId>    <artifactId>spring-boot-starter-data-redisartifactId>    <version>2.1.6.RELEASEversion>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 128zset-max-ziplist-value 64

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

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

0d5b47a6cc7fa29fab2cb3c686d367f6.png

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

常用命令

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

添加

/**     * ZADD     * 时间复杂度 O(log(N)),n是sorted set中元素个数     */    public void add(){        //批量添加        Set 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 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);        //返回2        return redisTemplate.opsForZSet().remove("zset6", "v4", "v5");    }    /**     * ZREMRANGEBYRANK     * 复杂度O(log(N)+M)     * @return 删除元素个数     */    public Long removeRange(){        Set 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 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 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],返回5        return redisTemplate.opsForZSet().zCard("zset1");    }

获取区间内元素个数

/**     * ZCOUNT     * 时间复杂度O(log(N))     * 返回分数是min~max之间的元素个数, 闭区间     * @return     */    public Long count(){        Set 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],返回2        return redisTemplate.opsForZSet().count("zset1", 20.0, 30.0);    }

获取元素索引

/**     * ZRANK     * 时间复杂度O(log(N))     *     * @return 返回元素的正序索引位置     */    public Long rank(){        Set 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],这里输出0        return redisTemplate.opsForZSet().rank("zset1", "v5");    }    /**     * ZREVRANK     * 时间复杂度O(log(N))     * 跟rank相反,返回元素逆序的位置     *     * @return 返回元素的逆序索引位置     */    public Long reverseRank(){        Set 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],这里输出4        return 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 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 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 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 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 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 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.0        return redisTemplate.opsForZSet().incrementScore("zset5", "v5", 10d);    }        /**     * ZSCAN命令     * 复杂度O(1)      * @return 查找元素的分数     */    public Double score(){        Set 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.0        return redisTemplate.opsForZSet().score("zset14", "v3");    }

交集和并集

/**     * ZINTER/ZINTERSTORE     *     * 复杂度O(N*K)+O(M*log(M)) ,N是元素少的zset的元素数量,K是2个zset的元素总数,M是返回结果     */    public void intersectAndStore(){        Set 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 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 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 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 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 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保存了分数值,所以对于阅读量、点击量排行等场景可以很方便的使用。

阅读量排行榜

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

Set 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位的商品,我们也可以初始化一个商品zset,分数就是销售量,每次售出一件商品时分数值加1,最后range命令去除前2个商品。

手机号幸运抽奖

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

Set 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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值