背景

618活动需要设计一个用户排行榜的功能,考虑到redis有SortedSet数据结构(由跳表 + 字典实现),比较适合实现排行榜。

遇坑

需求的场景是,如果两个用户的订单数量相同,那么先到达该订单数量的用户排在前面
一开始我先考虑的问题是:
在SortedSet中,如果score相同,是怎么排序的呢?
后来知道,如果score相同是按照member的字典顺序,即a排在b的前面,1排在2的前面。

那我是不是可以把时间戳加到SortedSet的member中,如 时间戳_用户id。为了让时间戳小的排在前面,我们可以把memebr的设计改成 (Long.MAX_VALUE - 时间戳)_用户id。
这样以后,感觉解决问题了。当时间戳越小,对应的member字典序就会越大,排在前面

殊不知,我们知道,SortedSet中的元素是唯一的(即SortSet中的member是唯一的),而我们的设计中,时间戳是会随着订单变化的。如果往SortedSet新增一个元素,会出现重复的两个。
如图:
在这里插入图片描述
同一个用户的数据会出现两条。
这里有两个问题:
1、在修改数据的时候,需要将原来的member删除,再新增
2、因为时间戳是会变化的,需要记录uid与member的映射关系

对于redis的操作次数都是很多的,虽然能够实现,但是非常绕。对redis不友好,不利于支持大数据量的排行榜。

解决方案

为了处理同score的用户的排名问题,可以把时间戳考虑到score里面。具体思路:

1、假设用户a的订单量为 40 单。最后一笔的订单下单时间戳是:1655567999
2、定义一个基准时间,可以是2050年(2539180799)、2100年这样。
3、给订单量 加上 (基准时间 - 下单时间)/ 基准时间。(基准时间 - 下单时间)/ 基准时间一定是小于1的。

在score的设计上:

    /**
     * 计算score,通过一个基准时间,可以是2100或2050年,减去lastOrderTime再除以基准时间,可以获得一个小于1的小数,
     * 在获取真正score的时候,只要舍去小数位即可
     * @param orderNum
     * @param lastOrderTime
     * @return
     */
    private double getOrderNum(int orderNum, long lastOrderTime) {
        return orderNum + (BASE_TIME - lastOrderTime) * 1.0 / BASE_TIME;
    }

4、在真正取score的时候,取整数位即可。

代码示例

    /**
     * 更新排行榜数据
     * @param ownerUid
     * @param lastOrderTime
     * @param orderNum
     */
    private void doUpdateCommunityRankingList(Long ownerUid, long lastOrderTime, int orderNum) {
        // 插入排行榜信息,对于zset,如果已经包含member,add的时候返回就是false
        redisTemplate.opsForZSet().add(ZSET_KEY, ownerUid.toString(),
                getOrderNum(orderNum, lastOrderTime));
    }

    /**
     * 计算score,通过一个基准时间,可以是2100或2050年,减去lastOrderTime再除以基准时间,可以获得一个小于1的小数,
     * 在获取真正score的时候,只要舍去小数位即可
     * @param orderNum
     * @param lastOrderTime
     * @return
     */
    private double getOrderNum(int orderNum, long lastOrderTime) {
        return orderNum + (BASE_TIME - lastOrderTime) * 1.0 / BASE_TIME;
    }

取出值的时候通过强转取整:

  long value =(long) t.getScore().doubleValue();
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐