概述
有序集合和集合类似,只是说它是有序的,和无序集合的主要区别在于每一个元素除了值之外,它还会多一个分数。
分数是一个浮点数,在 Java 中是使用双精度表示的,根据分数, Redis 就可以支持对分数从小到大或者从大到小的排序
和无序集合一样,对于每一个元素都是唯一的 ,但是对于不同元素而言,它的分数可以一样
元素也是 String 数据类型,也是一种基于 hash 的存储结构。
集合是通过哈希表实现的,所以添加、删除、 查找的复杂度都是 0(1)
集合中最大的成员数为 2的32次方减 1 ( 40 多亿个成员)
有序集合的数据结构
有序集合是依赖 key 标示它是属于哪个集合,依赖分数进行排序,所以值和分数是必须的,而实际上不仅可以对分数进行排序,在满足一定的条件下,也可以对值进行排序 。
Redis 有序集合的部分命令
官网: https://redis.io/commands#sorted_set
有序集合和无序集合的命令是接近的,只是在这些命令的基础上,会增加对于排序的操作,这些是我们在使用的时候需要注意的细节.
有些时候 Redis 借助数据区间的表示方法来表示包含或者不包含,比如在数学的区间表示中[2,5 ]表示包含 2,但是不包含 5 的 区间。
在对有序集合、下标、区间的表示方法进行操作的时候,需要十分小心命令,注意它是操作分数还是值,稍有不慎就会出现问题。
# 为了测试的数据干净,删除当前db的数据 127.0.0.1:6379> FLUSHDB OK 127.0.0.1:6379> 127.0.0.1:6379> # zadd key score1 value1 [score2 value2 …] 向有序集合zset1 ,增加9个成员 127.0.0.1:6379> ZADD zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9 (integer) 9 # zadd key score1 value1 [score2 value2 …] 向有序集合zset2 ,增加9个成员 127.0.0.1:6379> ZADD zset2 1 y1 2 x2 3 y3 4 x4 5 y5 6 x6 7 y7 8 x8 9 y9 (integer) 9 # zcard key 获取有序集合zset1的成员数 127.0.0.1:6379> ZCARD zset1 (integer) 9 # zcount key min max 根据分数返回对应的成员列表 127.0.0.1:6379> ZCOUNT zset1 1 4 (integer) 4 # zinterstore desKey nurnkeys key1 [key2 key3 …] 求多个有序集合的交集,并且将结果保存到 des Key 中 127.0.0.1:6379> ZINTERSTORE des_key 2 zset1 zset2 (integer) 4 # zlexcount key min max 求有序集合 zset1 成员值在 min 和 max 的范围 [ 表示包含该值,( 表示不包含该值 127.0.0.1:6379> ZLEXCOUNT zset1 (x1 [x5 (integer) 4 # zrange key start stop [withscores] 按照分值的大小(从小到大)返回成员,加入 start 和 stop 参数可以截取某一段返回.如果输入可选项 withscores,则连同分数一起返回 127.0.0.1:6379> ZRANGE zset1 1 5 withscores 1) "x2" 2) "2" 3) "x3" 4) "3" 5) "x4" 6) "4" 7) "x5" 8) "5" 9) "x6" 10) "6" # zrank key member 按从小到大求有序集合的排行 127.0.0.1:6379> ZRANK zset1 x5 (integer) 4 # zrangebylex key min max [limit offset count]根据值的大小,从小到大排序 [表示包含该值 (表示不包含该值 127.0.0.1:6379> ZRANGEBYLEX zset1 (x1 [x6 1) "x2" 2) "x3" 3) "x4" 4) "x5" 5) "x6" 127.0.0.1:6379> # zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围 127.0.0.1:6379> ZRANGEBYSCORE zset1 5 7 1) "x5" 2) "x6" 3) "x7" # zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围 127.0.0.1:6379> ZRANGEBYSCORE zset1 2 7 withscores limit 1 5 1) "x3" 2) "3" 3) "x4" 4) "4" 5) "x5" 6) "5" 7) "x6" 8) "6" 9) "x7" 10) "7" # zrevrange key start stop [withscores] 从大到小的按分数排序 127.0.0.1:6379> ZREVRANGE zset1 1 5 1) "x8" 2) "x7" 3) "x6" 4) "x5" 5) "x4" # zrevrangebyscore key max min [withscores] 从大到小的按分数排序 127.0.0.1:6379> ZREVRANGEBYSCORE zset2 5 2 withscores 1) "y5" 2) "5" 3) "x4" 4) "4" 5) "y3" 6) "3" 7) "x2" 8) "2" # zrevrank key member 按从大到小的顺序,求元素的排行 127.0.0.1:6379> ZREVRANK zset1 x4 (integer) 5 # zscore key member 返回成员的分数值 127.0.0.1:6379> ZSCORE zset1 x5 "5" # zunionstore desKey numKeys key1 [key2 key3 key4 …] 求多个有序集合的并集,其中 numKeys是有序,集合的个数 127.0.0.1:6379> ZUNIONSTORE des_key 2 zset1 zset2 (integer) 14 # zincrby key increment member 给有序集合成员值为 member 的分数增加 increment 127.0.0.1:6379> ZINCRBY zset1 5 x9 "14" # zremrangebyscore key start stop 根据分数区间进行删除 127.0.0.1:6379> ZREMRANGEBYSCORE zset1 3 2 (integer) 0 # zremrangebyscore key start stop 根据分数区间进行删除 127.0.0.1:6379> ZREMRANGEBYSCORE zset1 3 5 (integer) 3 # zremrangebyrank key start stop 按照分数排行从小到大的排序删除,从0开始计算 127.0.0.1:6379> ZREMRANGEBYRANK zset1 1 3 (integer) 3 # zremrangebylex key min max 按照值的分布进行删除 127.0.0.1:6379> ZREMRANGEBYLEX zset2 [y1 [y5 (integer) 6 127.0.0.1:6379> 127.0.0.1:6379> ZCARD zset1 (integer) 3 127.0.0.1:6379> ZCARD zset2 (integer) 3 127.0.0.1:6379> 127.0.0.1:6379> ZRANGE zset1 0 999 1) "x1" 2) "x8" 3) "x9" 127.0.0.1:6379> ZRANGE zset2 0 999 1) "y7" 2) "x8" 3) "y9" 127.0.0.1:6379>
spring-data-redis 对有序集合的封装
在 Spring 中使用 Redis 的有序集合,需要注意的是 Spring 对 Redis 有序集合的元素的值和分数的范围( Range )和限制( Limit)进行了封装。
我们来看下Spring是如何封装的。 先介绍一个主要的接口一一TypedTuple,它不是一个普通的接口,而一个内部接口.
org.springframework . data. redis.core .ZSetOperations 接口的内部接口,它定义了两个方
法
- getValue()是获取值, getScore()是获取分数,但是它只是一个接口,而不是一个实现类
- spring-data-red is 提供了 一个默认的实现类一DefaultTypedTuple
在默认的情况下 Spring 就会把带有分数的有序集合的值和分数封装到这个类中 ,这样就可以通过这个类对象读取对应的值和分数了 .
Spring 不仅对有序集合元素封装,而且对范围也进行了封装,方便使用.它是使用接口 org.springframe.work.data.redis.connection.RedisZSetCommands 下的内部类 Range 进行封装的,它有一个静态的 range()方法,使用它就可以生成一个 Range 对象了,只是要清楚 Range对象的几个方法才行.
// 设置大于等于 min public Range gte(Object min) // 设置大于 min public Range gt(Object min) // 设置小于等于 max public Range lte(Object max) // 设置小于 max public Range lt(Object max)
这 4 个方法就是最常用的范围方法.
下面看一下limit,它是接口 org.springframework.data.redis.connection.RedisZSetCommands 下的内部类,它是一个简单的 POJO,它存在两个属性
通过属性的名称很容易知道:offset 代表从第几个开始截取,而 count 代表限制返回的总数量。
使用 Spring 操作有序集合
刚才讨论了 spring-data-redis 项目对有序集合的封装,在此基础上这里的演示示例代码在测试代码前,要把 RedisTemplate 的 keySerializer 和 valueSerializer属性都修改为字符串序列化器 StringRedisSerializer
配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:redis/redis.properties" /> <!--2,注意新版本2.3以后,JedisPoolConfig的property name,不是maxActive而是maxTotal,而且没有maxWait属性,建议看一下Jedis源码或百度。 --> <!-- redis连接池配置 --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <!--最大空闲数 --> <property name="maxIdle" value="${redis.maxIdle}" /> <!--连接池的最大数据库连接数 --> <property name="maxTotal" value="${redis.maxTotal}" /> <!--最大建立连接等待时间 --> <property name="maxWaitMillis" value="${redis.maxWaitMillis}" /> <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟) --> <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" /> <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 --> <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" /> <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 --> <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" /> <property name="testOnBorrow" value="true"></property> <property name="testOnReturn" value="true"></property> <property name="testWhileIdle" value="true"></property> </bean> <!--redis连接工厂 --> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy"> <property name="poolConfig" ref="jedisPoolConfig"></property> <!--IP地址 --> <property name="hostName" value="${redis.host.ip}"></property> <!--端口号 --> <property name="port" value="${redis.port}"></property> <!--如果Redis设置有密码 --> <property name="password" value="${redis.password}" /> <!--客户端超时时间单位是毫秒 --> <property name="timeout" value="${redis.timeout}"></property> <property name="usePool" value="true" /> <!--<property name="database" value="0" /> --> </bean> <!-- 键值序列化器设置为String 类型 --> <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <!-- redis template definition --> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="jedisConnectionFactory" p:keySerializer-ref="stringRedisSerializer" p:defaultSerializer-ref="stringRedisSerializer" p:valueSerializer-ref="stringRedisSerializer"> </bean> </beans>
package com.artisan.redis.baseStructure.zset; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.redis.connection.RedisZSetCommands.Limit; import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.core.DefaultTypedTuple; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; public class SpringRedisZSetDemo { private static final String ZSET1 = "zset1"; private static final String ZSET2 = "zset2"; @SuppressWarnings({ "unchecked", "rawtypes" }) public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-zset.xml"); RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate"); // 方便测试,清空数据 redisTemplate.delete(ZSET1); redisTemplate.delete(ZSET2); // Spring 提供接口 TypedTuple 操作有序集合 Set<TypedTuple> set1 = new HashSet<ZSetOperations.TypedTuple>(); Set<TypedTuple> set2 = new HashSet<ZSetOperations.TypedTuple>(); // 构造数据 // 127.0.0.1:6379> // # zadd key score1 value1 [score2 value2 …] 向有序集合zset1 ,增加9个成员 // >ZADD zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9 // (integer) 9 // # zadd key score1 value1 [score2 value2 …] 向有序集合zset2 ,增加9个成员 // > ZADD zset2 1 y1 2 x2 3 y3 4 x4 5 y5 6 x6 7 y7 8 x8 9 y9 // (integer) 9 int j = 9; String value1, value2 = null; double score1, score2 = 0.0; for (int i = 1; i <= 9; i++) { // 计算分数和值 score1 = Double.valueOf(i); value1 = "x" + i; if (j > 0) { score2 = Double.valueOf(j); value2 = j % 2 == 1 ? "y" + j : "x" + j; j--; } // 使用 Spring提供的默认 TypedTuple-DefaultTypedTuple TypedTuple typedTuplel = new DefaultTypedTuple(value1,score1); set1.add(typedTuplel); TypedTuple typedTuple2 = new DefaultTypedTuple(value2,score2); set2.add(typedTuple2); } // 写入redis redisTemplate.opsForZSet().add(ZSET1, set1); redisTemplate.opsForZSet().add(ZSET2, set2); // 统计总数 Long size = redisTemplate.opsForZSet().size(ZSET1); System.out.println(ZSET1 + "的size为" + size); // 计分数为 score ,那么下面的方法就是求 3<=score<=6 的元素 Long count = redisTemplate.opsForZSet().count(ZSET1, 3, 6); System.out.println(ZSET1 + "中3<=score<=6 的count为" + count); // 从下标一开始截驭 5 个元素,但是不返回分数 , 每一个元素是 String Set set = redisTemplate.opsForZSet().range(ZSET1, 1, 5); printSet(set); // 截取集合所有元素,并且对集合按分数排序,并返回分数 , 每一个元素是 TypedTuple Set<TypedTuple> typedTuples = redisTemplate.opsForZSet().rangeWithScores(ZSET1, 0, -1); printTypedTuple(typedTuples); // 将 zsetl 和 zset2 两个集合的交集放入集合 inter_zset size = redisTemplate.opsForZSet().intersectAndStore(ZSET1, ZSET2, "inter_zset"); System.out.println("inter_zset size:" + size); // 查看交集inter_zset中的数据 set = redisTemplate.opsForZSet().range("inter_zset", 0, redisTemplate.opsForZSet().size("inter_zset")); printSet(set); // 区间 Range range = Range.range(); range.lt("x8");// 小于 range.gt("x1");// 大于 set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range); printSet(set); range.lte("x8");// 小于等于 range.gte("x1");// 大于等于 set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range); printSet(set); // 限制返回个数 Limit limit = Limit.limit(); // 限制返回个数 limit.count(4); // 限制从第2个开始截取 limit.offset(2); // 求区间内的元素,并限制返回 4 条 set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range, limit); printSet(set); // 求排行,排名第 1 返回 0 ,第 2 返回 1 Long rank = redisTemplate.opsForZSet().rank(ZSET1, "x4"); System.out.println("rank=" + rank); // 删除元素 , 返回删除个数 size = redisTemplate.opsForZSet().remove(ZSET1, "x5", "x6"); System.out.println("remove " + size + " 个元素"); // 按照排行删除从 0 开始算起,这里将删除第排名第 2 和第 3 的元素 size = redisTemplate.opsForZSet().removeRange(ZSET1, 1, 2); System.out.println("removeRange " + size + " 个元素"); // 获取所有集合的元索和分数 , 以 -1 代表全部元素 typedTuples = redisTemplate.opsForZSet().rangeWithScores(ZSET1, 0, -1); printTypedTuple(typedTuples); // 删除指定的元素 size = redisTemplate.opsForZSet().remove(ZSET2, "y3", "y5"); System.out.println("remove " + size + " 个元素"); // 给集合中的一个元素的分数加上 11 Double double1 = redisTemplate.opsForZSet().incrementScore(ZSET2, "y1", 11); printTypedTuple(redisTemplate.opsForZSet().rangeWithScores(ZSET2, 0, redisTemplate.opsForZSet().size(ZSET2))); // 从大到小排列 typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(ZSET2, 0, 99); printTypedTuple(typedTuples); } @SuppressWarnings("rawtypes") public static void printTypedTuple(Set<TypedTuple> typedTuples) { if (typedTuples != null && typedTuples.isEmpty()) { return; } Iterator<TypedTuple> iterator = typedTuples.iterator(); while (iterator.hasNext()) { TypedTuple typedTuple = iterator.next(); System.out.println("{value =" + typedTuple.getValue() + ", score=" + typedTuple.getScore() + "}"); } System.out.println("----------------------"); } @SuppressWarnings("rawtypes") public static void printSet(Set set) { if (set != null && set.isEmpty()) { return; } Iterator iterator = set.iterator(); while (iterator.hasNext()) { Object val = iterator.next(); System.out.println(val + "\t"); } System.out.println("----------------------"); } }
输出
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Wed Sep 26 23:26:54 CST 2018]; root of context hierarchy INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-zset.xml] zset1的size为9 zset1中3<=score<=6 的count为4 x2 x3 x4 x5 x6 ---------------------- {value =x1, score=1.0} {value =x2, score=2.0} {value =x3, score=3.0} {value =x4, score=4.0} {value =x5, score=5.0} {value =x6, score=6.0} {value =x7, score=7.0} {value =x8, score=8.0} {value =x9, score=9.0} ---------------------- inter_zset size:4 x2 x4 x6 x8 ---------------------- x2 x3 x4 x5 x6 x7 ---------------------- x1 x2 x3 x4 x5 x6 x7 x8 ---------------------- x3 x4 x5 x6 ---------------------- rank=3 remove 2 个元素 removeRange 2 个元素 {value =x1, score=1.0} {value =x4, score=4.0} {value =x7, score=7.0} {value =x8, score=8.0} {value =x9, score=9.0} ---------------------- remove 2 个元素 {value =x2, score=2.0} {value =x4, score=4.0} {value =x6, score=6.0} {value =y7, score=7.0} {value =x8, score=8.0} {value =y9, score=9.0} {value =y1, score=12.0} ---------------------- {value =y1, score=12.0} {value =y9, score=9.0} {value =x8, score=8.0} {value =y7, score=7.0} {value =x6, score=6.0} {value =x4, score=4.0} {value =x2, score=2.0} ----------------------
注意
使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。








