关于热搜功能,我们可以通过 Redis 的 ZSet 结构来实现。简单来说,ZSet 也是一个 Set,不过 Set 中的每一个元素都有一个决定排名的权重值,因此,我们可以直接通过 redis 提供的命令获取排名后的结果。
核心代码
因为要实现的是每日、每周(近七天),每月(近三十天)的热点文章列表,所以,这里我们先将这三个类型定义成常量
public abstract class ArticleRedisKey {
// 日点击排行的 key,
// 每天一个集合,key 的格式是 article:rank:月-日,比如 article:rank:11-01
public static final String RANK_PREFIX_KEY = "article:rank:";
// 周点击排行的 key
// 是用来存放这七天求完并集的并集结果的
public static final String RANK_WEED_PREFIX_KEY = "article:weekrank";
// 月点击排行的 key
// 是用来存放这这30天求完并集的并集结果的
public static final String RANK_MONTH_PREFIX_KEY = "article:monthrank";
}
下面到核心代码了,这里我单独封装了一个 Rankervice,然后交给IOC容器管理,所以,在用到的地方直接注入即可。
- increby():给指定文章当天权重+1(权重由点击量决定,初始为1)
- queryTodayRankList():查询当天文章权重前五
- queryWeekRankList():查看近七天文章权重前五
- queryMonthRankList():查看近三十天文章权重前五
@Component
public class Rankervice {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 增加文章 score
* 每当一篇文章被点击就将它相应 score++
*/
public void increby(long articleId) {
// 组装当日的 key
String date = new SimpleDateFormat("MM-dd").format(new Date());
String rankKey = ArticleRedisKey.RANK_PREFIX_KEY + date;
String id = String.valueOf(articleId);
// 判断zset中是否已经有该成员了
if (redisTemplate.opsForZSet().rank(rankKey, id) == null) {
// 没有就初始化
redisTemplate.opsForZSet().add(rankKey, id, 1);
} else {
// 有就 score++
redisTemplate.opsForZSet().incrementScore(rankKey, id, 1);
}
}
/**
* 获取当日排行
* @return 前五篇文章 ID
*/
public List<Long> queryTodayRankList() {
// 组中当日的 key
String date = new SimpleDateFormat("MM-dd").format(new Date());
String rankKey = ArticleRedisKey.RANK_PREFIX_KEY + date;
return getRankList(rankKey);
}
/**
* 获取这一周(过去七天)排行
* @return 前五篇文章 ID
*/
public List<Long> queryWeekRankList() {
String prefixKey = ArticleRedisKey.RANK_PREFIX_KEY;
// 今日的key
String todayKey = prefixKey + getLastTimeString(0);
// 组装进七日的 key
List<String> keys = new ArrayList<>();
for (int i = 1; i < 7; i++) {
String key = prefixKey + getLastTimeString(i);
keys.add(key);
}
// 求近七天并集,结果放到 article:weekrank
// 注意:并集的结果会覆盖此集合已有内容,所以不用手动清空
redisTemplate.opsForZSet().unionAndStore(todayKey, keys, ArticleRedisKey.RANK_WEED_PREFIX_KEY);
// 并集求前五
return getRankList(ArticleRedisKey.RANK_WEED_PREFIX_KEY);
}
/**
* 获取这一月(过去30天)排行
* @return 前五篇文章 ID
*/
public List<Long> queryMonthRankList() {
String prefixKey = ArticleRedisKey.RANK_PREFIX_KEY;
// 今日的key
String todayKey = prefixKey + getLastTimeString(0);
// 组装近30天的 key
List<String> keys = new ArrayList<>();
for (int i = 1; i < 30; i++) {
String key = prefixKey + getLastTimeString(i);
keys.add(key);
}
// 将近三十天的并集结果放到 article:monthrank 集合中
// 注意:并集的结果会覆盖此集合已有内容,所以不用手动清空
redisTemplate.opsForZSet().unionAndStore(todayKey, keys, ArticleRedisKey.RANK_MONTH_PREFIX_KEY);
// 并集求前五
return getRankList(ArticleRedisKey.RANK_MONTH_PREFIX_KEY);
}
/**
* 指定集合求权重前五
*/
private List<Long> getRankList(String rankKey) {
Set<String> ids = redisTemplate.opsForZSet().reverseRange(rankKey, 0, 5);
return ids.stream().map(id -> Long.valueOf(id)).collect(Collectors.toList());
}
/**
* 获取过去第 n 天的 MM-dd
*/
private String getLastTimeString(int days) {
long time = Calendar.getInstance().getTimeInMillis();
for (int i = 0; i < days; i++) {
time -= 60 * 1000 * 60 * 24;
}
Date date = new Date(time);
String format = new SimpleDateFormat("MM-dd").format(date);
return format;
}
}
下面我们就来看看在实际开发中是怎样的逻辑
1.打开文章时记入 ZSET
打开文章接口
@GetMapping("/{id}")
public ResponseEntity<ArticleResponse> queryArticle(@PathVariable("id")Long id){
return ResponseEntity.ok(this.articleService.queryArticle(id));
}
Service 实现
@Autowired
private RankService rankService; // 注入 RankService
@Override
public ArticleResponse queryArticle(Long id) {
Article article = this.articleDao.selectByPrimaryKey(id);
Long clickCount;
// 点击量++
String clickCountKey = ArticleRedisKey.COUNT_PREFIX_KEY + id;
if(!redisTemplate.hasKey(clickCountKey)){
clickCount = article.getClick();
redisTemplate.opsForValue().set(clickCountKey, String.valueOf(clickCount));
}else{
clickCount = Long.parseLong(redisTemplate.opsForValue().get(clickCountKey));
clickCount++;
redisTemplate.opsForValue().set(clickCountKey,clickCount.toString());
}
// 当前文章在当日集合的 score++
rankervice.increby(id);
// 组装返回结果
ArticleResponse response = new ArticleResponse();
response.setArtId(article.getId());
response.setCreate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(article.getCreated()));
response.setClick(clickCount);// 从 redis 中取值放入
response.setCommentCount(article.getCommentCount());
response.setContent(article.getContent());
response.setAlike(article.getAlike());
response.setImages(article.getImages());
response.setTitle(article.getTitle());
response.setAuthorName((String) this.userDetailService.queryUserInfo(article.getAuthor()).get("username"));
response.setAuthorHead((String) this.userDetailService.queryUserInfo(article.getAuthor()).get("head"));
response.setComments(commentService.queryCommnetLists(id));
return response;
}
2.提供查询热搜列表的接口
查询热点文章文章的接口,入参是类型(上面定义过常量)
@GetMapping("/ranklist")
public ResponseEntity<List<RankListResponse>> queryRankList(@RequestParam(name = "date",defaultValue = "1")Integer date){
return ResponseEntity.ok(this.articleService.queryRankList(date));
}
Service 逻辑
@Autowired
private RankService rankService; // 注入 RankService
@Override
public List<RankListResponse> queryRankList(Integer date) {
// 根据类型查询热点文章列表
List<Long> rankList;
if (date == DateType.TODAY) {
rankList = rankervice.queryTodayRankList();
} else if (date == DateType.WEEK){
rankList = rankervice.queryWeekRankList();
} else {
rankList = rankervice.queryMonthRankList();
}
// 组装返回结果(惯有id还不行,要有标题)
List<RankListResponse> result = rankList.stream().map(r -> {
RankListResponse rankListResponseDto = new RankListResponse();
rankListResponseDto.setId(r);
rankListResponseDto.setTitle(queryArticleTitle(r));
return rankListResponseDto;
}).collect(Collectors.toList());
return result;
}
3.通过 Quartz 定时任务,将超过30天的ZSet删除
这里是通过 Quartz 每天都清空一次三十天前的 key。
注:这里 redisTemplate.expire(key1, 30, TimeUnit.DAYS); 也能实现,只不过项目中已经有多处定时落库,所以这里统一用 Quartz 写定时任务,好管理
@Component
public class ArticleRankTask extends QuartzJobBean {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String KEY = ArticleRedisKey.RANK_PREFIX_KEY + "*";
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
String lastestKey = getLastTimeString(30);
if (redisTemplate.hasKey(lastestKey)) {
redisTemplate.delete(lastestKey);
}
}
/**
* 获取过去第 n 天的 MM-dd
*/
private String getLastTimeString(int days) {
long time = Calendar.getInstance().getTimeInMillis();
for (int i = 0; i < days; i++) {
time -= 60 * 1000 * 60 * 24;
}
Date date = new Date(time);
String format = new SimpleDateFormat("MM-dd").format(date);
return format;
}
}
@Configuration
public class QuartzConfig {
/**
* 热点文章列表
*/
@Bean
public JobDetail articleRankTask(){
return JobBuilder.newJob(ArticleRankTask.class).withIdentity("articleRankTask").storeDurably().build();
}
@Bean
public Trigger articleRankTaskTrigger(){
return TriggerBuilder.newTrigger().forJob(articleClickTask())
.withIdentity("articleRankTask")
.withSchedule(CronScheduleBuilder.cronSchedule("0 59 23 * * ? *"))
.build();
}
}
本文标题:【项目杂记】实现日、周、月热搜文章列表(完整逻辑)
本文链接:https://blog.quwenai.cn/post/9859.html
版权声明:本文不使用任何协议授权,您可以任何形式自由转载或使用。








还没有评论,来说两句吧...