Redis ZSet数据结构实现排行榜功能
一. 使用场景
公司新项目要求, 实现每日排行榜以及各省排行榜
二. 功能实现(Java)
1. 排行榜数据插入及更新
/**
* @date: 2022/11/2
* @description: 每天凌晨6点生成排行榜数据
*/
@Override
@Scheduled(cron = "0 0 6 * * ?")
public void generate() {
try {
// 今日排行榜
String dailyKey = today + "_PERSON_RANK";
String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
// 随机获取表里的机器人信息
Result<ArrayList<UserPlainVO>> userIdResults = userManager.getRankUserIds();
ArrayList<UserPlainVO> userPlainVOS = userIdResults.getResults();
if (Objects.equals("200",userIdResults.getCode()) && CollectionUtils.isNotEmpty(userPlainVOS)) {
for (UserPlainVO userPlainVO : userPlainVOS) {
// 作答时间为250到750的随机数
int usedTime = getRandomNum(250,750);
redisTemplate.opsForZSet().add(dailyKey,userPlainVO.getUserId(),usedTime);
}
}
// 设置次日凌晨6点key失效
String nextDayMorning = LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_DATE) + " 06:00:00";
Date nextDay = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(nextDayMorning);
redisTemplate.expireAt(dailyKey,nextDay);
// 获取全部省份名称,初始值为5K-2W的随机数,每日累加3K-5K的随机数
String provinceKey = "PROVINCE_RANK";
List<String> provinces = sysBasService.getRankProvinces(); // 从字典表获取
for (String province : provinces) {
// 判断该成员省是否已存入缓存中
Double score = redisTemplate.opsForZSet().score(provinceKey, province);
if (Objects.isNull(score)) {
// 不在赋予初始值(1W-2W)
int scoreFirst = getRandomNum(10000,20000);
redisTemplate.opsForZSet().add(provinceKey,province,scoreFirst);
} else {
// 存在则累加
int scoreAdd = getRandomNum(3000,5000);
redisTemplate.opsForZSet().incrementScore(provinceKey,province,scoreAdd);
}
}
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* @date: 2022/11/8
* @description: 获取范围内的随机整数
* @param min:
* @param max:
*/
public static int getRandomNum(int min, int max) {
return (int) (Math.random() * (max - min + 1) + min);
}
2. 排行榜数据获取
2.1 创建排行榜数据接收实体
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.List;
/**
* @author xiazz
* @Date 2022/10/14
* @Description:
*/
public class RankInfoVO implements Serializable {
@ApiModelProperty("用户排名: -1表示未上榜")
private Long userRank;
@ApiModelProperty("用户通关总耗时: -1表示未通关")
private Integer userSumTime;
@ApiModelProperty("用户所在地区排名: -1表示未上榜")
private Long userAreaRank;
@ApiModelProperty("今日排行榜列表")
private List<DailyRankVO> dailyRankVOS;
@ApiModelProperty("省份排行榜列表")
private List<ProvinceRankVO> provinceRankVOS;
@ApiModelProperty("用户名")
private String currentUsername;
@ApiModelProperty("用户头像")
private String currentImgUrl;
public Long getUserRank() {
return userRank;
}
public void setUserRank(Long userRank) {
this.userRank = userRank;
}
public Integer getUserSumTime() {
return userSumTime;
}
public void setUserSumTime(Integer userSumTime) {
this.userSumTime = userSumTime;
}
public Long getUserAreaRank() {
return userAreaRank;
}
public void setUserAreaRank(Long userAreaRank) {
this.userAreaRank = userAreaRank;
}
public List<DailyRankVO> getDailyRankVOS() {
return dailyRankVOS;
}
public void setDailyRankVOS(List<DailyRankVO> dailyRankVOS) {
this.dailyRankVOS = dailyRankVOS;
}
public List<ProvinceRankVO> getProvinceRankVOS() {
return provinceRankVOS;
}
public void setProvinceRankVOS(List<ProvinceRankVO> provinceRankVOS) {
this.provinceRankVOS = provinceRankVOS;
}
public String getCurrentUsername() {
return currentUsername;
}
public void setCurrentUsername(String currentUsername) {
this.currentUsername = currentUsername;
}
public String getCurrentImgUrl() {
return currentImgUrl;
}
public void setCurrentImgUrl(String currentImgUrl) {
this.currentImgUrl = currentImgUrl;
}
}
日榜数据
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* @author xiazz
* @Date 2022/10/18
* @Description:
*/
public class DailyRankVO implements Serializable {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("通关耗时: -1表示未通关")
private Integer sumUsedTime;
@ApiModelProperty("排名: -1表示未上榜")
private Long rank;
@ApiModelProperty("用户头像")
private String imgUrl;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getSumUsedTime() {
return sumUsedTime;
}
public void setSumUsedTime(Integer sumUsedTime) {
this.sumUsedTime = sumUsedTime;
}
public Long getRank() {
return rank;
}
public void setRank(Long rank) {
this.rank = rank;
}
public String getImgUrl() {
return imgUrl;
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
}
}
省榜
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* @author xiazz
* @Date 2022/10/18
* @Description:
*/
public class ProvinceRankVO implements Serializable {
@ApiModelProperty("省份")
private String province;
@ApiModelProperty("排名")
private Long rank;
@ApiModelProperty("通关人数")
private Integer passCount;
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public Long getRank() {
return rank;
}
public void setRank(Long rank) {
this.rank = rank;
}
public Integer getPassCount() {
return passCount;
}
public void setPassCount(Integer passCount) {
this.passCount = passCount;
}
}
2.2 获取排行榜数据(日榜前50, 省榜全部)
/**
* @param openid: 小程序用户的唯一凭证
* @param rankType: 1日榜 2省榜
* @date: 2022/10/18
* @description: 排行榜
*/
@Override
public net.huashitong.ssydt.provider.web.model.Result<RankInfoVO> getRank(String openid, Integer rankType) {
long t1 = System.currentTimeMillis();
RankInfoVO rankInfoVO = new RankInfoVO();
// 用户名
String userId = "";
String userProvince = "";
// 调用用户服务的接口获取用户信息
Result<User> userResult = userManager.info(openid);
User user = userResult.getResults();
if (Objects.equals("200",userResult.getCode()) && Objects.nonNull(user)) {
userId = user.getUserId();
userProvince = user.getProvince();
rankInfoVO.setCurrentUsername(StringUtils.isNotBlank(user.getNickname()) ? user.getNickname() : "");// 用户名
rankInfoVO.setCurrentImgUrl(StringUtils.isNotBlank(user.getImageUrl()) ? user.getImageUrl() : ""); // 用户头像
}
String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
if (rankType == 1) {
String dailyKey = today + "_PERSON_RANK";
// 用户排名
if (StringUtils.isBlank(userId)) {
rankInfoVO.setUserRank((long) -1);
rankInfoVO.setUserSumTime(-1);
} else {
Long userRank = redisTemplate.opsForZSet().rank(dailyKey, userId);
rankInfoVO.setUserRank(Objects.isNull(userRank) ? -1 : userRank + 1);
// 用户通关总耗时
Double score = redisTemplate.opsForZSet().score(dailyKey, userId);
rankInfoVO.setUserSumTime(Objects.isNull(score) ? -1 : score.intValue());
}
// 获取今日排行榜
ArrayList<DailyRankVO> dailyRankVOS = new ArrayList<>();
Set<ZSetOperations.TypedTuple<Object>> set = redisTemplate.opsForZSet().rangeWithScores(dailyKey, 0, 49);
for (ZSetOperations.TypedTuple<Object> tuple : set) {
DailyRankVO dailyRankVO = new DailyRankVO();
String dailyUserId = (String) tuple.getValue(); // 用户id
Result<User> dailyUserResult = userManager.get(dailyUserId);
User dailyUser = dailyUserResult.getResults();
if (!Objects.equals("200",dailyUserResult.getCode()) || Objects.isNull(dailyUser)) {
throw new ServiceException("100210","用户不存在");
}
dailyRankVO.setUsername(StringUtils.isNotBlank(dailyUser.getNickname()) ? dailyUser.getNickname() : ""); // 用户名
dailyRankVO.setImgUrl(StringUtils.isNotBlank(dailyUser.getImageUrl()) ? dailyUser.getImageUrl() : ""); // 头像
Double dailyUserSumTime = tuple.getScore(); // 通关耗时
dailyRankVO.setSumUsedTime(Objects.isNull(dailyUserSumTime) ? -1 : dailyUserSumTime.intValue());
// 排名
Long rank = redisTemplate.opsForZSet().rank(dailyKey, dailyUserId);
dailyRankVO.setRank(Objects.isNull(rank) ? -1 : rank + 1);
dailyRankVOS.add(dailyRankVO);
}
rankInfoVO.setDailyRankVOS(dailyRankVOS);
} else {
// 省份
String provinceKey = "PROVINCE_RANK";
// 用户所在省份排名
if (StringUtils.isBlank(userProvince)) {
rankInfoVO.setUserAreaRank((long) -1);
} else {
Long userAreaRank = redisTemplate.opsForZSet().reverseRank(provinceKey, userProvince);
rankInfoVO.setUserAreaRank(Objects.isNull(userAreaRank) || StringUtils.isBlank(userProvince) ? -1 : userAreaRank + 1);
}
Set<ZSetOperations.TypedTuple<Object>> set = redisTemplate.opsForZSet().reverseRangeWithScores(provinceKey, 0, -1);
if (CollectionUtils.isNotEmpty(set)) {
ArrayList<ProvinceRankVO> provinceRankVOS = new ArrayList<>();
for (ZSetOperations.TypedTuple<Object> tuple : set) {
ProvinceRankVO provinceRankVO = new ProvinceRankVO();
String province = (String) tuple.getValue();// 省份
provinceRankVO.setProvince(province);
Double passCount = tuple.getScore(); // 通关人数
provinceRankVO.setPassCount(Objects.isNull(passCount) ? -1 : passCount.intValue());
// 排名
Long rank = redisTemplate.opsForZSet().reverseRank(provinceKey, province);
provinceRankVO.setRank(Objects.isNull(rank) ? -1 : rank + 1);
provinceRankVOS.add(provinceRankVO);
}
rankInfoVO.setProvinceRankVOS(provinceRankVOS);
}
}
long t2 = System.currentTimeMillis();
logger.info("请求排行榜耗时: " + (t2-t1));
return ResultUtils.getSuccessResults(rankInfoVO);
}