持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的6天,点击查看活动详情
背景
我们经常遇到热点数据的问题,在一个博客网站中,最常遇到的就是文章的热榜的问题,即热点文章,现在我就简单实现一下这个热点文章榜单。
技术栈
Java + MySQL + Redis
思路
- 靠什么形成热点?🔥
- 用户的访问量
- 点赞数量,收藏数量,评论数量
- 人工自动形成热点数据
分数制:每一次进行 访问、点赞、收藏、评论,都会让该篇文章 "加分",然后根据分数的高低进行排序即可,分数最高的即 "榜一",依次排序;
- 热点数据的保存形式🔥
顾名思义,热点数据是大概率会被经常访问到的数据,所以我们使用 Redis 作为缓存数据库进行存储,加快访问的效率;
- 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:对于所有文章,我们每天定时减少其一定的分数,以便后来的文章居上,如果某篇文章成为了负数,也要立刻删除;
/**
* 每天定时扣分
*/
@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));
}
}
}