文章目录
- 前言
- 需求说明
- Redis有序集合zset介绍
- 实现的关键问题
- 实现代码
- 组件依赖
- JAVA代码实现
- 结语
前言
最近在整理一些自己以前做过的项目,然后想到以前的一个项目中做过一个排行榜的功能,大概说一下这个功能吧。那个时候项目中推出一个活动:每月答题通关,按答题通关的次数做一个排行榜,月末的时候排行榜前50名的用户可以瓜分积分。
以前不懂事,每天都很颓废,得过且过。由于项目中排行榜这一块功能没有分配给我,想着事不关己高高挂起,就没管具体怎么实现的,只模糊知道是用Redis的zset实现的。现在想想是真的后悔,其实项目中没有事不关己的地方,只有努力去了解项目中的各个方面自己才能进步。希望这次的复习研究能让自己对这个知识点更加熟悉。
需求说明
实现用户的答题排行榜功能,用户每次答题通关一次给用户加1分,用户分数相同的情况下越早达成此分数的排行越靠前,每月的排行榜重新计算。
Redis有序集合zset介绍
Redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
实现的关键问题
现在我们知道zset是根据分数(score)来给集合中的元素排序的,那么关键的问题就是分数怎么设计才能实现我们的需求,特别是要注意用户的答题通关次数相同时越早达成此分数的排名越靠前。
此时我想到的设计方法是:
score的整数位存储用户的答题次数,小数位通过对时间戳处理后生成(1 - 时间戳/10^13)。
然后发现此时的小数位长度达到了13位,但是score的长度不建议超过16位(超过16位会转科学计数法,转字符串会丢失精度),这样的话整数位只有3位长度了,肯定远远不够啊。这个时候就需要想办法减少小数位的长度了,然后发现需求中排行榜的持续时间为一个月,那我们只取这一个月的时间戳不就行了吗。
改进后的小数位算法为:1 - (当前时间戳 - 当月1号0点的时间戳)/10^10,这个时候小数位的长度就只有10位了,6位的长度对于整数位也够用了。
实现代码
组件依赖
首先在pom.xml文件中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
JAVA代码实现
package com.test.javademo.redis.rankinglist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 用户分数排行榜,分数相同的情况下,获得此分数时间最早的用户排前面
* @author ZhengNC
* @date 2020/5/15 16:28
*/
@RestController
@RequestMapping("/ranking")
public class RankingController {
@Autowired
RedisTemplate redisTemplate;
//排行榜在redis中的key
//可以改造加上月份,实现每个月的排行榜单独存在,不用清除历史数据
private final String rankingKey = "ranking";
/**
* 给用户加一分
* @param name
* @return
*/
@GetMapping("addCore")
public String addCore(@RequestParam("name") String name){
Double score = redisTemplate.opsForZSet().score(rankingKey, name);
Date now = new Date();
//当前时间的时间戳
long mi = now.getTime();
//本月初的时间戳
long startMi = 0;
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM");
try {
Date startDate = format.parse(format.format(now));
startMi = startDate.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(mi - startMi);
double dmi = 1D - (double)(mi - startMi) / 10000000000D;
//保留10位小数,舍去10位以后的数值
BigDecimal b = new BigDecimal(dmi);
dmi = b.setScale(10, BigDecimal.ROUND_DOWN).doubleValue();
System.out.println(dmi);
double newScore;
if (score == null){
newScore = 1D + dmi;
System.out.println(name+"还没有分数");
}else {
System.out.println(name+"的分数增加前为:"+score);
newScore = Math.floor(score) + 1D + dmi;
}
redisTemplate.opsForZSet().add(rankingKey, name, newScore);
return "添加成功";
}
/**
* 获取排行榜前50名的姓名和分数
* @return
*/
@GetMapping("rankingList")
public Object rankingList(){
Set<String> rankList = redisTemplate.opsForZSet().reverseRange(rankingKey, 0L, 49L);
List<Map> result = new ArrayList<>();
for (String name : rankList){
Map<String, Object> map = new HashMap<>();
map.put("name", name);
Double score = redisTemplate.opsForZSet().score(rankingKey, name);
map.put("score", score);
result.add(map);
}
return result;
}
}
结语
简单的使用Redis实现的排行榜功能,还很简陋,欢迎大家提出改进的意见。