一些网站的排行公式:
开发过程
通常而言,启动一个定时任务进行分数计算,合理的方式:前面的热门帖子保持一定的时间不变,等过段时间定时任务算完在更新。这里为了方便测试,定时任务设置为5min更新一次。
结果的展现:排序的不同
定时计算没必要把所有的帖子都算一遍,把分数变化的帖子丢到一个缓存中,等到定时到了要计算的时候把缓存中的帖子拿出来计算,这样减轻服务器的负担。
RedisKeyUtil
//帖子分数,存的是产生变化的帖子,是多个。不需要传入Id
public static String getPostScoreKey(){
return PREFIX_POST + SPLIT + "score";
}
DiscussPostController
在能影响帖子分数发生变化的地方增加上述的key。
addDiscussPost方法中,在return CommunityUtil.getJSONString(0,“发送成功!”);之前,添加
//计算帖子的分数,不是立刻算,是放到redis中等定时任务到了才算
String redisKey = RedisKeyUtil.getPostScoreKey();
//这里放到set比较好,而不是放到队列中,因为存到队列里会导致重复计算,不关注顺序,能去重的话用set
redisTemplate.opsForSet().add(redisKey,post.getId());
置顶不需要加分,直接就到最上面了。
加精:
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,id);
CommentController
评论
//只有评论给帖子才放到搜索服务器中
if (comment.getEntityType() == ENTITY_TYPE_COMMENT){
event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(comment.getUserId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPostId);
eventProducer.fireEvent(event);
//只有评论给帖子才计算分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,discussPostId);
}
LikeController
点赞
//只有给帖子点赞才触发计算分数
if (entityType == ENTITY_TYPE_POST){
//只有评论给帖子才计算分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,postId);
}
Quartz定时任务
Job
public class PostScoreRefreshJob implements Job, CommunityConstant {
//定时任务在关键节点记录一下日志
private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);
@Autowired
private RedisTemplate redisTemplate;
//查询帖子、用户,并记录到es中
@Autowired
private DiscussPostService discussPostService;
@Autowired
private LikeService likeService;
@Autowired
private ElasticsearchService elasticsearchService;
//初始化牛客纪元
private static final Date epoch;
static {
//只需要初始化一次,指定格式转换日期给epoch,后面写的是满足格式的日期
try {
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();
BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);
if (operations.size() == 0 ){
logger.info("任务取消,无需要刷新的帖子");
}
logger.info("任务开始,正在刷新帖子分数"+operations.size());
while (operations.size()>0){
this.refresh((Integer) operations.pop());
}
logger.info("任务结束,帖子分数刷新完毕");
}
private void refresh(Integer postId) {
DiscussPost post = discussPostService.findDiscussPostById(postId);
if (post == null){
logger.error("该帖子不存在,可能是操作后被删除了"+postId);
return;
}
//是否精华帖子
boolean wonderful = post.getStatus() == 1;
//评论数量
int commentCount = post.getCommentCount();
//点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST,postId);
//计算分数的公式
// 计算权重,如果是精华,就固定加75的权重值
double w = (wonderful?75:0)+commentCount*10+likeCount*2;
//分数=log10(帖子权重,要保证w不小于1,不然会负数,所以有个max)+以天为单位计算距离时间
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);
}
}
discusspostMapper新增updateScore方法
mapper->mapper.xml->service
Quartz中更新配置
//刷新帖子分数的任务
//先配置JobDetail 任务详情
@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;
}
//然后配置Trigger 触发器
@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail){
//Trigger依赖于JobDetail
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshJobDetail);//对那个JobDetail进行设置,spring会优先注入同名的
factoryBean.setName("PostScoreRefreshTrigger");
factoryBean.setGroup("communityTriggerGroup");
factoryBean.setRepeatInterval(1000*60*5);//多长时间执行 5min
factoryBean.setJobDataAsMap(new JobDataMap()); //底层要用对象来存,存job的状态
return factoryBean;
}
首页最热帖子排行
代码重构,支持两种模式orderMode
mapper中:
List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit,int orderMode);
mapper.xml:
<select id="selectDiscussPosts" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
-- 如果状态为2(删除状态的)就不能传入
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
</if>
limit #{offset},#{limit}
</select>
前面调用的地方都得改
public List<DiscussPost> findDiscussPosts(int userId,int offset,int limit,int orderMode){
return discussPostMapper.selectDiscussPosts(userId,offset,limit,orderMode);
}
homeController
public String getIndexPage(Model model, Page page,
@RequestParam(name = "orderMode",defaultValue = "0") int orderMode){
List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit(),orderMode);
page.setPath("/index?orderMode="+orderMode); //在这里把order传进去,不然分页的参数就没有orderMode了
前端HTML
<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>