总结-14 热帖排行

我们需要实现首页对帖子进行排序的功能,具体就是计算每个帖子的分数,然后按分数降序排列。
因此,我们需要解决的是两个问题,第一个是如何计算帖子分数,第二个是在何时对帖子分数进行计算。
首先,计算帖子分数,我们希望发布越新的帖子的分数要更高一点,然后其变化能够平缓一点,不要出现分数骤降的情况。参考了牛客本身的公式,我们采用这样的公式
socre=log(精华分数+评论数*10+点赞数*2)+(发布时间-牛客纪元)。
然后是第二个问题,我们何时进行计算,首先每个帖子在更新时进行计算肯定是没有必要的,因为这样对服务器压力太大了,比如一个热帖同时有100人点赞,那服务器就要计算100边,但是用到的只有最后一个结果。
因此,我们可以采用定时任务的方式进行,比如每隔一小时计算一次,因此我们需要确定的是哪些帖子需要进行计算,对其用redis进行缓存起来,所用的类型很显然是set,因为需要去重,并且不要求顺序。
在计算完成之后,我们需要更新mysql的数据、elasticsearch的数据。
首先,我们定义存储帖子的redis的key,采用post:score的键,然后所有需要计算的帖子放入即可。

// 帖子分数
public static String getPostScoreKey(){
    return PREFIX_POST + SPLIT + "score";
}

然后,我们实现具体的job.该job需要实现Job接口的execute方法。首先,我们需要声明一个Date类型的值作为牛客纪元,然后在static块中初始化。
在execute方法中,我们使用BoundSetOperations实例来对set集合进行操作,判断set的大小,如果大于零,则调用refresh方法,参数为从set中弹出的一个Post实例。
在refresh方法中,我们得到是否加精、评论数量、点赞数量和发帖时间,然后对score进行计算,计算完成后我们更新帖子和elasticsearch服务器内容。

public class PostScoreRefreshJob implements Job, CommunityConstant {

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

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private LikeService likeService;

    @Autowired
    private ElasticsearchService elasticsearchService;

    // 牛客纪元
    private static final Date epoch;

    static {
        try {
            epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
        } catch (ParseException e) {
            throw new RuntimeException("初始化牛客纪元失败!");
        }
    }

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

        if (operations.size() == 0){
            logger.info("任务取消,没有需要刷新的帖子!");
            return;
        }

        logger.info("[任务开始] 正在刷新帖子分数:" + operations.size());
        while (operations.size() > 0){
            this.refresh((Integer) operations.pop());
        }
        logger.info("[任务结束] 刷新帖子分数完毕");
    }

    private void refresh(int postId){
        DiscussPost post = discussPostService.findDiscussPostByid(postId);
        if (post == null){
            logger.error("该帖子不存在,id="+postId);
            return;
        }

        // 是否加精
        boolean wonderful = post.getStatus() == 1;
        // 评论数量
        int commentCount = post.getCommentCount();
        // 点赞数量
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST,postId);

        // 计算权重
        double w = (wonderful? 75 : 0) + commentCount * 10 + likeCount * 2;
        // 分数 = 帖子权重 + 距离天数
        double score = Math.log10(Math.max(w, 1))
                + (post.getCreateTime().getTime() - epoch.getTime()) / (1000*3600*24);

        // 更新帖子分数
        discussPostService.updateScore(postId,score);
        // 同步搜索数据
        post.setScore(score);
        elasticsearchService.saveDiscussPost(post);
    }
}

那我们在何时将帖子加入redis缓存呢?首先就是发布帖子的时候,然后就是点赞、评论和加精处。
比如,在加精完成之后,我们将帖子加入redis中.

// 加精
@RequestMapping(path = "/wonderful",method = RequestMethod.POST)
@ResponseBody
public String setWonderful(int id){
    discussPostService.updateStatus(id,1);

    // 触发发帖事件
    Event event = new Event()
            .setTopic(TOPIC_PUBLISH)
            .setUserId(hostHolder.getUser().getId())
            .setEntityType(ENTITY_TYPE_POST)
            .setEntityId(id);
    eventProducer.fireEvent(event);

    // 计算帖子分数
    String redisKey = RedisKeyUtil.getPostScoreKey();
    redisTemplate.opsForSet().add(redisKey,id);

    return CommunityUtil.getJSONString(200);
}

此时Job已经完全就绪了,然后我们需要对JobDetail和Trigger进行配置,配置步骤和上一篇博客同理。

// 刷新帖子分数任务
@Bean
public JobDetailFactoryBean postScoreRefreshJobDetail(){
    JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
    factoryBean.setJobClass(PostScoreRefreshJob.class);
    factoryBean.setName("postScoreRefreshJob");
    factoryBean.setGroup("communityJobGroup");
    // 任务是否持久保存
    factoryBean.setDurability(true);
    factoryBean.setRequestsRecovery(true);
    return factoryBean;
}

@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail){
    SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
    factoryBean.setJobDetail(postScoreRefreshJobDetail);
    factoryBean.setName("postScoreRefreshTrigger");
    factoryBean.setGroup("communityTriggerGroup");
    factoryBean.setRepeatInterval(1000 * 60 * 5);
    factoryBean.setJobDataMap(new JobDataMap());
    return factoryBean;
}

最后就是展现,用户在首页点击最热可以查看按分数排名的帖子,因此我们需要对得到首页帖子的接口进行略微修改,我们需要引入一个排序模式参数,如果是1,我们就按热度进行排序。

List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);
<select id="selectDiscussPosts" resultType="DiscussPost">
    select <include refid="selectFields"></include>
    from discuss_post
    where status != 2
    <if test="userId!=0">
        and user_id = #{userId}
    </if>
    <if test="orderMode==0">
        order by type desc, create_time desc
    </if>
    <if test="orderMode==1">
        order by type desc, score desc, create_time desc
    </if>
    limit #{offset}, #{limit}
</select>

同时在controller中我们需要修改一些细节,首先是得到orderMode参数,默认设为0,然后对分页路径加上orderMode参数,最后是往model中加入orderMode参数即可

public String getIndexPage(Model model, Page page,
                               @RequestParam(name = "orderMode", defaultValue = "0") int orderMode)
/**

......

**/
page.setPath("/index?orderMode=" + orderMode);

/**

......

**/

model.addAttribute("orderMode",orderMode);

最后是在模板中,我们对最新和最热按钮加上路径即可

<ul class="nav nav-tabs mb-3">
	<li class="nav-item">
		<a th:class="|nav-link ${orderMode==0?'active':''}|" th:href="@{/index(orderMode=0)}">最新</a>
	</li>
	<li class="nav-item">
		<a th:class="|nav-link ${orderMode==1?'active':''}|" th:href="@{/index(orderMode=1)}">最热</a>
	</li>
</ul>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值