我正在参加「掘金·启航计划」
热帖排行
我们对于点赞、加精、评论的时候我们不去立刻算分,而是把分数变化的帖子先丢到一个缓存里,等定时的时间到了把缓存里这些产生变化的帖子算一下,其他没变的帖子不算,那这样每次算的数据量比较小,效率也比较高。
在能够影响帖子分数处将帖子id存到redis里
既然要往redis里存数据,先在 RedisKeyUtil 里定义一个 key
定义key的方法不需要传postId,因为产生变化的帖子是多个,不是一个,所以不要传帖子id进来
private static final String PREFIX_POST = "post";
// 帖子分数
public static String getPostScoreKey() {
return PREFIX_POST + SPLIT + "score";
}
然后在那些能够影响帖子分数的操作发生的时候把帖子Id扔到 redis 里去,在存的时候我们应该存到 redis 的 set 里,因为我们只需要算一次(如果有多次,每次算都是一样的,重复了,效率不高)。
比如说:
-
新增帖子的时候也给它一个初始的分
-
加精的时候
-
评论的时候
-
点赞的时候
新增帖子时:
@Autowired
private RedisTemplate redisTemplate;
// 把帖子存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());
加精时:
评论帖子时:
注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
// 将帖子id存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);
点赞时:
下面要做的就是每隔一段时间算一下
要更新分数,先添加一下更改分数方法
需要用到定时任务,就用到了 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);
}
}
然后这个任务要想正常运行我们还得做配置(配置一下JobDetail和Trigger)
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;
}
}
最后记得在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
最后启动项目测试的时候一定要开启 kafka、elasticsearch
展现
在主页最新是按时间倒序排,不用管
我们要处理的是点最热,按照分数来排
点最热的时候要给服务端传一个新的参数,让它做一个排序,有一个新的排序模式
所以我们要对之前的代码做一下重构,让它能够支持
DiscussPostMapper
discusspost-mapper.xml
DiscussPostService
HomeController
然后是 index.html 页面