牛客网项目——项目开发(十):热帖排行,生成长图,上传云服务器,性能优化


在这里插入图片描述

1. 热帖排行

1.1 统计发生分数变化帖子

把一段时间内分数发生变化的帖子放入一个set

1.1.1 RedisKeyUtil

  1. 定义前缀
private static final String PREFIX_POST = "post";
  1. 添加方法统计帖子分数
// 帖子分数
public static String getPostScoreKey() {
    return PREFIX_POST + SPLIT + "score";
}

1.1.2 DiscussPostController

  1. addDiscussPost新增帖子时做处理,把新增的帖子放入redis,不重复,无序,存入set
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());
  1. 置顶不做处理,直接加到最上面
  2. setWonderful加精做处理
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, id);

1.1.3 CommentController

addComment添加评论时做处理

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

1.1.4 LikeController

处理逻辑同上

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

1.2 定时任务

1.2.1 PostScoreRefreshJob

  1. 打日志,养成习惯
  2. 注入redis,DiscussPostService,LikeService,ElasticsearchService
  3. 声明静态常量牛客纪元private static final Date epoch;
  4. 重写execute
    1. 取到上一步存的rediskey
    2. 如果没有数据变化,打个日志任务取消
    3. 否则开始刷新前刷新后都记日志
    4. 遍历刷新
  5. 刷新方法
    1. 得到帖子id
    2. 如果帖子不在了打个日志
    3. 更新帖子分数,同步es搜索数据(在相应的Mapper和Config里添加响应方法)
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("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("[任务取消] 没有需要刷新的帖子!");
            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;
        // 分数 = 帖子权重 + 距离天数
        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);
    }

}

1.2.2 QuartzConfig

  1. JobDetailFactoryBean
  2. SimpleTriggerFactoryBean
// 刷新帖子分数任务
@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);
    factoryBean.setJobDataMap(new JobDataMap());
    return factoryBean;
}

1.3 页面展现

1.3.1 DiscussPostMapper

添加参数 orderMode,默认值是0按照原先的排,如果1就按照热度排

List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);

1.3.2 discusspost-mapper.xml

完成下面修改后通过finduse把用到这个方法的地方都改一下

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

1.3.3 HomeController

  1. 添加参数ordermode
  2. 第一次访问,还没有值,所以需要默认参数是0
  3. 在路径上拼上参数page.setPath("/index?orderMode=" + orderMode);
  4. 最后ordermode再装到模板,模板要用
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model, Page page,
                           @RequestParam(name = "orderMode", defaultValue = "0") int orderMode) {
    // 方法调用钱,SpringMVC会自动实例化Model和Page,并将Page注入Model.
    // 所以,在thymeleaf中可以直接访问Page对象中的数据.
    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 HashMap<>();
            map.put("post", post);
            User user = userService.findUserById(post.getUserId());
            map.put("user", user);

            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";
}

1.3.4 index.html

  1. 修改超链接th:href="@{/index(orderMode=0)}"
  2. 动态显示页签th:class="|nav-link ${orderMode==0?'active':''}|"
<!-- 筛选条件 -->
<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>

2. 生成长图

在这里插入图片描述

2.1 固定命令调用

package com.nowcoder.community;

import java.io.IOException;

public class WkTests {

    public static void main(String[] args) {
        String cmd = "d:/work/wkhtmltopdf/bin/wkhtmltoimage --quality 75  https://www.nowcoder.com d:/work/data/wk-images/3.png";
        try {
            Runtime.getRuntime().exec(cmd);
            System.out.println("ok.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

2.2 application.properties

配置文件定义命令和存放目录

# wk
wk.image.command=d:/work/wkhtmltopdf/bin/wkhtmltoimage
wk.image.storage=d:/work/data/wk-images

2.3 WkConfig

在服务启动时检查目录是否存在否则创建

package com.nowcoder.community.config;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.io.File;

@Configuration
public class WkConfig {

    private static final Logger logger = LoggerFactory.getLogger(WkConfig.class);

    @Value("${wk.image.storage}")
    private String wkImageStorage;

    @PostConstruct
    public void init() {
        // 创建WK图片目录
        File file = new File(wkImageStorage);
        if (!file.exists()) {
            file.mkdir();
            logger.info("创建WK图片目录: " + wkImageStorage);
        }
    }

}

2.4 模拟开发分享功能

2.4.1 ShareController.share

  1. logger
  2. 异步方式,注入kafka生产者
  3. 注入域名项目访问名,图片存放位置,
  4. share方法
    1. 传入参数为路径
    2. 文件名UUID随机生成
    3. 构建一个event,异步生成长图
    4. 返回访问路径
@RequestMapping(path = "/share", method = RequestMethod.GET)
@ResponseBody
public String share(String htmlUrl) {
    // 文件名
    String fileName = CommunityUtil.generateUUID();

    // 异步生成长图
    Event event = new Event()
            .setTopic(TOPIC_SHARE)
            .setData("htmlUrl", htmlUrl)
            .setData("fileName", fileName)
            .setData("suffix", ".png");
    eventProducer.fireEvent(event);

    // 返回访问路径
    map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);

    return CommunityUtil.getJSONString(0, null, map);
}

2.4.2 EventConsumer

// 消费分享事件
@KafkaListener(topics = TOPIC_SHARE)
public void handleShareMessage(ConsumerRecord record) {
    if (record == null || record.value() == null) {
        logger.error("消息的内容为空!");
        return;
    }

    Event event = JSONObject.parseObject(record.value().toString(), Event.class);
    if (event == null) {
        logger.error("消息格式错误!");
        return;
    }

    String htmlUrl = (String) event.getData().get("htmlUrl");
    String fileName = (String) event.getData().get("fileName");
    String suffix = (String) event.getData().get("suffix");

    String cmd = wkImageCommand + " --quality 75 "
            + htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;
    try {
        Runtime.getRuntime().exec(cmd);
        logger.info("生成长图成功: " + cmd);
    } catch (IOException e) {
        logger.error("生成长图失败: " + e.getMessage());
    }

    // 启用定时器,监视该图片,一旦生成了,则上传至七牛云.
    UploadTask task = new UploadTask(fileName, suffix);
    Future future = taskScheduler.scheduleAtFixedRate(task, 500);
    task.setFuture(future);
}

2.4.3 ShareController.getShareImage

// 废弃
// 获取长图
@RequestMapping(path = "/share/image/{fileName}", method = RequestMethod.GET)
public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response) {
    if (StringUtils.isBlank(fileName)) {
        throw new IllegalArgumentException("文件名不能为空!");
    }

    response.setContentType("image/png");
    File file = new File(wkImageStorage + "/" + fileName + ".png");
    try {
        OutputStream os = response.getOutputStream();
        FileInputStream fis = new FileInputStream(file);
        byte[] buffer = new byte[1024];
        int b = 0;
        while ((b = fis.read(buffer)) != -1) {
            os.write(buffer, 0, b);
        }
    } catch (IOException e) {
        logger.error("获取长图失败: " + e.getMessage());
    }
}

3. 将文件上传至云服务器

在这里插入图片描述

3.1 引入依赖

<dependency>
	<groupId>com.qiniu</groupId>
	<artifactId>qiniu-java-sdk</artifactId>
	<version>7.2.23</version>
</dependency>

3.2 配置

  1. 密钥
  2. 空间名字
# qiniu
qiniu.key.access=6RA-Uus95ZT_1znMrCMD8BpqfjT-K7OKmQTfKB48
qiniu.key.secret=kPNnLFz2_tzztKUVpSLm0lYngtuHWyIq5LzTmLIL
qiniu.bucket.header.name=community_header
quniu.bucket.header.url=http://pvghrij81.bkt.clouddn.com
qiniu.bucket.share.name=community_share
qiniu.bucket.share.url=http://pvghvvuzm.bkt.clouddn.com

3.3 客户端上传:上传头像

3.3.1 UserController

  1. 注入属性
  2. 废弃原有uploadHeadergetHeader方法
  3. getSettingPage
    1. 上传文件名称
    2. 设置响应信息,存入StringMap对象
    3. 生成上传凭证
    4. 数据存入model
  4. updateHeaderUrl,更新头像路径
@Value("${qiniu.key.access}")
private String accessKey;

@Value("${qiniu.key.secret}")
private String secretKey;

@Value("${qiniu.bucket.header.name}")
private String headerBucketName;

@Value("${quniu.bucket.header.url}")
private String headerBucketUrl;

@LoginRequired
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage(Model model) {
    // 上传文件名称
    String fileName = CommunityUtil.generateUUID();
    // 设置响应信息
    StringMap policy = new StringMap();
    policy.put("returnBody", CommunityUtil.getJSONString(0));
    // 生成上传凭证
    Auth auth = Auth.create(accessKey, secretKey);
    String uploadToken = auth.uploadToken(headerBucketName, fileName, 3600, policy);

    model.addAttribute("uploadToken", uploadToken);
    model.addAttribute("fileName", fileName);

    return "/site/setting";
}

// 更新头像路径
@RequestMapping(path = "/header/url", method = RequestMethod.POST)
@ResponseBody
public String updateHeaderUrl(String fileName) {
    if (StringUtils.isBlank(fileName)) {
        return CommunityUtil.getJSONString(1, "文件名不能为空!");
    }

    String url = headerBucketUrl + "/" + fileName;
    userService.updateHeader(hostHolder.getUser().getId(), url);

    return CommunityUtil.getJSONString(0);
}

3.3.2 setting.html

  1. 为了提交js添加idid="uploadForm"
  2. 补充两个hidden,uploadTokenfileName
<!--上传到七牛云-->
<form class="mt-5" id="uploadForm">
	<div class="form-group row mt-4">
		<label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label>
		<div class="col-sm-10">
			<div class="custom-file">
				<input type="hidden" name="token" th:value="${uploadToken}">
				<input type="hidden" name="key" th:value="${fileName}">
				<input type="file" class="custom-file-input" id="head-image" name="file" lang="es" required="">
				<label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label>
				<div class="invalid-feedback">
					该账号不存在!
				</div>
			</div>
		</div>
	</div>
	<div class="form-group row mt-4">
		<div class="col-sm-2"></div>
		<div class="col-sm-10 text-center">
			<button type="submit" class="btn btn-info text-white form-control">立即上传</button>
		</div>
	</div>
</form>

<script th:src="@{/js/setting.js}"></script>

3.3.3 setting.js

  1. 页面加载完后给form定义事件,点击按钮触发upload
  2. upload()最后return false,事件到此为止
  3. upload() 使用$.ajax()功能完全
    1. url
    2. 提交方式post
    3. processData: false不转成字符串
    4. contentType: false
    5. data封装表单对象
$(function(){
    $("#uploadForm").submit(upload);
});

function upload() {
    $.ajax({
        url: "http://upload-z1.qiniup.com",
        method: "post",
        processData: false,
        contentType: false,
        data: new FormData($("#uploadForm")[0]),
        success: function(data) {
            if(data && data.code == 0) {
                // 更新头像访问路径
                $.post(
                    CONTEXT_PATH + "/user/header/url",
                    {"fileName":$("input[name='key']").val()},
                    function(data) {
                        data = $.parseJSON(data);
                        if(data.code == 0) {
                            window.location.reload();
                        } else {
                            alert(data.msg);
                        }
                    }
                );
            } else {
                alert("上传失败!");
            }
        }
    });
    return false;
}

3.4 服务端直传:上传分享图片

3.4.1 ShareController

访问路径更改

@Value("${qiniu.bucket.share.url}")
private String shareBucketUrl;

@RequestMapping(path = "/share", method = RequestMethod.GET)
@ResponseBody
public String share(String htmlUrl) {
    // 文件名
    String fileName = CommunityUtil.generateUUID();

    // 异步生成长图
    Event event = new Event()
            .setTopic(TOPIC_SHARE)
            .setData("htmlUrl", htmlUrl)
            .setData("fileName", fileName)
            .setData("suffix", ".png");
    eventProducer.fireEvent(event);

    // 返回访问路径
    Map<String, Object> map = new HashMap<>();
//        map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);
    map.put("shareUrl", shareBucketUrl + "/" + fileName);

    return CommunityUtil.getJSONString(0, null, map);
}

3.4.2 handleShareMessage

启用定时器,监视该图片,一旦生成了,则上传至七牛云.

UploadTask方法:

  1. 启动任务的返回值Future可以用来停止
  2. 超时或超次数就失败
// 消费分享事件
@KafkaListener(topics = TOPIC_SHARE)
public void handleShareMessage(ConsumerRecord record) {
    if (record == null || record.value() == null) {
        logger.error("消息的内容为空!");
        return;
    }

    Event event = JSONObject.parseObject(record.value().toString(), Event.class);
    if (event == null) {
        logger.error("消息格式错误!");
        return;
    }

    String htmlUrl = (String) event.getData().get("htmlUrl");
    String fileName = (String) event.getData().get("fileName");
    String suffix = (String) event.getData().get("suffix");

    String cmd = wkImageCommand + " --quality 75 "
            + htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;
    try {
        Runtime.getRuntime().exec(cmd);
        logger.info("生成长图成功: " + cmd);
    } catch (IOException e) {
        logger.error("生成长图失败: " + e.getMessage());
    }

    // 启用定时器,监视该图片,一旦生成了,则上传至七牛云.
    UploadTask task = new UploadTask(fileName, suffix);
    Future future = taskScheduler.scheduleAtFixedRate(task, 500);
    task.setFuture(future);
}


class UploadTask implements Runnable {
    // 文件名称
    private String fileName;
    // 文件后缀
    private String suffix;
    // 启动任务的返回值
    private Future future;
    // 开始时间
    private long startTime;
    // 上传次数
    private int uploadTimes;

    public UploadTask(String fileName, String suffix) {
        this.fileName = fileName;
        this.suffix = suffix;
        this.startTime = System.currentTimeMillis();
    }

    public void setFuture(Future future) {
        this.future = future;
    }

    @Override
    public void run() {
        // 生成失败
        if (System.currentTimeMillis() - startTime > 30000) {
            logger.error("执行时间过长,终止任务:" + fileName);
            future.cancel(true);
            return;
        }
        // 上传失败
        if (uploadTimes >= 3) {
            logger.error("上传次数过多,终止任务:" + fileName);
            future.cancel(true);
            return;
        }

        String path = wkImageStorage + "/" + fileName + suffix;
        File file = new File(path);
        if (file.exists()) {
            logger.info(String.format("开始第%d次上传[%s].", ++uploadTimes, fileName));
            // 设置响应信息
            StringMap policy = new StringMap();
            policy.put("returnBody", CommunityUtil.getJSONString(0));
            // 生成上传凭证
            Auth auth = Auth.create(accessKey, secretKey);
            String uploadToken = auth.uploadToken(shareBucketName, fileName, 3600, policy);
            // 指定上传机房
            UploadManager manager = new UploadManager(new Configuration(Zone.zone1()));
            try {
                // 开始上传图片
                Response response = manager.put(
                        path, fileName, uploadToken, null, "image/" + suffix, false);
                // 处理响应结果
                JSONObject json = JSONObject.parseObject(response.bodyString());
                if (json == null || json.get("code") == null || !json.get("code").toString().equals("0")) {
                    logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));
                } else {
                    logger.info(String.format("第%d次上传成功[%s].", uploadTimes, fileName));
                    future.cancel(true);
                }
            } catch (QiniuException e) {
                logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));
            }
        } else {
            logger.info("等待图片生成[" + fileName + "].");
        }
    }
}

4. 性能优化

在这里插入图片描述

4.1 本地缓存和分布式缓存

本地缓存性能最好
分布式缓存比本地缓存性能低一点,主要是因为网络开销上
在这里插入图片描述
使用本地缓存,如果存的时用户信息相关数据,如果app从缓存中取用户相关数据没有,就无法登录。如果时热门帖子等信息,使用本地缓存没事,用户和数据本身不是有强关联可以本地缓存。
在这里插入图片描述
使用redis进行缓存就没有这种情况。redis可以跨服务器

4.2 多级缓存

在这里插入图片描述

4.3 优化热门帖子列表

spring整合缓存用一个缓存管理器管理所有缓存,不合适,不如单独使用一个缓存

4.3.1 pom.xml

<dependency>
	<groupId>com.github.ben-manes.caffeine</groupId>
	<artifactId>caffeine</artifactId>
	<version>2.7.0</version>
</dependency>

4.3.2 application.properties

  1. max-size:缓存空间缓存数量
  2. expire-seconds:过期时间,数据变化淘汰不合适,因为缓存的是一整页,有一个变了都淘汰不合适
# caffeine
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180

4.3.4 DiscussPostService

  1. 注入日志和参数
  2. LoadingCache:同步缓存,AsyncLoadingCache:异步缓存
  3. 声明帖子列表缓存和帖子总数缓存
  4. 初始化缓存
    1. .newBuilder()初始化
    2. .maximumSize最大缓存数
    3. .expireAfterWrite过期时间
    4. new CacheLoader接口的匿名实现
      1. load是缓存中没有的查询方法
      2. 如果key或长为0抛异常
      3. 切割参数得到两个数据
      4. (可以先访问redis,如果redis没有再访问数据库)
      5. 使用discussPostMapper中的查询方法查询
  5. 修改方法findDiscussPosts,如果是第一页且按热度排行就调取咖啡因缓存
  6. 修改方法findDiscussPostRows,如果首页访问就调取咖啡因
@PostConstruct
public void init() {
    // 初始化帖子列表缓存
    postListCache = Caffeine.newBuilder()
            .maximumSize(maxSize)
            .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
            .build(new CacheLoader<String, List<DiscussPost>>() {
                @Nullable
                @Override
                public List<DiscussPost> load(@NonNull 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]);

                    // 二级缓存: 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>() {
                @Nullable
                @Override
                public Integer load(@NonNull Integer key) throws Exception {
                    logger.debug("load post rows from DB.");
                    return discussPostMapper.selectDiscussPostRows(key);
                }
            });
}

4.3.5 CaffeineTests

  1. 放30万条数据进数据库,方便压力测试
  2. 三次访问热门帖子列表,应该只打印一次日志
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class CaffeineTests {

    @Autowired
    private DiscussPostService postService;

    @Test
    public void initDataForTest() {
        for (int i = 0; i < 300000; i++) {
            DiscussPost post = new DiscussPost();
            post.setUserId(111);
            post.setTitle("互联网求职暖春计划");
            post.setContent("今年的就业形势,确实不容乐观。过了个年,仿佛跳水一般,整个讨论区哀鸿遍野!19届真的没人要了吗?!18届被优化真的没有出路了吗?!大家的“哀嚎”与“悲惨遭遇”牵动了每日潜伏于讨论区的牛客小哥哥小姐姐们的心,于是牛客决定:是时候为大家做点什么了!为了帮助大家度过“寒冬”,牛客网特别联合60+家企业,开启互联网求职暖春计划,面向18届&19届,拯救0 offer!");
            post.setCreateTime(new Date());
            post.setScore(Math.random() * 2000);
            postService.addDiscussPost(post);
        }
    }

    @Test
    public void testCache() {
        System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
        System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
        System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
        System.out.println(postService.findDiscussPosts(0, 0, 10, 0));
    }

}

4.3.6 jmeter压力测试

在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平什么阿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值