Day30:热帖排行、生成长图、将文件上传到云服务器、优化热门帖子列表、压力测试

热帖排行

不同的算分方式:

image

只存变化的帖子到redis中,每五分钟算一次分,定时任务

存redis

构建redis键

//统计帖子分数
//key:post:score -> value:postId
public static String getPostScoreKey() {
    return PREFIX_POST + SPLIT + "score";
}

添加帖子时

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

加精时

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

        //触发发帖事件,将帖子存入es服务器
        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(0);
    }

评论时

//触发发帖时间,存到es服务器
        if(comment.getEntityType() == ENTITY_TYPE_POST) {
            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);
        }
        

点赞时

  if(entityType == ENTITY_TYPE_POST){
      //计算帖子分数
      String redisKey = RedisKeyUtil.getPostScoreKey();
      redisTemplate.opsForSet().add(redisKey, postId);
  }

设置定时任务

定时任务类:

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("2021-08-01 00:00:00");
        } catch (ParseException e) {
            throw new RuntimeException("初始化日期失败!", e);
        }
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) 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;
        // 分数 = 帖子权重 + 距离天数(天数越大,分数越低)
        //Math.max(w, 1) 防止分数为负数
        //秒->天
        double score = Math.log10(Math.max(w, 1))
                + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);
        // 更新帖子分数
        discussPostService.updateScore(postId, score);
        // 同步es的搜索数据
        post.setScore(score);
        elasticsearchService.saveDiscussPost(post);
    }
}

配置Quartz任务

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

image

在首页按时间和分数展现

之前的mapper默认按时间排,现在修改成两种模式,0安时间排,1按得分排

@Mapper
public interface DiscussPostMapper {
    //userId为0时,表示查询所有用户的帖子,如果不为0,表示查询指定用户的帖子
    //offset表示起始行号,limit表示每页最多显示的行数
    //orderMode表示排序模式,0-默认排序,1-按热度排序
    List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);

    //查询帖子的行数
    //userId为0时,表示查询所有用户的帖子
    int selectDiscussPostRows(@Param("userId") int userId);
    //@param注解用于给参数取别名,拼到sql语句中,如果只有一个参数,并且在<if>标签里,则必须加别名

    int insertDiscussPost(DiscussPost discussPost);

    DiscussPost selectDiscussPostById(int id);
    //根据id查询帖子

    int updateCommentCount(int id, int commentCount);

    //修改帖子类型
    int updateType(int id, int type);

    //修改帖子状态
    int updateStatus(int id, int status);

    //修改帖子分数
    int updateScore(int id, double score);

}

修改xml:

    <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>

重构service:

    public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
        return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
    }

重构homeController(传入orderMode)

package com.newcoder.community.controller;

import com.newcoder.community.entity.DiscussPost;
import com.newcoder.community.entity.Page;
import com.newcoder.community.service.DiscussPostService;
import com.newcoder.community.service.LikeService;
import com.newcoder.community.service.UserService;
import com.newcoder.community.util.CommunityConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Controller
public class HomeController implements CommunityConstant {
    @Autowired
    private UserService userService;

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private LikeService likeService;

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model, Page page, @RequestParam(name="orderMode", defaultValue = "0") int orderMode) {
        //方法调用前,Spring会自动把page注入给model,所以html中可以直接访问page的数据。
        //先查前10个帖子
        page.setRows(discussPostService.findDiscussPostRows( 0));
        page.setPath("/index?orderMode=" + orderMode);

        List<DiscussPost> list = discussPostService.findDiscussPosts(0,page.getOffset(), page.getLimit(), orderMode);
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if(list != null) {
            for (DiscussPost post : list) {
                Map<String, Object> map = new java.util.HashMap<>();
                map.put("post", post);
                map.put("user", userService.findUserById(post.getUserId()));
                //查询帖子的点赞数量
                long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
                map.put("likeCount", likeCount);
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        model.addAttribute("orderMode", orderMode);

        return "/index";
    }

    @RequestMapping(path = "/error", method = RequestMethod.GET)
    public String getErrorPage() {
        return "/error/500";
    }

    // 没有权限时的页面
    @RequestMapping(path = "/denied", method = RequestMethod.GET)
    public String getDeniedPage() {
        return "/error/404";
    }


}

  • @RequestParam注解,参数通过request请求传过来,有默认值。

修改index.html

<!-- 筛选条件 -->
					<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>

生成长图(Deprecated)

image

wkhtmltopdf

将文件上传到云服务器(Deprecated)

上面上一步的长图,工具没调试好,因此也不用。

优化热门帖子列表

缓存:适用于不经常更新的数据(更新缓存不频繁)

image

(避免直接访问数据库,Redis可跨服务器)

多级缓存-redis

image

缓存详细的调用过程:

image

(本地缓存→ redis→ 数据库)

导入依赖

使用Caffeine进行本地缓存

<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

设置参数

# CaffeineProperties
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180

在业务层注入:

@Value("${caffeine.posts.max-size}")
private int maxSize;

@Value("${caffeine.posts.expire-seconds}")
private int expireSeconds;

修改DiscussPostService

配置缓存

//帖子列表缓存
private LoadingCache<String, List<DiscussPost>> postListCache;

//帖子总数缓存
private LoadingCache<Integer, Integer> postRowsCache;



缓存写

在构造函数执行之前:

@PostConstruct
    public void init() {
        //初始化帖子列表缓存
        postListCache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
                .build(new CacheLoader<String, List<DiscussPost>>() {
                           @Override
                           public @Nullable List<DiscussPost> load(String key) throws Exception {
                               if (key == null || key.length() == 0)
                                   throw new IllegalArgumentException("参数错误!");
                               String[] params = key.split(":");
                               if (params == null || params.length != 2)
                                   throw new IllegalArgumentException("参数错误!");
                               int offset = Integer.valueOf(params[0]);
                               int limit = Integer.valueOf(params[1]);

                               //TODO: 二级缓存:Redis -> mysql

                               logger.debug("load post list from DB.");
                               return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
                           }
                       }
                );

        //初始化帖子总数缓存
        postRowsCache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable Integer load(Integer integer) throws Exception {
                        logger.debug("load post rows from DB.");
                        return discussPostMapper.selectDiscussPostRows(0);
                    }
                });
    }

缓存读

public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
      //只有首页按照热门排序才会缓存
      //key是offset:limit
      //get方法是从缓存中取数据
      if (userId == 0 && orderMode == 1) {
          return postListCache.get(offset + ":" + limit);
      }

      //不缓存
      logger.debug("load post list from DB.");

      return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
  }

  public int findDiscussPostRows(int userId) {
      if (userId == 0) {
          return postRowsCache.get(userId);
      }
      //不缓存
      logger.debug("load post rows from DB.");

      return discussPostMapper.selectDiscussPostRows(userId);
  }

首先在测试类中创建函数插入100000w条帖子:

//创建10w条数据进行压力测试
    @Test
    public void initDataForTest() {
        for(int i = 0; i < 100000; i++) {
            DiscussPost post = new DiscussPost();
            post.setUserId(111);
            post.setTitle("互联网寒冬");
            post.setContent("今年的互联网寒冬真是太冷了。");
            post.setCreateTime(new Date());
            post.setScore(Math.random() * 2000);
            discussPostService.addDiscussPost(post);


        }
    }

image

测试代码

 @Test
    public void testCache() {
        //第一次查询,应该从数据库中查
        System.out.println(discussPostService.findDiscussPosts(0, 0, 10, 1));
        //第二次查询,应该从缓存中查
        System.out.println(discussPostService.findDiscussPosts(0, 0, 10, 1));
        //第三次查询,应该从缓存中查
        System.out.println(discussPostService.findDiscussPosts(0, 0, 10, 1));

        //第三次查询,应该从数据库中查
        System.out.println(discussPostService.findDiscussPosts(0, 0, 10, 0));
    }

image

(前三次查询,日志只打印了一次,说明只差了一次数据库)

压力测试

使用JMeter进行测试,下载地址:

https://jmeter.apache.org/download_jmeter.cgi

下载后到bin目录下,运行

sh jmeter.sh

出来GUI界面,依次配置测试计划、线程组、HTTP请求,在聚合报告里看:

image

image

不加缓存

注释掉ServiceLogAspect的注解,可以干净日志

image

100个线程,吞吐量约12.5

加缓存

image

直接191.7,增加了十几倍

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值