7.7 热帖排行

198 阅读4分钟

我正在参加「掘金·启航计划」

热帖排行

image-20220730141821385

我们对于点赞、加精、评论的时候我们不去立刻算分,而是把分数变化的帖子先丢到一个缓存里,等定时的时间到了把缓存里这些产生变化的帖子算一下,其他没变的帖子不算,那这样每次算的数据量比较小,效率也比较高。

在能够影响帖子分数处将帖子id存到redis里

既然要往redis里存数据,先在 RedisKeyUtil 里定义一个 key

定义key的方法不需要传postId,因为产生变化的帖子是多个,不是一个,所以不要传帖子id进来

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

image-20220730154142874

image-20220730154157890

然后在那些能够影响帖子分数的操作发生的时候把帖子Id扔到 redis 里去,在存的时候我们应该存到 redis 的 set 里,因为我们只需要算一次(如果有多次,每次算都是一样的,重复了,效率不高)。

比如说:

  • 新增帖子的时候也给它一个初始的分

  • 加精的时候

  • 评论的时候

  • 点赞的时候

新增帖子时:

@Autowired
private RedisTemplate redisTemplate;

// 把帖子存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());

image-20220730154239898

image-20220730154323647

加精时:

image-20220730154401878

评论帖子时:

​ 注入RedisTemplate

@Autowired
private RedisTemplate redisTemplate;
// 将帖子id存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);

image-20220730160316505

image-20220730160353445

点赞时:

image-20220730160757322

image-20220730160905868

下面要做的就是每隔一段时间算一下

要更新分数,先添加一下更改分数方法

image-20220730165750368

image-20220730165900362

image-20220730170007618

需要用到定时任务,就用到了 Quartz

首先写一个Job (使用Quartz时需要写的)

PostScoreRefreshJob

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 {
            // SimpleDateFormat能把日期转成字符串,也能把字符串解析为日期,但前提是要指定格式
            epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
        } catch (ParseException e) {
            throw new RuntimeException("初始化牛客纪元失败!", e); // 出错误时抛出异常
        }
    }

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String redisKey = RedisKeyUtil.getPostScoreKey();
        // 因为对一个key反复操作,所以使用Bound API绑定key
        BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);

        // 如果没有数据就不用算了
        if (operations.size() == 0) {
            logger.info("[任务取消] 没有需要刷新的帖子!");
            return;
        }

        logger.info("[任务开始] 正在刷新帖子分数: " + operations.size());
        while (operations.size() > 0) {
            // refresh是计算,refresh具体内容就在下面
            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);

        // 计算权重,精华+75分 评论*10 + likeCount*2
        double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;
        // 分数 = 帖子权重 + 距离天数
        double score = Math.log10(Math.max(w, 1))    // 权重求个对数,里面和1求max是为了防止取对数之后变成负分
                + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24); //ms / (1000 * 3600 * 24)表示天
        // 更新帖子分数
        discussPostService.updateScore(postId, score);
        // 同步搜索数据
        post.setScore(score);
        elasticsearchService.saveDiscussPost(post);
    }

}

image-20220730183829362

image-20220730183901359

image-20220730183926030

然后这个任务要想正常运行我们还得做配置(配置一下JobDetailTrigger)

QuartzConfig


// 配置 -> 数据库 -> 调用
@Configuration
public class QuartzConfig {

    // FactoryBean可简化Bean的实例化过程:
    // 1.通过FactoryBean封装Bean的实例化过程.
    // 2.将FactoryBean装配到Spring容器里.
    // 3.将FactoryBean注入给其他的Bean.
    // 4.该Bean得到的是FactoryBean所管理的对象实例.

    // 配置JobDetail
    //@Bean
    public JobDetailFactoryBean alphaJobDetail() {
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(AlphaJob.class);
        factoryBean.setName("alphaJob");
        factoryBean.setGroup("alphaJobGroup");
        factoryBean.setDurability(true);        // 持久运行
        factoryBean.setRequestsRecovery(true);  // 任务可恢复
        return factoryBean;
    }

    // 配置Trigger(SimpleTriggerFactoryBean, CronTriggerFactoryBean)
    //@Bean
    public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) {
        SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(alphaJobDetail);
        factoryBean.setName("alphaTrigger");
        factoryBean.setGroup("alphaTriggerGroup");
        factoryBean.setRepeatInterval(3000);        // 多久执行一次
        factoryBean.setJobDataMap(new JobDataMap());
        return factoryBean;
    }

    // 刷新帖子分数任务
    @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);    // 5分钟执行一次
        factoryBean.setJobDataMap(new JobDataMap());
        return factoryBean;
    }
}

image-20220730183955346

image-20220730184011725

最后记得在application.properties配置一下Quartz

# QuartzProperties配置Quartz
# 下面配置的意思是
# 底层是jdbc
# communityScheduler是调度器名字
# 调度器id自动生成
# 用org.quartz.impl.jdbcjobstore.JobStoreTX将任务存到数据库
# 使用 org.quartz.impl.jdbcjobstore.StdJDBCDelegate 这个jdbc驱动,
# 采用集群方式
# 用org.quartz.simpl.SimpleThreadPool这个线程池
# 线程数量5
spring.quartz.job-store-type=jdbc
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=5

image-20220730184042598

最后启动项目测试的时候一定要开启 kafkaelasticsearch

展现

在主页最新是按时间倒序排,不用管

我们要处理的是点最热,按照分数来排

最热的时候要给服务端传一个新的参数,让它做一个排序,有一个新的排序模式

所以我们要对之前的代码做一下重构,让它能够支持

DiscussPostMapper

image-20220730184109349

discusspost-mapper.xml

image-20220730184149359

DiscussPostService

image-20220730184231701

HomeController

image-20220730184334650

然后是 index.html 页面

image-20220730184457648