目录
1.将文件上传至云服务器
1.1 处理上传头像逻辑
1.1.1 客户端上传
1.1.2 服务器直传
2.优化网站的性能
2.1 本地缓存优化查询方法
2.2 压力测试
使用七牛云服务器
导入依赖:
com.qiniu
qiniu-java-sdk
7.12.1
在 application.properties 中配置:
# qiniu
qiniu.key.access=NHzlA2MRle10vB0wNeO54aGS-tMjEpO5BiIrflz9
qiniu.key.secret=qicgWpPOslm5_dFu_j_94r5gUcKm_UekhT2MMLPf
qiniu.bucket.header.name=communityheader123321
quniu.bucket.header.url=http://s6sc3za8f.hb-bkt.clouddn.com
qiniu.bucket.share.name=communityshare123321
qiniu.bucket.share.url=http://s6scgzhqs.hb-bkt.clouddn.com
打开 UserController 类:
@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);
}
再处理表单 setting.html:
上传头像
在 static 包下 js 包新建 setting.js:
$(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;
}
需要重构分享相关的功能,打开 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 map = new HashMap<>();
// map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);
map.put("shareUrl", shareBucketUrl + "/" + fileName);
return CommunityUtil.getJSONString(0, null, map);
}
// 废弃
// 获取长图
@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());
}
}
修改 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;
}
//获取 html、文件名、后缀
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();
// 成功返回0
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 + "].");
}
}
}
本地缓存:将数据缓存在应用服务器上,性能最好;常用的缓存工具:Ehcache、Guava、Caffeine等
分布式缓存:将数据缓存在 NoSQL 数据库上,跨服务器;常用缓存工具:MemCache、Redis等
多级缓存:一级缓存 > 二级缓存 > DB;避免缓存雪崩(缓存失效,大量请求直达DB),提高系统的可用性
本地缓存主要使用 Caffeine 工具,导入依赖:
com.github.ben-manes.caffeine
caffeine
2.7.0
设置自定义参数:
# caffeine
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180
优化查询方法(discussPostService):
private static final Logger logger = LoggerFactory.getLogger(DiscussPostService.class);
@Value("${caffeine.posts.max-size}")
private int maxSize;
@Value("${caffeine.posts.expire-seconds}")
private int expireSeconds;
// Caffeine核心接口: Cache, LoadingCache, AsyncLoadingCache
// 帖子列表缓存
private LoadingCache> postListCache;
// 帖子总数缓存
private LoadingCache postRowsCache;
@PostConstruct
public void init() {
// 初始化帖子列表缓存
postListCache = Caffeine.newBuilder()
.maximumSize(maxSize)//缓存最大数据量
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)//把缓存写入缓存空间多长时间自动过期
.build(new CacheLoader>() {
//当尝试从缓存中取数据,caffeine会看缓存是否有数据:
//没有,需要提供一个查询的方法,load就是实现这个查询方法
@Nullable
@Override
public List load(@NonNull String key) throws Exception {
//实现访问数据库查数据
if (key == null || key.length() == 0) {
throw new IllegalArgumentException("参数错误!");
}
//不为空,解析 key(offset + ":" + limit),:进行切割
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.");
//调用discussPostMapper查询数据(userId = 0 orderMode = 1)
return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
}
});
// 初始化帖子总数缓存
postRowsCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader() {
@Nullable
@Override
public Integer load(@NonNull Integer key) throws Exception {
logger.debug("load post rows from DB.");
return discussPostMapper.selectDiscussPostRows(key);
}
});
}
//声明一个业务方法:查询某一页的方法,返回类型是集合
public List findDiscussPosts(int userId, int offset, int limit, int orderMode) {
//缓存热门帖子(orderMode == 1)并且缓存首页(访问首页的时候,userId 是不传为0)
// 缓存的是一页的数据(key 为 offset 和 limit 组合)
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);
}
测试类:
package com.example.demo;
import com.example.demo.entity.DiscussPost;
import com.example.demo.service.DiscussPostService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = DemoApplication.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("今年的就业形势,确实不容乐观");
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));
}
}
使用 jmeter 工具测试:
未添加缓存:
添加缓存: