背景
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();