商品详情页架构的要求,高可用,高性能,高并发 ;一般来说 业界分为两种主流的方案。
中小公司的详情页方案
很多中小型 电商的商品详情页 可能一分钟都没有一个访问,这种的话,就谈不上并发设计,一个tomcat 就能搞定。还有一种中小型公司呢?虽然说公司不大,但是也是有几十万日活,然后几百万用户,他们的商品详情用,采取的方案可能是全局的一个静态页面这样子。
就是我们有把商品详情页直接做成一个静态页面,然后这样子每次全量的更新,把数据全部静态放到redis
里面,每次数据变化的时候,我们就通过一个Java服务去渲染这个数据,然后把这个静态页面推送到到文件服务器。
缺点
- 这种方案的缺点,如果商品很多,那么渲染的时间会很长,达不到实时的效果
- 文件服务器性能高,tomcat性能差,压力都在Tomcat服务器了
- 只能处理一些静态的东西,如果动态数据很多,比如有库存的,你不可能说每次去渲染,然后推送到文件服务器,那不是更加慢?
大型公司的商品详情页的核心思想
生成静态页
添加修改页面的时候生成静态页,这个地方生成的是一个通用的静态页,敏感数据比如 价格,商品名称等,通过占位符来替换,然后将生成的静态页的链接,以及敏感数据同步到redis中,如果只修改价格不需要重新生成静态页,只需要修改redis敏感数据即可。
推送到文件服务器
这个的文件服务器泛指能够提供静态文件处理的文件服务器,nginx代理静态文件,tomcat,以及OSS等都算静态文件服务器,生成完静态文件后将文件推送到文件服务器,并将请求连接存放进redis中
布隆过滤器过滤请求
Redis和nginx的速度很快,但是如果有人恶意请求不存在的请求会造成redis很大的开销,那么可以采用布隆过滤器将不存在的请求过滤出去。
lua直连Redis读取数据
因为java连接Reids进行操作并发性能很弱,相对于OpenResty来说性能差距很大,这里使用OpenResty,读取Redis中存放的URL以及敏感数据。
OpenResty 渲染数据
从Redis获取到URL后lua脚本抓取模板页面内容,然后通过redis里面的敏感数据进行渲染然后返回前端,因为都是lua脚本操作性能会很高。
模拟环境准备
我们的的文件服务器页面在nginx-server
的代码中可以通过http://IP/template.html
访问
配置资源反向代理
upstream dynamicserver {
server 192.168.64.1:9001 fail_timeout=60s max_fails=3;
server 192.168.64.1:9002 fail_timeout=60s max_fails=3;
keepalive 256;
}
server {
server_name www.resources.com 127.0.0.1;
default_type text/html;
charset utf-8;
location ~ .*$ {
index index.jsp index.html;
proxy_pass http://dynamicserver;
# 表示重试超时时间是3s
proxy_connect_timeout 30;
proxy_send_timeout 10;
proxy_read_timeout 10;
#表示在 6 秒内允许重试 3 次,只要超过其中任意一个设置,Nginx 会结束重试并返回客户端响应
proxy_next_upstream_timeout 60s;
proxy_next_upstream_tries 3;
}
}
访问测试
可以通过访问www.resources.com/template.html
访问测试
redis布隆过滤器
安装步骤请参考 Redis系列-安装布隆过滤器
OpenResty支持Reids集群配置
下载安装lua_resty_redis
lua_resty_redis 它是一个基于rockspec API的为ngx_lua模块提供Lua redis客户端的驱动。
resty-redis-cluster模块地址:GitHub - steve0511/resty-redis-cluster: Openresty lua client for redis cluster.
- 将
resty-redis-cluster/lib/resty/
下面的文件 拷贝到openresty/lualib/resty
总共两个文件rediscluster.lua
,xmodem.lua
连接Redis集群封装
创建redisUtils.lua
的文件
--操作Redis集群,封装成一个模块
--引入依赖库
local redis_cluster = require "resty.rediscluster"
--配置Redis集群链接信息
local config = {
name = "testCluster", --rediscluster name
serv_list = { --redis cluster node list(host and port),
{ip="192.168.245.164", port = 7001},
{ip="192.168.245.164", port = 7002},
{ip="192.168.245.164", port = 7003},
{ip="192.168.245.164", port = 7004},
{ip="192.168.245.164", port = 7005},
{ip="192.168.245.164", port = 7006},
},
keepalive_timeout = 60000, --redis connection pool idle timeout
keepalive_cons = 1000, --redis connection pool size
connection_timout = 1000, --timeout while connecting
max_redirection = 5
}
--定义一个对象
local lredis = {}
--创建查询数据get()
function lredis.get(key)
local red = redis_cluster:new(config)
local res, err = red:get(key)
if err then
ngx.log(ngx.ERR,"执行get错误:",err)
return false
end
return res
end
-- 执行hgetall方法并封装成table
function lredis.hgetall(hash_key)
local red = redis_cluster:new(config)
local flat_map, err = red:hgetall(hash_key)
if err then
ngx.log(ngx.ERR,"执行hgetall错误:",err)
return false
end
local result = {}
for i = 1, #flat_map, 2 do
result[flat_map[i]] = flat_map[i + 1]
end
return result
end
-- 判断key中的item是否在布隆过滤器中
function lredis.bfexists(key,item)
local red = redis_cluster:new(config)
-- 通过eval执行脚本
local res,err = red:eval([[
local key=KEYS[1]
local val= ARGV[1]
local res,err=redis.call('bf.exists',key,val)
return res
]],1,key,item)
if err then
ngx.log(ngx.ERR,"过滤错误:",err)
return false
end
return res
end
return lredis
配置lua脚本路径
我们编写了lua脚本需要交给nginx服务器去执行,我们需要将创建一个lua目录,并在全局的nginx‘配置文件中配置lua目录,配置参数使用lua_package_path
user root;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
# 注意!! error日志输出info级别日志
error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
# 注意!!配置redis 本地缓存
lua_shared_dict redis_cluster_slot_locks 100k;
# 注意!!lua脚本路径
lua_package_path "/usr/local/openresty/script/?.lua;;";
#gzip on;
include conf.d/*.conf;
}
测试脚本
server {
listen 9999;
charset utf-8;
location /test {
default_type text/html;
content_by_lua '
local lrredis = require("redisUtils")
-- 尝试读取redis中key的值
local value = lrredis.get("key")
--判断key是否在bf_taxi的布隆过滤器中
local bfexist = lrredis.bfexists("bf_taxi","key")
local htest = lrredis.hgetall("h_taxi")
ngx.log(ngx.INFO, "key的值是",value)
ngx.log(ngx.INFO, "bf_taxi布隆过滤器key的状态",bfexist)
ngx.log(ngx.INFO, "h_taxi[url]的值是",htest["url"])
';
}
}
设置Redis数据
# 登录Redis
docker exec -ti redis01 /bin/bash
redis-cli -h 172.18.0.2 -c
# 设置key
set key value11
# 设置hash
hset h_taxi url http://www.baidu.com
# 创建布隆过滤器
BF.RESERVE bf_taxi 0.01 10000 NONSCALING
# 布隆过滤器添加key
BF.ADD bf_taxi key
访问测试
tail -f /usr/local/openresty/nginx/logs/error.log
请求参数封装
nginx为了能够处理请求需要获取请求数据,需要将获取请求参数的lua脚本封装以下,创建requestUtils.lua
的文件
--定义一个对象
local lreqparm={}
-- 获取请求参数的方法
function lreqparm.getRequestParam()
-- 获取请求方法 get或post
local request_method = ngx.var.request_method
-- 定义参数变量
local args = nil
if "GET" == request_method then
args = ngx.req.get_uri_args()
elseif "POST" == request_method then
ngx.req.read_body()
args = ngx.req.get_post_args()
end
return args
end
return lreqparm
测试脚本
server {
listen 9999;
charset utf-8;
location /testreq {
default_type text/html;
content_by_lua '
local lreqparm = require("requestUtils")
local params = lreqparm.getRequestParam()
local title = params["title"]
if title ~= nil then
ngx.say("<p>请求参数的Title是:</p>"..title)
return
end
ngx.say("<P>没有输入title请求参数<P>")
';
}
}
抓取模板内容封装
下载安装lua-resty-http
下载地址 GitHub - ledgetech/lua-resty-http: Lua HTTP client cosocket driver for OpenResty / ngx_lua.
将lua-resty-http-master\lib\resty
下的所有文件复制到openresty/lualib/resty
httpclient
总共两个文件http.lua
,http_headers.lua
因为需要从远程服务器抓取远程页面的内容,需要用到http模块,将其封装起来,创建
requestHtml.lua
-- 引入Http库
local http = require "resty.http"
--定义一个对象
local lgethtml={}
function lgethtml.gethtml(requesturl)
--创建客户端
local httpc = http:new()
local resp,err = httpc:request_uri(requesturl,
{
method = "GET",
headers = {["User-Agent"]="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36"}
})
--关闭连接
httpc:close()
if not resp then
ngx.say("request error:",err)
return
end
local result = {}
--获取状态码
result.status = resp.status
result.body = resp.body
return result
end
return lgethtml
测试脚本
server {
listen 9999;
charset utf-8;
# 配置路径重写
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {
rewrite ^/(.*) http://www.resources.com/$1 permanent;
}
location /testgetHtml {
default_type text/html;
content_by_lua '
local lgethtml = require("requestHtml")
local url = "http://127.0.0.1/template.html"
local result = lgethtml.gethtml(url);
ngx.log(ngx.INFO, "状态是",result.status)
ngx.log(ngx.INFO, "body是",result.body)
ngx.say(result.body)
';
}
}
访问http://192.168.64.181:9999/testgetHtml 测试
模版渲染配置
下载安装lua-resty-template
wget https://github.com/bungle/lua-resty-template/archive/v1.9.tar.gz
tar -xvzf v1.9.tar.gz
解压后可以看到lib/resty下面有一个template.lua,这个就是我们所需要的,在template目录中还有两个lua文件,将这两个文件复制到/usr/openResty/lualib/resty中即可。
使用方式
local template = require "resty.template"
-- Using template.new
local html=[[<html>
<body>
<h1>{{message}}</h1>
</body>
</html>
]]
template.render(html, { message = params["title"] })
测试
server {
listen 9999;
charset utf-8;
location /testtemplate {
default_type text/html;
content_by_lua '
local lreqparm = require("requestUtils")
local template = require "resty.template"
local params = lreqparm.getRequestParam()
-- Using template.new
local html=[[<html>
<body>
<h1>{{message}}</h1>
</body>
</html>
]]
template.render(html, { message = params["title"] })
';
}
}
访问http://192.168.64.150:9999/testtemplate?title=123456
整体业务分析
整个调用流程如下,需要使用lua脚本编写,整体流程分为7 步
面我们将各个组件都给封装了,下面我们需要将各个组件组合起来
编写lua脚本
创建一个requestTemplateRendering.lua
的lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by baiyp.
--- DateTime: 2020/11/24 13:24
---
local template = require("resty.template")
local lrredis = require("redisUtils")
local lgethtml = require("requestHtml")
local lreqparm = require("requestUtils")
--获取请求参数
local reqParams = lreqparm.getRequestParam()
-- 定义本地缓存
local html_template_cache = ngx.shared.html_template_cache
-- 获取请求ID的参数
local reqId = reqParams["id"];
ngx.log(ngx.INFO, "requestID:", reqId);
-- 校验参数
if reqId==nil then
ngx.say("缺少ID参数");
return
end
-- 布隆过滤器检查id是否存在
local bfexist = lrredis.bfexists("bf_taxi",reqId)
ngx.log(ngx.INFO, "布隆过滤器检验:", bfexist)
-- 校验key不存在直接返回
if bfexist==0 then
ngx.say("布隆过滤器校验key不存在...")
return
end
-- 拼接hget的key
local hgetkey = "hkey_".. reqId
-- 通过hget获取map的所有数据
local templateData = lrredis.hgetall(hgetkey);
if next(templateData) == nil then
ngx.say("redis没有存储数据...")
return
end
--获取模板价格数据
local amount = templateData["amount"]
ngx.log(ngx.INFO, "amount:", amount)
if amount == nil then
ngx.say("价格数据未配置");
return
end
-- 获取本地缓存对象
ngx.log(ngx.INFO, "开始从缓存中获取模板数据----")
local html_template = html_template_cache:get(reqId)
-- 判断本地缓存是否存在
if html_template == nil then
-- 获取模板url中的数据
ngx.log(ngx.INFO, "缓存中不存在数据开始远程获取模板")
local url = templateData["url"]
ngx.log(ngx.INFO, "从缓存中获取的url地址:", url)
if url == nil then
ngx.say("URL路径未配置");
return
end
-- 抓取远程url的html
ngx.log(ngx.INFO, "开始抓取模板数据:", url)
local returnResult = lgethtml.gethtml(url);
-- 判断抓取模板是否正常
if returnResult==nil then
ngx.say("抓取URL失败...");
return
end
-- 判断html状态
if returnResult.status==200 then
html_template = returnResult.body
--设置模板缓存为一小时
ngx.log(ngx.INFO, "将模板数据加入到本地缓存")
html_template_cache:set(reqId,html_template,60 * 60)
end
end
ngx.log(ngx.INFO, "缓存中获取模板数据结束----")
-- 模板渲染
--编译得到一个lua函数
local func = template.compile(html_template)
local context = {amount=amount}
ngx.log(ngx.INFO, "开始渲染模板数据")
--执行函数,得到渲染之后的内容
local content = func(context)
--通过ngx API输出
ngx.say(content)
编写nginx配置
在nginx主配置文件中添加模板缓存
lua_shared_dict html_template_cache 10m;
编写nginx配置文件
vi template.conf
server {
listen 8888;
charset utf-8;
# 配置路径重写
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {
rewrite ^/(.*) http://www.resources.com/$1 permanent;
}
#删除本地缓存
location /delete {
default_type text/html;
content_by_lua '
local lreqparm = require("requestUtils")
--获取请求参数
local reqParams = lreqparm.getRequestParam()
-- 定义本地缓存
local html_template_cache = ngx.shared.html_template_cache
-- 获取请求ID的参数
local reqId = reqParams["id"];
ngx.log(ngx.INFO, "requestID:", reqId);
-- 校验参数
if reqId==nil then
ngx.say("缺少ID参数");
return
end
-- 获取本地缓存对象
html_template_cache:delete(reqId);
ngx.say("清除缓存成功");
';
}
location /template {
default_type text/html;
content_by_lua_file /usr/local/openresty/script/requestTemplateRendering.lua;
}
}
初始化数据
# 进入一个redis集群的节点内部
docker exec -ti redis01 /bin/bash
# 以集群方式登录172.18.0.2:3306节点
redis-cli -h 172.18.0.2 -c
# 在redis中添加一个布隆过滤器 错误率是0.01 数量是1万个
BF.RESERVE bf_taxi 0.01 10000 NONSCALING
# 在bf_test 的布隆过滤器添加一个key
BF.ADD bf_taxi 1
#检查数据是否存在
BF.EXISTS bf_taxi 1
# 添加URL以及价格
hset hkey_1 url http://127.0.0.1/template.html amount 100.00
访问http://192.168.64.181:8888/template?id=1
进行测试