1、向不同榜单中添加数据
/**
* 更新zset的分数
*
* @param type 榜单类型 1-小时榜 2-日榜 3-周榜 4-月榜 5-年榜
* @param key key
* @param userId 用户id
* @param score 增加的分数
*/
public static void updateScore(int type, String key, long userId, double score) {
double timestamp = 1 - (System.currentTimeMillis() / 1e13);
//查询当前用户在该榜单中的分数
Double currentScore = redisTemplate.opsForZSet().score(key, userId);
if (ObjectUtil.isNull(currentScore)) {
boolean hasKey = redisTemplate.hasKey(key);
//如果不存在排行榜中,要添加到排行榜
redisTemplate.opsForZSet().add(key, userId, score + timestamp);
if(!hasKey){
//如果之前没有这个榜单,需要给当前榜单设置过期时间
LocalDateTime startTime = LocalDateTime.now();
LocalDateTime endTime = null;
switch (type) {
case 2:
//2-日榜
endTime = startTime.withHour(23).withMinute(59).withSecond(59);
break;
case 3:
//3-周榜
endTime = startTime.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).withHour(23).withMinute(59).withSecond(59);
break;
case 4:
//4-月榜
endTime = startTime.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59);
break;
case 5:
//5-年榜
endTime = startTime.with(TemporalAdjusters.lastDayOfYear()).withHour(23).withMinute(59).withSecond(59);
break;
default:
//1-小时榜
endTime = LocalDateTime.now().withMinute(59).withSecond(59);
}
redisTemplate.expire(key, Duration.between(startTime, endTime).getSeconds(), TimeUnit.SECONDS);
}
}else {
//拿到原有的实际分数
Long originScore = getScoreAndRankByUserId(key, userId).getLong("score");
//更新到排行榜
redisTemplate.opsForZSet().add(key, userId, originScore + score + timestamp);
}
}
2、获取zset排行榜前 N 名
/**
* 获取zset排行榜前 N 名。
*
* @param n 排名数量
* @return 排行榜数据
*/
public static Set<ZSetOperations.TypedTuple<Object>> getTopN(String key, int n) {
Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, n - 1);
return typedTuples;
}
3、获取zset某个用户的分数和名次
/**
* 获取zset某个用户的分数和名次
*
* @param key key
* @param userId 用户id
* @return
*/
public static JSONObject getScoreAndRankByUserId(String key, long userId) {
JSONObject result = JSONUtil.createObj();
Double score = redisTemplate.opsForZSet().score(key, userId);
if (ObjectUtil.isNull(score)) {
return result.putOpt("score", 0).putOpt("rank", 0);
}
// 如果玩家已经在排行榜中
double rank = redisTemplate.opsForZSet().reverseRank(key, userId) + 1;
return result.putOpt("score", Convert.toLong(score)).putOpt("rank", rank);
}
4、根据名次获取zset某个用户的分数
/**
* 根据名次获取zset某个用户的分数
*
* @param key key
* @param rank 名次
* @return
*/
public static Long getScoreByRank(String key, int rank) {
// 获取指定名次的用户ID
Object userId = redisTemplate.opsForZSet().reverseRange(key, rank, rank).stream().findFirst().orElse(null);
if (userId == null) {
// 如果没有找到对应的用户
return 0l;
}
// 获取用户的分数
return getScoreAndRankByUserId(key, Convert.toLong(userId)).getLong("score");
}
在Redis中,使用zset可以实现排行榜的功能这个大家都知道。
zset可以实现,将每个用户的得分作为zset中元素的score,将用户ID作为元素的value。使用zset提供的排序功能,可以按照分数从高到低排序,**但是如果分数相同,按照默认的排序规则会按照value值排序**,而不是按照时间顺序排序。
为了实现分数相同按照时间顺序排序,**我们可以将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳**,如下所示:
> score = 分数 + 时间戳/1e13
假设现在的时间戳是1680417299000,除以1e13得到0.1680417299000,再加上一个固定的分数(比如10),那么最终的分数就是10.1680417299000,可以将它作为zset中某个成员的分数,用来排序。
这么做了之后,假如有四个数字:
10.1680417299000、10.1680417299011、11.1680417299000、11.1680417299011
他们按照倒序拍完顺序之后,会是:
11.1680417299011>11.1680417299000>10.1680417299011>10.1680417299000
实现了分数倒序排列,分数相同时间戳大的排在了前面,这和我们的需求相反了,所以,就需要在做一次转换。
> score = 分数 + 1-时间戳/1e13
> 因为时间戳是这种形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13个0,所以用时间戳/1e13就能得到一个小数
这样可以保证分数相同时,按照时间戳从小到大排序,即先得分的先被排在前面。
代码实现如下:
```
import redis.clients.jedis.Jedis;
/**
*@author Hollis
**/
public class RedisZsetDemo {
private static final String ZSET_KEY = "my_zset";
public static void addMember(String member,Int score, long timestamp, Jedis jedis) {
double final_score = score + 1 - timestamp / 1e13;
jedis.zadd(ZSET_KEY, final_score, member);
}
}