文章热榜的简易实现

643 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的6天,点击查看活动详情

背景

我们经常遇到热点数据的问题,在一个博客网站中,最常遇到的就是文章的热榜的问题,即热点文章,现在我就简单实现一下这个热点文章榜单。

技术栈

Java + MySQL + Redis

思路

  1. 靠什么形成热点?🔥
  • 用户的访问量
  • 点赞数量,收藏数量,评论数量
  • 人工自动形成热点数据

分数制:每一次进行 访问、点赞、收藏、评论,都会让该篇文章 "加分",然后根据分数的高低进行排序即可,分数最高的即 "榜一",依次排序;

  1. 热点数据的保存形式🔥

顾名思义,热点数据是大概率会被经常访问到的数据,所以我们使用 Redis 作为缓存数据库进行存储,加快访问的效率;

  1. Redis数据结构的选取🔥
  • 由1中得知,我们是依靠分数作为 排行榜的依据,所以我们可以选择 Zset 作为Redis中的数据结构,根据分数获取前10个用户即可;
    • key:我们使用一个固定的key:article:hot
    • value文章的ID
    • score: 即为对应的分数;
  • 同时我们还需要 hash 数据结构 来记录 热点文章ID所对应的 具体文章的内容;
    • key:我们使用一个固定的key:article:hotcontent
    • hashKey文章的ID
    • value:具体文章内容

具体实现

定义一个加分枚举类

根据某一特定的操作,对某一篇文章进行加分,从而形成热点文章。

public enum HotRank {
    HOT_LIKE(100, 1),
    HOT_COMMENT(50, 2),
    HOT_COLLECT(150, 3);

    private int score;
    private int type;

    HotRank(int score, int type) {
        this.score = score;
        this.type = type;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }
}

实现热点数据的方法

在 Redis 中操作加分的方法

@Override
public void addRank(Long articleID, HotRank hotRank) {

    Boolean aBoolean = redisTemplate.opsForHash().hasKey(HOT_RANK_ARTICLE_CONTENT, articleID.toString());
    // 如果不存在
    if (!aBoolean) {
        // 添加到 排行榜中
        redisTemplate.opsForZSet().add(HOT_RANK_ARTICLE, articleID, hotRank.getScore());

        // 添加内容到hash集合中
        String title = articleMapper.selectTitleByArticleId(articleID);
        redisTemplate.opsForHash().put(HOT_RANK_ARTICLE_CONTENT, articleID.toString(), title);
    }

    // 存在就直接加分
    redisTemplate.opsForZSet().incrementScore(HOT_RANK_ARTICLE, articleID, hotRank.getScore());
}

根据 Redis 中的数据返回热点文章

/**
 * 获取热点文章
 *
 * @return
 */
@Override
public BaseResponse getHotRankArticles() {
    // 获取前10的文章
    Set<Object> ids = redisTemplate.opsForZSet().reverseRange(HOT_RANK_ARTICLE, 0, 9);
    HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
    if (ids != null) {
        List<ArticleHistory> collect = ids.stream().map((item) -> {
            // 根据id 获取详细内容
            Integer articleId = (Integer) item;
            String title = (String) hash.get(HOT_RANK_ARTICLE_CONTENT, String.valueOf(articleId));
            ArticleHistory articleHistory = new ArticleHistory();
            articleHistory.setArticleId(articleId.longValue());
            articleHistory.setTitle(title);
            return articleHistory;
        }).collect(Collectors.toList());

        return ResultUtils.success(collect);
    }
    return ResultUtils.success("暂无内容");
}

前端定时查询热点文章

前端要以较为频繁的频率去定时发送请求更新数据

const loadHotArticle = async () => {
        let ret = await http.get(`/article/hotRank`)
        return ret.data.data
    }

    useEffect(() => {
        loadHotArticle().then((ret) => {
            setHotArticle(ret)
        })

        // 每小时刷新一次文章热榜
        setInterval(() => {
            loadHotArticle().then((ret) => {
                setHotArticle(ret)
            })
        }, 1000 * 60 * 60) // 一小时刷一次

    }, [])

怎么移除热点文章

这里有两种情况:

  1. 长期霸占热点文章的需要及换,避免永久霸占热榜;
  2. 分数处于尾部的文章也要及时删除,但这样带来的问题就是会影响其成为热榜的可能;

解决方案:

对于问题1:对于所有文章,我们每天定时减少其一定的分数,以便后来的文章居上,如果某篇文章成为了负数,也要立刻删除

/**
 * 每天定时扣分
 */
@Override
public void decreaseScore() {
    // 获取分数
    int dropScore = HotRank.DROP_SCORE.getScore();

    // 获取所有的元素
    ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
    HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
    Set<Object> range = zSet.range(HOT_RANK_ARTICLE, 0, -1);

    if (range != null) {
        // 扣分
        for (Object obj : range) {
            Double incrementScore = zSet.incrementScore(HOT_RANK_ARTICLE, obj, dropScore);
            // 如果为0或者为负数就直接剔除就行

            if (incrementScore <= 0) {
                zSet.remove(HOT_RANK_ARTICLE, obj);
                // 同时还要剔除 hash里面对应的值
                hash.delete(HOT_RANK_ARTICLE_CONTENT, String.valueOf(obj));
            }
        }
    }
}

对于问题2:我们可以每3天清除末尾的文章,如果不小心刚删除了某一篇可能成为热点的文章,也没有关系,因为热点文章并不在乎其开始的分数,即使删除了,后面依旧会火起来。

/**
 * 使用定时任务来执行删除尾部的文章,如果文章数量过少就不打算删除
 */
@Override
public void removeRank() {
    ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
    Set<Object> range = zSet.reverseRange(HOT_RANK_ARTICLE, 20, -1);
    if (range != null && !range.isEmpty()) {
        HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
        // 移除数据
        for (Object key : range) {
            zSet.remove(HOT_RANK_ARTICLE, key);
            hash.delete(HOT_RANK_ARTICLE_CONTENT, String.valueOf(key));
        }
    }
}