一、设计思路
排行榜一般来说分为实时的,非实时的。
1.1 实时方案
- 数据库统计:
现在数据库里面的 createby 字段。用户的标识是唯一的,可直接通过 group by 的形式统计 count。
select count(1),create_by from subject_info group by create_by limit 0,5;
数据量比较小,并发也比较小。这种方案是 ok 的。保证可以走到索引,返回速度快,不要产生慢 sql。
也可以在数据库层面加一层缓存,但要接受一定的延时性。
- redis 的 sorted set:
采用有序集合Zset,在key,value的基础上,每一个 value 都会包含一个 score 分数的概念。redis 根据分数可以帮助我们做从小到大,和从大到小的一个处理,zset列表内数据一直是排好序的。
此时同样需要出题人及对应的贡献度,所以value存放createBy,score作为贡献度。
这种的好处在于,完全不用和数据库做任何的交互,纯纯的通过缓存来做,速度非常快,要避免一些大 key 的问题。
1.2 非实时方案
采用定时任务 xxl-job 统计数据库的数据形式,帮助我们统计完成后,直接写入缓存。外部交互直接走缓存。
二、传统数据库实现排行榜
首先需要知道排行榜展示需要的字段:subjectCount(贡献度,题目数量),createUser(创建人,nickname用户昵称),createUserAvatar(头像)。
- 在SubjectInfoDto和BO中加入这三个字段信息
- 在subjectInfo表可按createdBy分组查询到每个人的题目数量:
select count(1) as subjectCount, created_by from subject_info where is_deleted = 0 and created_by is not null group by created_by limit 0,5
- 此时获取到subjectInfo集合存储了(createdBy和subjectCount),遍历集合rpc调用auth微服务,根据username查询auth_user表获取用户的nickname和avatar,最后组装BOList返回。
domain层代码:
@Override
public List<SubjectInfoBO> getContributeList() {
List<SubjectInfo> subjectInfoList = subjectInfoServices.getContributeList();
if(CollectionUtils.isEmpty(subjectInfoList)) {
return Collections.emptyList();
}
List<SubjectInfoBO> subjectInfoBOList = new LinkedList<>();
subjectInfoList.forEach(subjectInfo -> {
SubjectInfoBO subjectInfoBO = new SubjectInfoBO();
subjectInfoBO.setSubjectCount(subjectInfo.getSubjectCount());
UserInfo userInfo = userRpc.getUserInfo(subjectInfo.getCreatedBy());
subjectInfoBO.setCreateUserAvatar(userInfo.getAvatar());
subjectInfoBO.setCreateUser(userInfo.getNickName());
subjectInfoBOList.add(subjectInfoBO);
});
return subjectInfoBOList;
}
三、基于redis的zset实现排行榜(项目采用)
3.1 redisUtil中有关Zset的api
/**
* 有序集合 Zadd key score1 member1 [score2 member2] 向有序集合添加一个 / 多个 成员
*/
public Boolean zAdd(String key, String value, Long score) {
return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
}
/**
* 获取有序集合对象数量
*/
public Long countZset(String key) {
return redisTemplate.opsForZSet().size(key);
}
/**
* Zrange key start stop 通过索引区间返回指定区间内的成员
*/
public Set<String> rangeZset(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* Zrem key member1移除一个成员
*/
public Long removeZset(String key, Object value) {
return redisTemplate.opsForZSet().remove(key, value);
}
/**
* Zrem key member1 [member2] ... 移除多个成员
*/
public void removeZsetList(String key, Set<String> value) {
value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
}
/**
* 获取有序列表 某个元素的分数
*/
public Double score(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 获取有序集合中指定分数范围内的元素
*/
public Set<String> rangeByScore(String key, long start, long end) {
return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
}
/**
* Zincrby key increment member 给有序列表中的 某个元素添加分数
*/
public Object addScore(String key, Object obj, double score) {
return redisTemplate.opsForZSet().incrementScore(key, obj, score);
}
/**
* 获取有序列表 某个元素的分数排名
*/
public Object rank(String key, Object obj) {
return redisTemplate.opsForZSet().rank(key, obj);
}
/**
* 获取有序集合中的元素及其分数(rangeWithScores默认从小往大排,reverseRangeWithScores翻转为从大往小排)
* TypedTuple 接口包含了两个重要的方法:
* getValue():用于获取元素的值,在这种情况下是 String 类型。
* getScore():用于获取元素在有序集合中的分数,通常是 Double 类型。
*/
public Set<ZSetOperations.TypedTuple<String>> rankWithScore(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
3.2 domain层代码实现
@Override
public List<SubjectInfoBO> getContributeList() {
Set<ZSetOperations.TypedTuple<String>> typedTuples = redisUtil.rankWithScore(RANK_KEY, 0, 5);
if(CollectionUtils.isEmpty(typedTuples)) {
return Collections.emptyList();
}
List<SubjectInfoBO> subjectInfoBOList = new LinkedList<>();
typedTuples.forEach(rank -> {
SubjectInfoBO subjectInfoBO = new SubjectInfoBO();
subjectInfoBO.setSubjectCount(rank.getScore().intValue());
UserInfo userInfo = userRpc.getUserInfo(rank.getValue());
subjectInfoBO.setCreateUserAvatar(userInfo.getAvatar());
subjectInfoBO.setCreateUser(userInfo.getNickName());
subjectInfoBOList.add(subjectInfoBO);
});
return subjectInfoBOList;
}