015:亿万级别商品详情页面实现动态优化
1 亿万级别商品详情页面如何设计
今日课程任务
- 回顾商品详情页面静态化的好处
- 基于FreeMarker实现生成商品详情静态页面
- 将商品详情页面直接放入CDN缓存中运行
- 快速构建OpenResty(nginx)运行环境
- 基于OpenResty+lua实现静态化模版引擎
- 使用lua语言动态调用后端接口获取实时数据
- 基于OpenResty+lua实现亿万级别商品详情页面设计
亿万级别商品详情页面设计
商品详情页面的设计存在哪些问题?
- Mysql与ES如何保持数据同步的问题
方案1 每次发布商品的时候调用es接口实现数据的同步
方案2 使用canal解决 - 在商品详情页面除了商品价格和库存经常发生变化以外,其他数据可能很少发生变化,所以需要将商品详情页面设计成静态化形式。
- 商品页面静态化存在哪些好处?(FreeMarker实现)
访问速度非常快、不需要查询数据库得到数据减轻数据库访问压力、对SEO搜索非常友好。
2 使用Nginx缓存商品详情页面
- 可以采用nginx或者cdn直接缓存商品详情页面
Nginx缓存商品详情页面原理:
直接将网页源代码缓存到nginx目录中,缓存的key为商品详情的url路径地址。
Nginx与MySQL数据不一致如何解决?
- 直接清除nginx缓存 不靠谱
- 在url后面加上最新更新商品的时间戳
基于ES实现商品的搜索
新建模块mt-shop-service-api-product、mt-shop-service-product
核心代码
public interface ProductDetailService {
@GetMapping("/getProductsearchDetails")
BaseResponse<ProductDto> getProductsearchDetails(@RequestParam ("productId") Long productId);
}
@RestController
public class ProductDetailServiceImpl extends BaseApiService implements ProductDetailService {
@Autowired
private ProductReposiory productReposiory;
@Override
public BaseResponse<ProductDto> getProductsearchDetails(Long productId) {
Optional<ProductEntity> optionalResult = productReposiory.findById(productId);
ProductEntity productEntity = optionalResult.get();
if (productEntity == null) {
return setResultError("没有查询到数据");
}
ProductDto productDto = doToDto(productEntity, ProductDto.class);
return setResultSuccess(productDto);
}
}
创建商品web应用
新建模块mt-shop-web/mt-shop-commodity-web
@FeignClient("app-mayikt-product")
public interface ProductDetailsFeign extends ProductDetailService {
}
@Controller
public class CommodityIndexController {
@Autowired
private ProductDetailsFeign productDetailsFeign;
@RequestMapping("/getProductsearchDetails")
public String getProductsearchDetails(Long productId, HttpServletRequest request) {
// 控制层采用feign客户端调用商品服务接口
BaseResponse<ProductDto> productsearchDetails = productDetailsFeign.getProductsearchDetails(productId);
ProductDto productDto = productsearchDetails.getData();
request.setAttribute("p", productDto);
return "productsearchDetails";
}
@RequestMapping("/generateHtml")
@ResponseBody
public String generateHtml(Long productId) throws IOException, TemplateException {
//创建配置类
Configuration configuration = new Configuration(Configuration.getVersion());
//设置模板路径
String classpath = this.getClass().getResource("/").getPath();
configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
//设置字符集
//configuration.setDefaultEncoding("UTF‐8");
//加载模板
Template template = configuration.getTemplate("productsearchDetails.ftl");
//数据模型
Map<String, Object> map = new HashMap<>();
// 调用商品服务接口获取商品详情的数据
BaseResponse<ProductDto> productsearchDetails = productDetailsFeign.getProductsearchDetails(productId);
ProductDto productDto = productsearchDetails.getData();
map.put("p", productDto);
//静态化
String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
InputStream inputStream = IOUtils.toInputStream(content);
//输出文件 nginx 目录
Integer id = productDto.getId();
FileOutputStream fileOutputStream = new FileOutputStream(new File("F:\\path\\nginx\\nginx-1.17.3\\html\\" + id + ".html"));
int copy = IOUtils.copy(inputStream, fileOutputStream);
return copy > 0 ? "success" : "fail";
}
}
配置nginx conf/nginx.conf文件
events {
#的最大连接数(包含所有连接数)1024
worker_connections 1024; ## Default: 1024
}
http{
# 代理缓存配置
proxy_cache_path "./meite_cachedata" levels=1:2 keys_zone=meitecache:256m inactive=1d max_size=1000g;
server {
listen 80;
location /{
#使用缓存名称
proxy_cache meitecache;
#对以下状态码实现缓存
proxy_cache_valid 200 206 304 301 302 1d;
#缓存的key
proxy_cache_key $request_uri;
add_header X-Cache-Status $upstream_cache_status;
#反向代理地址
proxy_pass http://127.0.0.1:8030/;
}
}
}
测试效果:
3 使用FreeMarker生成静态化模板页面
商品详情页面中频繁变化的是价格和库存,因此在一个商品详情页面中库存和商品价格不能放入到缓存中,要实时读取,其他可以作为伪静态页面直接读取即可。
productsearchDetails.ftl
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${p.name}</title>
</head>
<body>
<h1>商品名称:${p.name}</h1>
<img src="${p.mainImage}">
<div class="footer">
<p>客服小微QQ:483966038 <span></span>Email:644064779@.com <span></span>官方粉丝群:193086273<br> Copyright ©2016-2019
上海每特教育科技有限公司 版本所有Powered by 蚂蚁课堂 <span></span>沪ICP备18018606号-2</p>
</div>
</body>
</html>
查询ES数据替换上述${}内容生成静态html页面
@Controller
public class CommodityIndexController {
@Autowired
private ProductDetailsFeign productDetailsFeign;
@RequestMapping("/getProductsearchDetails")
public String getProductsearchDetails(Long productId, HttpServletRequest request) {
// 控制层采用feign客户端调用商品服务接口
BaseResponse<ProductDto> productsearchDetails = productDetailsFeign.getProductsearchDetails(productId);
ProductDto productDto = productsearchDetails.getData();
request.setAttribute("p", productDto);
return "productsearchDetails";
}
@RequestMapping("/generateHtml")
@ResponseBody
public String generateHtml(Long productId) throws IOException, TemplateException {
//创建配置类
Configuration configuration = new Configuration(Configuration.getVersion());
//设置模板路径
String classpath = this.getClass().getResource("/").getPath();
configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
//设置字符集
//configuration.setDefaultEncoding("UTF‐8");
//加载模板
Template template = configuration.getTemplate("productsearchDetails.ftl");
//数据模型
Map<String, Object> map = new HashMap<>();
// 调用商品服务接口获取商品详情的数据
BaseResponse<ProductDto> productsearchDetails = productDetailsFeign.getProductsearchDetails(productId);
ProductDto productDto = productsearchDetails.getData();
map.put("p", productDto);
//静态化
String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
InputStream inputStream = IOUtils.toInputStream(content);
//输出文件 nginx 目录
Integer id = productDto.getId();
FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\devtool\\nginx\\nginx-1.17.3\\html\\" + id + ".html"));
int copy = IOUtils.copy(inputStream, fileOutputStream);
return copy > 0 ? "success" : "fail";
}
}
测试效果:
1 每次在发布商品的时候,通过ftl技术生成好静态商品详情页面放入到nginx的目录中,提前预热商品详情页面。
2 也可以将静态html页面放入第三方cdn中,遵循就近原则访问可以减少带宽的传输距离,从而提高整个页面访问效率。
4 openresty+lua实现亿级别商品详情页面原理
openresty+nginx+lua实现亿级别商品详情页面设计原理
- 后台每次在更新商品详情页面的时候,会直接生成对应的商品详情页面的模板,放入到nginx中,采用lua脚本读取mysql/es/接口拿到数据源,再渲染给客户端。
- 优点:可以灵活扩展操作nginx、减轻服务后台访问压力。
整个商品详情页面数据缓存到nginx中,对价格、库存等数据进行实时查询,既可以做缓存又能动态实时读取mysql数据,无需调用web层能有效减轻后台服务压力。
5 openresty的基本介绍
Openresty http://openresty.org/cn/ 官网
Lua语言介绍 https://baike.baidu.com/item/lua/7570719?fr=aladdin
OpenResty®是一个基于Nginx与Lua的高性能Web平台,其内部集成了大量精良的Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。(可以理解为nginx加强版本)
linux环境上安装openresty环境
yum install yum-utils -y
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum install openresty –y
yum install openresty-resty -y
yum --disablerepo="*" --enablerepo="openresty" list available
yum install pcre-devel openssl-devel gcc curl
openresty-1.13.6.2下载:http://openresty.org/en/download.html
上传到/usr/local目录后解压
tar -zxvf openresty-1.13.6.2.tar.gz
cd openresty-1.13.6.2
./configure
make
make install
安装完成之后,nginx默认安装在 /usr/local/openresty/nginx目录下
cd /usr/local/openresty/nginx
/usr/local/openresty/nginx/sbin/nginx 启动nginx
/usr/local/openresty/nginx/sbin/nginx -s reload 重启nginx
测试效果:
6 openrety整合简单lua代码指令
指定简单lua脚本 直接修改conf/nginx.conf配置文件
location /luademo {
default_type 'text/html';
content_by_lua_block {
ngx.say("meite mayikt");
}
}
指定文件位置/usr/local/openresty/nginx/lua
location /luademo2 {
default_type 'text/html';
content_by_lua_file /usr/local/openresty/nginx/lua/luademo.lua;
}
luademo.lua文件上传到/usr/local/openresty/nginx/lua/目录
luademo.lua
ngx.say("meite 111 333 mayikt");
测试效果:
7 openresty整合lua模板引擎框架
1. 引入lua模板文件
需要引入lua-resty-template-master
https://github.com/bungle/lua-resty-template
将template.lua 放入/usr/local/openresty/lualib/resty
将html.lua 放入/usr/local/openresty/lualib/resty/html
2. 上传html模板文件和lua引用文件
cd /usr/local/openresty/nginx
mkdir template
上传mayikt.html到/usr/local/openresty/nginx/template目录下
mayikt.html
<!DOCTYPE html>
<html>
<body>
<h1>{{message}}</h1>
</body>
</html>
上传mayikt_template.lua文件到/usr/local/openresty/nginx/lua/目录
mayikt_template.lua
local template = require "resty.template"
content={
message ="mayikt"
}
template.render("mayikt.html",content)
3 配置nginx
## nginx配置指定模版引擎位置
set $template_root /usr/local/openresty/nginx/template;
location /template {
default_type 'text/html';
content_by_lua_file /usr/local/openresty/nginx/lua/mayikt_template.lua;
}
重启nginx /usr/local/openresty/nginx/sbin/nginx -s reload
测试效果:
8 使用openresty发送Http请求获取数据
1. 引入lua发送http请求依赖
下载lua-resty-http-master
将http.lua、http_headers.lua 上传到/usr/local/openresty/lualib/resty
2. nginx添加
location /luahttpclient {
default_type 'text/html';
content_by_lua_file /usr/local/openresty/nginx/lua/lua_httpclient.lua;
}
- lua_httpclient.lua文件上传到/usr/local/openresty/nginx/lua
lua_httpclient.lua
local uri_args=ngx.req.get_uri_args()
local productId = uri_args["productId"]
local host = {"192.168.149.1:4050/getProductsearchDetails","192.168.149.1:4050/getProductsearchDetails"}
local hash = ngx.crc32_long(productId)
ngx.say(hash)
hash = (hash % 2) + 1
ngx.say(hash)
backend = "http://"..host[hash]
ngx.say(backend)
local requestBody =backend.."?productId="..productId
ngx.say(requestBody)
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri(requestBody ,{
method = "GET"
})
if not resp then
ngx.say("request error :", err)
return
end
ngx.status = resp.status
--ngx.say(resp.status)
--ngx.say(resp.body)
local cjson = require "cjson"
local productJSON = cjson.decode(resp.body);
local data=productJSON.data;
--ngx.say(data.data.name)
local template = require "resty.template"
content={
name =data.name,
mainImage =data.mainImage
}
template.render("commodity_details.html",content)
- commodity_details.html文件上传到/usr/local/openresty/nginx/template
commodity_details.html
<!DOCTYPE html>
<html>
<body>
<meta charset="utf-8"/>
<h1>{{name}}</h1>
<img src="{{mainImage}}">
</body>
</html>
测试效果:
查询数据库/ES仅需要替换发送http请求依赖及相应代码即可。
openresty+lua代码优化说明
- lua代码发送http请求改为查询es数据,再加缓存,根据url把整个es数据缓存到lua中,这样只有第一次查es后期走缓存。
- 商品的价格、库存每次都访问数据源读取最新的数据。