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,增加了十几倍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/593685.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【解决】docker一键部署报错

项目场景见&#xff1a;【记录】Springboot项目集成docker实现一键部署-CSDN博客 问题&#xff1a; 1.docker images 有tag为none的镜像存在。 2.有同事反馈&#xff0c;第一次启动docker-compose up -d 项目无法正常启动。后续正常。 原因&#xff1a; 1.服务中指定了镜像m…

mqtt上行数据传送

{"id": "123","version": "1.0","params": {"wendu": {"value": 25.0},"humi": {"value": 23.6}} } 不要time!!!!!!!!!!!!!!!!!!!!!!!!!!! 下面是官方文档的代码&#xff0c;我用…

自制RAG工具:docx文档读取工具

自制RAG工具&#xff1a;docx文档读取工具 1. 介绍2. 源码2.1 chunk2.2 DocReader 3. 使用方法3.1 文档格式设置3.2 代码使用方法 1. 介绍 在RAG相关的工作中&#xff0c;经常会用到读取docx文档的功能&#xff0c;为了更好地管理文档中的各个分块&#xff0c;以提供更高质量的…

伺服电机初识

目录 一、伺服电机的介绍二、伺服电机的基本原理三、伺服电机的技术特点四、伺服电机的分类五、实际产品介绍1、基本技术规格&#xff1a;2、MD42电机硬件接口3、通讯协议介绍3.1 通讯控制速度运行3.2 通讯控制位置运行3.3 通讯控制转矩运行 4、状态灯与报警信息 一、伺服电机的…

MyScaleDB:SQL+向量驱动大模型和大数据新范式

大模型和 AI 数据库双剑合璧&#xff0c;成为大模型降本增效&#xff0c;大数据真正智能的制胜法宝。 大模型&#xff08;LLM&#xff09;的浪潮已经涌动一年多了&#xff0c;尤其是以 GPT-4、Gemini-1.5、Claude-3 等为代表的模型你方唱罢我登场&#xff0c;成为当之无愧的风口…

富文本编辑器CKEditor4简单使用-07(处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题)

富文本编辑器CKEditor4简单使用-07&#xff08;处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题&#xff09; 1. 前言——CKEditor4快速入门2. 默认情况下的粘贴2.1 先看控制粘贴的3个按钮2.1.1 工具栏粘贴按钮2.1.2 存在的问题 2.2 不解决按钮问题的情况下2.2.1 使用ct…

Linux——基础IO2

引入 之前在Linux——基础IO(1)中我们讲的都是(进程打开的文件)被打开的文件 那些未被打开的文件呢&#xff1f; 大部分的文件都是没有被打开的文件&#xff0c;这些文件在哪保存&#xff1f;磁盘(SSD) OS要不要管理磁盘上的文件&#xff1f;(如何让OS快速定位一个文件) 要…

设计模式之拦截过滤器模式

想象一下&#xff0c;在你的Java应用里&#xff0c;每个请求就像一场冒险旅程&#xff0c;途中需要经过层层安检和特殊处理。这时候&#xff0c;拦截过滤器模式就化身为你最可靠的特工团队&#xff0c;悄无声息地为每一个请求保驾护航&#xff0c;确保它们安全、高效地到达目的…

Endnote X9 20 21如何把中文引用的et al 换(变)成 等

描述 随着毕业的临近&#xff0c;我在写论文时可能会遇到在引用的中文参考文献中出现“et al”字样。有的学校事比较多&#xff0c;非让改成等等&#xff0c;这就麻烦了。 本身人家endnote都是老美的软件&#xff0c;人家本身就是针对英文文献&#xff0c;你现在让改成等等&a…

JavaScript的操作符运算符

前言&#xff1a; JavaScript的运算符与C/C一致 算数运算符&#xff1a; 算数运算符说明加-减*乘%除/取余 递增递减运算符&#xff1a; 运算符说明递增1-- 递减1 补充&#xff1a; 令a1&#xff0c;b1 运算a b ab12ab22ab--10a--b00 比较(关系)运算符&#xff1a; 运算…

【ChatGPT with Date】使用 ChatGPT 时显示消息时间的插件

文章目录 1. 介绍2. 使用方法2.1 安装 Tampermonkey2.2 安装脚本2.3 使用 3. 配置3.1 时间格式3.2 时间位置3.3 高级配置(1) 生命周期钩子函数(2) 示例 4. 反馈5. 未来计划6. 开源协议7. 供给开发者自定义修改脚本的文档7.1 项目组织架构7.2 定义新的 Component(1) 定义一个新的…

提示找不到msvcr110.dll怎么办,分享多种靠谱的解决方法

当用户在操作计算机时遇到系统提示“找不到msvcr110.dll&#xff0c;无法继续执行代码”这一错误信息&#xff0c;这个问题会导致软件无法启动运行。本文将介绍计算机找不到msvcr110.dll的5种详细的解决方法&#xff0c;帮助读者解决这个问题。 一&#xff0c;关于msvcr110.dll…

《十六》QT TCP协议工作原理和实战

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍如何运用QTcpSocket组件实现基于TCP的网络通信…

论文| Where Is Your Place, Visual Place Recognition?

论文| Where Is Your Place, Visual Place Recognition&#xff1f;

1.pytorch加载收数据(B站小土堆)

数据的加载主要有两个函数&#xff1a; 1.dataset整体收集数据&#xff1a;提供一种方法去获取数据及其label&#xff0c;告诉我们一共有多少数据&#xff08;就是自开始把要的数据和标签都收进来&#xff09; 2.dataloader&#xff0c;后面传入模型时候&#xff0c;每次录入数…

某站戴师兄——Excel学习笔记

1、拿到源数据第一件事——备份工作表&#xff0c;隐藏 Ctrlshift键L打开筛选 UV (Unique visitor)去重 是指通过互联网访问、浏览这个网页的自然人。访问网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。 PV …

【C++】详解STL的容器之一:list

目录 简介 初识list 模型 list容器的优缺点 list的迭代器 常用接口介绍 获取迭代器 begin end empty size front back insert push_front pop_front push_back pop_back clear 源代码思路 节点设计 迭代器的设计 list的设计 begin() end() 空构造 ins…

【编程题-错题集】chika 和蜜柑(排序 / topK)

牛客对于题目链接&#xff1a;chika和蜜柑 (nowcoder.com) 一、分析题目 排序 &#xff1a;将每个橘⼦按照甜度由高到低排序&#xff0c;相同甜度的橘子按照酸度由低到高排序&#xff0c; 然后提取排序后的前 k 个橘子就好了。 二、代码 1、看题解之前AC的代码 #include <…

企业计算机服务器中了halo勒索病毒怎么处理,halo勒索病毒解密流程

随着网络技术的不断发展&#xff0c;网络在企业生产运营过程中发挥着重大作用&#xff0c;很多企业利用网络开展各项工作业务&#xff0c;网络也大大提高了企业的生产效率&#xff0c;但随之而来的网络数据安全问题成为众多企业关心的主要话题。近日&#xff0c;云天数据恢复中…

机械臂标准DH建模及正运动学分析(以IRB4600型工业机械臂为例)

1. 前言 对于工业机械臂而言&#xff0c;运动学是不考虑力学特性的情况下对机械臂的几何参数与其位置、速度、加速度等运动特性的关系研究。DH建模是运动学的基础&#xff0c;全称为Denavit-Hartenberg建模方法&#xff0c;是一种广泛应用于机器人运动学中的建模技术。该方法通…