SpringBoot + Redis的zset实现排行榜,实现分数相同按照时间顺序排序

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);
    }
}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值