排行榜设计

324 阅读3分钟

一、设计思路

image.png

排行榜一般来说分为实时的,非实时的。

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列表内数据一直是排好序的。

image.png

此时同样需要出题人及对应的贡献度,所以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;
}