牛客网项目——项目开发(十):热帖排行,生成长图,上传云服务器,性能优化
文章目录
- 1. 热帖排行
- 1.1 统计发生分数变化帖子
- 1.1.1 RedisKeyUtil
- 1.1.2 DiscussPostController
- 1.1.3 CommentController
- 1.1.4 LikeController
- 1.2 定时任务
- 1.2.1 PostScoreRefreshJob
- 1.2.2 QuartzConfig
- 1.3 页面展现
- 1.3.1 DiscussPostMapper
- 1.3.2 discusspost-mapper.xml
- 1.3.3 HomeController
- 1.3.4 index.html
- 2. 生成长图
- 2.1 固定命令调用
- 2.2 application.properties
- 2.3 WkConfig
- 2.4 模拟开发分享功能
- 2.4.1 ShareController.share
- 2.4.2 EventConsumer
- 2.4.3 ShareController.getShareImage
- 3. 将文件上传至云服务器
- 3.1 引入依赖
- 3.2 配置
- 3.3 客户端上传:上传头像
- 3.3.1 UserController
- 3.3.2 setting.html
- 3.3.3 setting.js
- 3.4 服务端直传:上传分享图片
- 3.4.1 ShareController
- 3.4.2 handleShareMessage
- 4. 性能优化
- 4.1 本地缓存和分布式缓存
- 4.2 多级缓存
- 4.3 优化热门帖子列表
- 4.3.1 pom.xml
- 4.3.2 application.properties
- 4.3.4 DiscussPostService
- 4.3.5 CaffeineTests
- 4.3.6 jmeter压力测试
1. 热帖排行
1.1 统计发生分数变化帖子
把一段时间内分数发生变化的帖子放入一个set
1.1.1 RedisKeyUtil
- 定义前缀
private static final String PREFIX_POST = "post";
- 添加方法统计帖子分数
// 帖子分数
public static String getPostScoreKey() {return PREFIX_POST + SPLIT + "score";
}
1.1.2 DiscussPostController
addDiscussPost
新增帖子时做处理,把新增的帖子放入redis,不重复,无序,存入set
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());
- 置顶不做处理,直接加到最上面
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
- 打日志,养成习惯
- 注入redis,DiscussPostService,LikeService,ElasticsearchService
- 声明静态常量牛客纪元
private static final Date epoch;
- 重写execute
- 取到上一步存的rediskey
- 如果没有数据变化,打个日志任务取消
- 否则开始刷新前刷新后都记日志
- 遍历刷新
- 刷新方法
- 得到帖子id
- 如果帖子不在了打个日志
- 更新帖子分数,同步es搜索数据(在相应的Mapper和Config里添加响应方法)
public class PostScoreRefreshJob implements Job, CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate LikeService likeService;@Autowiredprivate 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);}}@Overridepublic 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
- JobDetailFactoryBean
- 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_postwhere 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
- 添加参数ordermode
- 第一次访问,还没有值,所以需要默认参数是0
- 在路径上拼上参数
page.setPath("/index?orderMode=" + orderMode);
- 最后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
- 修改超链接
th:href="@{/index(orderMode=0)}"
- 动态显示页签
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;@PostConstructpublic void init() {// 创建WK图片目录File file = new File(wkImageStorage);if (!file.exists()) {file.mkdir();logger.info("创建WK图片目录: " + wkImageStorage);}}}
2.4 模拟开发分享功能
2.4.1 ShareController.share
- logger
- 异步方式,注入kafka生产者
- 注入域名项目访问名,图片存放位置,
- share方法
- 传入参数为路径
- 文件名UUID随机生成
- 构建一个event,异步生成长图
- 返回访问路径
@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 配置
- 密钥
- 空间名字
# 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
- 注入属性
- 废弃原有
uploadHeader
和getHeader
方法 getSettingPage
- 上传文件名称
- 设置响应信息,存入
StringMap
对象 - 生成上传凭证
- 数据存入
model
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
- 为了提交js添加id
id="uploadForm"
- 补充两个hidden,
uploadToken
和fileName
<!--上传到七牛云-->
<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
- 页面加载完后给form定义事件,点击按钮触发
upload
upload()
最后return false
,事件到此为止upload()
使用$.ajax()
功能完全- url
- 提交方式post
processData: false
不转成字符串contentType: false
- 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方法:
- 启动任务的返回值
Future
可以用来停止 - 超时或超次数就失败
// 消费分享事件
@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;}@Overridepublic 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
max-size
:缓存空间缓存数量expire-seconds
:过期时间,数据变化淘汰不合适,因为缓存的是一整页,有一个变了都淘汰不合适
# caffeine
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180
4.3.4 DiscussPostService
- 注入日志和参数
LoadingCache
:同步缓存,AsyncLoadingCache
:异步缓存- 声明帖子列表缓存和帖子总数缓存
- 初始化缓存
.newBuilder()
初始化.maximumSize
最大缓存数.expireAfterWrite
过期时间new CacheLoader
接口的匿名实现load
是缓存中没有的查询方法- 如果key或长为0抛异常
- 切割参数得到两个数据
- (可以先访问redis,如果redis没有再访问数据库)
- 使用
discussPostMapper
中的查询方法查询
- 修改方法
findDiscussPosts
,如果是第一页且按热度排行就调取咖啡因缓存 - 修改方法
findDiscussPostRows
,如果首页访问就调取咖啡因
@PostConstruct
public void init() {// 初始化帖子列表缓存postListCache = Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(expireSeconds, TimeUnit.SECONDS).build(new CacheLoader<String, List<DiscussPost>>() {@Nullable@Overridepublic 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 -> mysqllogger.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@Overridepublic Integer load(@NonNull Integer key) throws Exception {logger.debug("load post rows from DB.");return discussPostMapper.selectDiscussPostRows(key);}});
}
4.3.5 CaffeineTests
- 放30万条数据进数据库,方便压力测试
- 三次访问热门帖子列表,应该只打印一次日志
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class CaffeineTests {@Autowiredprivate DiscussPostService postService;@Testpublic 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);}}@Testpublic 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压力测试
牛客网项目——项目开发(十):热帖排行,生成长图,上传云服务器,性能优化相关推荐
- Java牛客网社区项目——知识点面试题
Java牛客网社区项目--知识点&面试题 持续更新中(ง •̀_•́)ง 文章目录 Java牛客网社区项目--知识点&面试题 请简要介绍一下你的项目? 什么是Spring框架? 对Sp ...
- 2019牛客网高级项目
本项目是一个基于SpringBoot的社区平台,实现了牛客网讨论区的功能.实现了邮箱注册.验证码登录.发帖.评论.私信.点赞.关注.统计网站访问次数等功能,数据库使用Mybatis.Redis,使用K ...
- 仿牛客网社区项目 全栈总结
学习仿牛客网社区项目 代码&资源 各章节总结 第一章 第二章 第三章 第四章 第五章 第六章 第七章 第八章 争取让每个知识点都有链接可点 项目总结 网站架构图 常见面试题 MySQL Red ...
- webpack项目上传云服务器,webpack项目上传云服务器
webpack项目上传云服务器 内容精选 换一换 为了避免不必要的费用产生,完成本示例体验后,可释放以下资源.资源释放后无法恢复,请谨慎操作.项目是使用DevCloud各服务的基础,删除项目可将该项目 ...
- 怎样把项目上传服务器,怎样将项目上传云服务器
怎样将项目上传云服务器 内容精选 换一换 本节操作介绍本地Linux操作系统主机通过SCP向Linux云服务器传输文件的操作步骤.登录管理控制台,在ECS列表页面记录待上传文件的云服务器的弹性公网IP ...
- 牛客网中级项目学习笔记(一)
牛客中级项目学习: Controller 解析web请求 Service 业务层 DAO(data access object)数据处理层 database 底层数据库 重定向 代码如下: @Requ ...
- SSM项目上传云服务器
1.购买服务器 阿里云新人免费领取网站 腾讯云新人免费领取网站 领取后在控制台进行操作,可以发现公网ip地址与重置密码,之后记住修改服务器安全组规则,打开8080端口 2.连接服务器 开始菜单输入ms ...
- 2022-1-13牛客网C++项目—— 第二章 Linux 多进程开发(一)
复习用的问题 进程和程序之间的关系是什么? 进程包含了哪些信息? 一.程序当中包含了一系列的信息,这些信息用于描述如何创建一个进程. 1)二进制格式标识:描述文件的格式,内核根据这个信息来解释文件中的 ...
- 牛客网实战项目详细到每一步(更新中)
一技术架构 Spring Boot Spring Spring MVC MyBatics Redis Kafka Elasticsearch重点的提高性能的技术 Spring Security, Sp ...
- [牛客网中级项目]第四章用户注册登陆管理
目录 1. 预习 1.1 拦截器: 1.2 MD5加密算法: 2. 内容: 3. 注册: 3.1 注册要实现的功能: 3.2 代码实现: 3.2.1 建立LoginCotroller.class 3. ...
最新文章
- pandas计算滑动窗口中的数值总和实战(Rolling Sum of a Pandas Column):计算单数据列滑动窗口中的数值总和(sum)、计算多数据列滑动窗口中的数值总和(sum)
- selenium2使用记录
- umi搭建react+antd项目(一)环境配置
- Windows核心编程 第七章 线程的调度、优先级和亲缘性(上)
- 空间句法软件_【特训营2:空间句法高阶班】GIS中的空间句法运用 丨城市数据派...
- DTO数据传输对象详解
- Apache Flink 零基础入门(一):基础概念解析
- 6,synchronized, lock 区别
- CV2摄像头人脸、人眼、微笑等检测
- 异步api_如何设计无服务器异步API
- Android系统(46)--- 基本常识
- 你认识的老罗又回来了!罗永浩:我的创造力才刚刚开始猥琐发育
- 无线传感器网络 | 期末复习知识点1
- Battle Zone 战争地带
- js调用html打印去掉页眉页脚,js 客户端打印html 并且去掉页眉、页脚的实例
- 笔记本的结构深入分析
- mysql 查询view_MySQL之视图(VIEW)
- 【牛客】网易2018校招数据分析师笔试解析
- python 读取csv文件转成字符串,python实现csv格式文件转为asc格式文件的方法
- 微信小游戏云开发 | 72小时极限编程体验
热门文章
- T检验和p-value含义及计算公式
- Collecting Coins
- 基于python的税额计算器
- 肿瘤信息学中一些专业词汇整理(不断更新ing)
- robot_localization 源码解析(1)ekf_localization_node
- html 打印页面不全,浏览器网页打印内容显示不全的解决方法教程[多图]
- 论文阅读笔记:Weakly-supervised Semantic Segmentation in Cityscape via Hyperspectral Image
- 算法训练 - 黑色星期五 有些西方人比较迷信,如果某个月的13号正好是星期五,他们就会觉得不太吉利,用古人的说法,就是“诸事不宜”。请你编写一个程序,统计出在某个特定的年份中,出现了多少次既是13号又
- java数组下标从几开始的_为什么数组角标从0开始
- 超声成像发射声场仿真(Ultrasound Emit Field Simulation)