性能提升效果
coderbbb最近针对博客文章页面做了一次压力测试,在8C16G机器上压测,在保证响应时间500毫秒内的前提下,压测的最高并发只有80。考虑到文章页面的数据大部分都是静态数据,所以我们针对文章页面的HTML做了缓存。之后再次压测同样的页面,并发在提升到8000之后,仍有一定的提升空间,性能提升100+倍。
针对以上现象,我们强烈建议静态数据较多的WEB网页,使用缓存技术静态化HTML页面,提高性能。
技术栈介绍
coderbbb使用springboot+thymeleaf开发。在该技术栈下,目前并没有针对整个HTML页面进行静态化缓存的方案,只能自己DIY。我们使用了EhCache作为缓存框架,当用户请求文章页时,会先在EhCache中检查是否有缓存。
之所以选择EhCache的原因是该缓存框架可以把数据缓存到内存+硬盘两个地方。对于高热度的文章,缓存到内存中。对于较低热度的文章,整页缓存到硬盘中。最大限度的避免了数据库查询等高耗时操作。
技术实现
一、springboot整合EhCache作为缓存框架
这部分之前写过文章专门讲解,请移步《springboot中配置使用EhCache做持久化缓存(内存+硬盘)》阅读。
二、springboot中新建拦截器,判断是否有缓存
在springboot中创建一个拦截器,该拦截器在收到web请求后,将URL的md5 hash值作为缓存key,在EhCache中查询缓存是否存在,如果存在,则返回缓存。如果不存在,则让用户正常访问,并把返回给用户的HTML字符串缓存到key中。
核心知识点:springboot+thymeleaf如何获取HTML返回结果?这里要用到thymeleaf的ThymeleafViewResolver
类,演示代码如下:
WebContext ctx = new WebContext(request, response, request.getServletContext(), new Locale("zh"), modelAndView.getModel());
String html = thymeleafViewResolver.getTemplateEngine().process(modelAndView.getViewName(), ctx);
springboot HTML网页静态化缓存拦截器完整代码如下:
package com.coderbbb.blogv2.config.interceptor;
import com.coderbbb.blogv2.config.anno.CacheHtml;
import com.coderbbb.blogv2.database.dos.UserDO;
import com.coderbbb.blogv2.service.CacheHtmlService;
import com.coderbbb.blogv2.utils.HttpServletUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CacheHtmlInterceptor implements HandlerInterceptor {
@Autowired
private CacheHtmlService cacheHtmlService;
@Autowired
private ThymeleafViewResolver thymeleafViewResolver;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
String urlFull = (String) request.getAttribute("urlFull");
request.setAttribute("urlFull",urlFull);
String urlHash = DigestUtils.md5Hex(urlFull);
String cache = cacheHtmlService.getCache(urlHash);
if (cache != null) {
//缓存存在,将缓存的数据直接返回给客户端
try {
response.setHeader("Content-Type", "text/html;charset=UTF-8");
OutputStream outputStream = response.getOutputStream();
outputStream.write(cache.getBytes(StandardCharsets.UTF_8));
outputStream.close();
} catch (Exception e) {
logger.error("返回结果Response写入异常", e);
}
return false;
} else {
request.setAttribute("refreshCacheHtml", true);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (modelAndView != null && modelAndView.getViewName() != null) {
//缓存部分HTML页面
Boolean refreshCacheHtml = (Boolean) request.getAttribute("refreshCacheHtml");
if (Boolean.TRUE.equals(refreshCacheHtml)) {
//需要刷新缓存
String urlFull = (String) request.getAttribute("urlFull");
//apache codec类库的MD5加密方法,你可以替换为你常用的。
String urlHash = DigestUtils.md5Hex(urlFull);
//这个IF语句是防止高并发场景下,缓存重复更新的问题。你可以替换为你的加锁方式,也可以去掉IF,将IF中的代码拿出来。
if (redisLockService.getLock(urlHash, 60000)) {
String viewName = modelAndView.getViewName();
try {
//获取HTML返回结果,存入缓存中
WebContext ctx = new WebContext(request, response, request.getServletContext(), new Locale("zh"), modelAndView.getModel());
String html = thymeleafViewResolver.getTemplateEngine().process(viewName, ctx);
cacheHtmlService.putCache(urlHash, html);
logger.warn("## Cache Html ##" + urlFull);
} catch (Exception e) {
logger.error("cache html err:" + urlFull, e);
}
}
}
}
}
}
EhCache缓存封装好的Service,CacheHtmlService
完整代码如下:
package com.coderbbb.blogv2.service;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class CacheHtmlService {
@Cacheable(value = "cacheHtml",key = "#urlHash",sync = true)
public String getCache(String urlHash){
return null;
}
@CachePut(value = "cacheHtml", key = "#urlHash")
public String putCache(String urlHash,String html){
return html;
}
@CacheEvict(value = "cacheHtml", allEntries = true)
public void clear(){
}
@CacheEvict(value = "cacheHtml", key = "#urlHash")
public void clearOne(String urlHash){
}
}
最后,将拦截器注入到Springboot中。之后,当你访问你的web页面时,将会生成缓存到EhCache中,为下一次访问加速。你可以在上面代码的基础上,定制你自己的逻辑。比如设置哪些页面缓存,哪些不缓存等等。