使用Quartz实现热帖排行功能

热帖排行

Hacker News

Score = (P - 1) / (T + 2)^G
P: 流量(观看次数,点赞等)
T: 帖子发布的时间
G: 常数,1.5或1.8

StackOverflow

Score = P / T
P = log(Qviews) * 4 + (Qanswers * Qscore) / 5 + sum(Ascores)
T = (QageInHours + 1) - ((QageInHours - Qupdated) / 2) ^ 1.5
Qviews,Qanswers,Qscore,Ascores:观看次数,回答次数,问题的分数,回答的分数
QageInHours,Qupdated:问题发布时长,最新回答时间

Nowcoder

log(精华分 + 评论数 * 10 + 点赞数 * 2 + 收藏数 * 2) + (发布时间 - 牛客纪元)

策略

可以看到,帖子的热度和点赞观看的次数息息相关,但是点赞是很频繁的操作,如果每次点赞都要重新计算帖子的分值会大大影响性能。在实际项目中一般是采取一个定时任务,每隔一段时间就启动一次(如一小时)。

帖子的分值变化操作

发布时

@RequestMapping(path = "add", method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title, String content) {
        // 其他操作
        。。。
        
        // calculate the score of the post
        String redisKey = RedisKeyUtil.getPostScoreKey();
        redisTemplate.opsForSet().add(redisKey, post.getId());

        return CommunityUtil.getJSONString(0, "Article published successfully");
    }

点赞

 	@PostMapping(path = "/like")
    @ResponseBody
    @LoginRequired
    public String like(int entityType, int entityId, int entityUserId, int postId) {
        // 其他操作
        。。。。

        if(entityType == ENTITY_TYPE_POST){

            // calculate the score of the post
            String redisKey = RedisKeyUtil.getPostScoreKey();
            redisTemplate.opsForSet().add(redisKey, postId);
        }

        return CommunityUtil.getJSONString(0, null, map);
    }
}

添加评论

    @LoginRequired
    @RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
    public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
        // 其他操作
        。。。。

        if(comment.getEntityType() == ENTITY_TYPE_POST){

            // calculate the score of the post
            String redisKey = RedisKeyUtil.getPostScoreKey();
            redisTemplate.opsForSet().add(redisKey, discussPostId);
        }

        return "redirect:/discuss/detail/" + discussPostId;
    }

使用Quartz实现定时刷新

定义任务

@Component
public class PostScoreRefreshJob implements Job, CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);

//    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private void setRedisTemplate(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private LikeService likeService;

    private static final Date epoch;

    static{
        try {
            epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2023-8-29 00:00:00");
        } catch (Exception e) {
            throw new RuntimeException("initiate CoderCommunity epoch fail! ", e);
        }
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        String redisKey = RedisKeyUtil.getPostScoreKey();
        BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);

        if(operations.size() == 0) {
            logger.info("[No operations] No post needs to be refreshed.");
            return;
        }

        logger.info("[operations start] Refreshing the score of posts..." + operations.size());

        while(operations.size() > 0) {
            this.refresh((Integer) operations.pop());
        }

        logger.info("[operations end] Refreshing the score of posts...");
    }

    private void refresh(int postId) {
        DiscussPost post = discussPostService.findDiscussPostById(postId);

        if(post == null) {
            logger.error("This post is not exist, id = " + postId);
            return;
        }

        // whether this post is wonderful
//        boolean wonderful = post.getStatus() == 1;
        boolean wonderful = false;
        // the number of comments
        int commentCount = post.getCommentCount();
        // the number of likes
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);

        // calculate the weight of the post
        double w = (wonderful? 75 : 0) + commentCount * 10 + likeCount * 2;
        // PostScore = PostWeight + Time
        // in case the result after log is less than 0, we need to take max between w and 1
        double score = Math.log10(Math.max(w, 1)) + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);

        // refresh the score of the post
        discussPostService.updateScore(postId, score);
        // update the data in the Elasticsearch
        // Elasticsearch.updateScore(postId, score)
    }
}

配置Quartz的触发器及JobDetail

@Configuration
public class QuartzConfig {

    // refresh the score of the post
    @Bean
    public JobDetailFactoryBean postScoreRefreshJobDetail() {
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(PostScoreRefreshJob.class);
        factoryBean.setName("PostScoreRefreshJob");
        factoryBean.setGroup("communityJobGroup");
        // this task will be stored permanently although there are no triggers existing
        factoryBean.setDurability(true);
        // this task can be recovered after meeting some faults
        factoryBean.setRequestsRecovery(true);

        return factoryBean;
    }

    @Bean
    public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
        SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(postScoreRefreshJobDetail);
        factoryBean.setName("postScoreRefreshTrigger");
        factoryBean.setGroup("communityTriggerGroup");
        // the interval of the trigger
        factoryBean.setRepeatInterval(1000 * 60);
        // use an object to store the status of the job, `JobDataMap()` is the default object
        factoryBean.setJobDataMap(new JobDataMap());

        return factoryBean;
    }
}

你可能感兴趣的:(javaweb项目,python,开发语言)