前言
redis在业务开发中会被频繁使用,zset是其中一种特殊用法,zset具排行榜的天然特性,我前几个月在一次开发中使用到了zset,就是因为涉及到要实现一个排行榜,那是我第一次用到zset,虽然之前都看过redis几种数据类型的数据结构及其使用方法,但是真正用起来的时候,还是有一些细节的东西要处理的。
产品需求
用户在某条赛道上的跑步数据排行榜,以用户完成赛道的最佳成绩(最快圈速)为排序,做出排行榜。
当时产品给的需求是排行榜要实时变更,之前我们app里面也有其他业务的排行榜,但都是月排行榜、周排行榜,实现方法就是先捞mysql的数据,然后放到redis中,过期时间就是一周或者一天(昨日最佳)。但是这一次产品要求的是要实时变更的,这时候如果实时取mysql,再以某个过期时间存储到redis,就不合适了,因为一放完redis,某个用户完赛后,如果要使排行榜数据是实时的,就得马上reset一下redis中的数据,同时也得操作一次数据库,这样就显得有点累赘了,过期时间也没有存在的必要了。退一万步讲,从redis拿出来的数据也不是有序的,我们还得重排序一次。
技术背景
作为公司的新手菜鸟自然不敢自己做技术选型,我当时接到这个需求就想到了zset可能是可以实现这个功能的,后来跟我们的技术老大说了我的想法,她也支持使用zset,于是乎,开了技术评审后,我使用了zset来实现。
zset简单介绍
zset 和 set 很像,都是字符串的集合,都不允许重复的成员出现在一个 set 中。
他们的区别在于有序集合中每一个成员都有一个分数(score)与之关联,redis 正是通过分数来对集合里的成员进行从小到大的排序。尽管有序集合中的成员必须是惟一的,但是分数(score)却可以重复。
工具类
/**
* 新增或者更新数据
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param member 排行榜对象
* @param score 排行依据(值)
* 如果在key中,不存在该member,则新增,如果存在,则更新
*/
public void addOrUpdate(String sortedSetKey, int member, double score) {
redisTemplate.opsForZSet().add(sortedSetKey, member, score);
}
/**
* 获取排行榜指定范围的内容
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param startIndex 起始值(从0开始)
* @param endIndex 结束值
* @return
*/
public Set<Object> reverseRange(String sortedSetKey, int startIndex, int endIndex) {
return redisTemplate.opsForZSet().reverseRange(sortedSetKey, startIndex, endIndex);
}
/**
* 获取排行榜指定范围的内容,反序
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param startIndex 起始值(从0开始)
* @param endIndex 结束值
* @return
*/
public Set<Object> range(String sortedSetKey, int startIndex, int endIndex) {
return redisTemplate.opsForZSet().range(sortedSetKey, startIndex, endIndex);
}
/**
* 获取排行榜前N的数据
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param number
* @return
*/
public Set<Object> top(String sortedSetKey, long number) {
return redisTemplate.opsForZSet().reverseRange(sortedSetKey, 0L, number);
}
/**
* 获取排行榜start到end的value以及他的score
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param start 从0开始算起
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> rangeWithScore(String sortedSetKey, int start, int end) {
return redisTemplate.opsForZSet().rangeWithScores(sortedSetKey, start, end);
}
/**
* 获取排名
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param member 排行榜对象
* @return
*/
public Long getRankNum(String sortedSetKey, int member) {
return redisTemplate.opsForZSet().rank(sortedSetKey, member);
}
/**
* 获取score
*
* @param sortedSetKey
* @param member
* @return
*/
public Double getScore(String sortedSetKey, Object member) {
return redisTemplate.opsForZSet().score(sortedSetKey, member);
}
/**
* 批量新增/更新数据
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param tuples Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
* long start = System.currentTimeMillis();
* for (int i = 0; i < 100000; i++) {
* DefaultTypedTuple<String> tuple = new DefaultTypedTuple<>("张三" + i, 1D + i);
* tuples.add(tuple);
* }
* 注:这个方法,我没测过
*/
public Long addOrUpdateSet(String sortedSetKey, Set<ZSetOperations.TypedTuple<Object>> tuples) {
if (null != tuples && !tuples.isEmpty()) {
return redisTemplate.opsForZSet().add(sortedSetKey, tuples);
}
return 0L;
}
/**
* 删除数据
*
* @param sortedSetKey (用于区分不同的排行榜)
* @param member 排行榜对象
* @return
*/
public Long remove(String sortedSetKey, Object member) {
return redisTemplate.opsForZSet().remove(sortedSetKey, member);
}
/**
* 查询,范围内的数据
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param startIndex 起始值(从0开始)
* @param endIndex 结束值
* @return
*/
public String getRangeData(String sortedSetKey, Long startIndex, Long endIndex) {
Set<ZSetOperations.TypedTuple<Object>> rangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(sortedSetKey, startIndex, endIndex);
return JSON.toJSONString(rangeWithScores);
}
/**
* 增量添加score
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @param member 排行榜对象
* @param addScore 添加的分数
*/
public void incrementScore(String sortedSetKey, String member, double addScore) {
redisTemplate.opsForZSet().incrementScore(sortedSetKey, member, addScore);
}
/**
* 获取当前redis中排名的数据的总个数
*
* @param sortedSetKey
* @return
*/
public Long getSize(String sortedSetKey) {
return redisTemplate.opsForZSet().size(sortedSetKey);
}
/**
* 获取排行榜所有的数据
*
* @param sortedSetKey key(用于区分不同的排行榜)
* @return 注意:数量多,容易卡死,不建议使用
*/
public Set<Object> getAll(String sortedSetKey) {
Long size = redisTemplate.opsForZSet().size(sortedSetKey);
if (size != null) {
return redisTemplate.opsForZSet().reverseRange(sortedSetKey, 0L, size);
}
return Collections.emptySet();
}
实现过程
相信看了上面我列出来的文档里面的几个api和上面的工具类,大家对用法也清楚了,其实就是取的时候用rangebyscore,存的时候用zadd,zadd之后zset会自己在内部自动排序,拿出来的结果就是排好序的,不过有个需要注意的地方就是,当score分数一样时,zset会按照unicode编码的自然顺序来排序(这个我就被坑过),以我之前那个需求为例子的话,当score分数一样,要以用户上传跑步数据的时间作升序来排的,就是说如果分数一样,就看谁先完成,谁先完成就排前面,我当时是用分数拼接时间戳的方法。
double timeRank = Double.parseDouble(vo.getFastSpeed() + "." + vo.getLastUploadTime().getTime());
取出来的时候可以直接用Int来接收,就直接去掉了后面的小数点了。如果业务需求是时间越大排越前面,就需要用一个最大时间戳99999999减去业务时间戳,再拼接。
深入了解
如果想了解下zset具体的数据结构,可以看下这篇文章https://www.jianshu.com/p/fb7547369655