3.OpenResty
使用Nginx来完成静态页面模板的渲染,之前学习过Nginx的web容器功能、反向代理功能、负载均衡功能。
但是如何利用Nginx查询Redis缓存?如何利用Nginx完成页面模板渲染?
nginx中有一个模块叫做ngx_lua,可以将Lua嵌入到Nginx中,从而可以使用Lua来编写脚本,这样就可以使用Lua编写应用逻辑,操作Redis、MySQL等等;这样就可以使用Lua语言开发高性能Web应用了。
那么OpenResty是什么呢?
3.1.介绍OpenResty
OpenResty官网地址:http://openresty.org/cn/
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
3.2.安装OpenResty
首先你的Linux虚拟机必须联网,这里建议大家统一使用CentOS7版本。
1)安装开发库:
首先要安装OpenResty的依赖开发库,执行命令:
yum install -y pcre-devel openssl-devel gcc --skip-broken
2)安装OpenResty仓库
你可以在你的 CentOS 系统中添加 openresty
仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update
命令)。运行下面的命令就可以添加我们的仓库:
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果提示说命令不存在,则运行:
yum install -y yum-utils
然后再重复上面的命令
3)安装OpenResty
然后就可以像下面这样安装软件包,比如 openresty
:
yum install -y openresty
4)安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
如果你想安装命令行工具 opm
,那么可以像下面这样安装 openresty-opm
包:
yum install -y openresty-opm
5)目录结构
默认情况下,OpenResty安装的目录是:/usr/local/openresty
看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。
4)配置nginx的环境变量
打开配置文件:
vi /etc/profile
在最下面加入两行:
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile
3.3.Lua语言
OpenResty的开发和使用离不开Lua脚本,那么Lua又是什么呢?
3.3.1.Lua介绍
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。
3.3.2.语法入门
Lua的详细语法大家可以参考网站上的一些教学,例如:Lua菜鸟教程,任何语言都是从基本的如:变量、数据类型、循环、逻辑判断、运算、数组等入手。相信熟悉java的你应该可以快速上手Lua。
因此我们从这几块入手,看一些简单命令即可:
1)变量声明
声明一个局部变量,用local关键字即可:
-- 定义数字
local a = 123
-- 定义字符串
local b = "hello world"
-- 定义数组
local c = {"hello", "world", "lua"}
-- 定义table
local d = {
name = "jack",
age = 21
}
2)打印结果
print('hello world')
3)条件控制
if( 布尔表达式 1)
then
--[ 在布尔表达式 1 为 true 时执行该语句块 --]
elseif( 布尔表达式 2)
then
--[ 在布尔表达式 2 为 true 时执行该语句块 --]
else
--[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end
4)循环语句:
遍历数字
for i=0, 10, 1 do
print(i)
end
遍历数组:
--打印数组a的所有值
local a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
-- 遍历时,i是角标,v是元素。Lua中数组角标从1开始
遍历table:
-- 定义table
local b = {
name = "jack",
age = 21
}
for k, v in pairs(b) do
print(k, v)
end
-- 遍历时,k是key,v是值。
3.4.OpenResty快速入门
为了不影响OpenResty安装目录的结构,我们在新的目录中来启动和配置。
3.4.1.基本配置
我们创建一个新的目录:/usr/resty
cd /usr
mkdir resty
然后在这个目录下创建几个新目录:
cd resty
mkdir conf logs lua
然后新建一个配置文件:
cd /usr/resty
vi conf/nginx.conf
添加下面的内容:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; #c模块
lua_shared_dict item_local_cache 50m; #共享全局变量,在所有worker间共享
default_type text/html; # 默认响应类型是html
include lua.conf; # 引入一个lua.conf文件
}
我们通过include导入另一个配置,不在nginx.conf中写入太多内容。
3.4.2.监听端口
现在,lua.conf
已经被引入,我们的所有配置都写到这个里面。
新建一个文件:
cd /usr/resty
vi conf/lua.conf
添加下面的内容:
server {
listen 80;
location / {
# 响应数据由 lua/test.lua这个文件来指定
content_by_lua_file lua/test.lua;
}
}
3.4.3第一个lua脚本
现在,响应数据已经交给 lua/test.lua
来处理了,我们编写lua/test.lua
文件,返回你想返回的任何内容即可。
新建一个文件:
cd /usr/resty
vi lua/test.lua
写入下面内容:
ngx.say("<h1>hello, world</h1>")
ngx.say()
可以理解成HttpServletResponse
中的response.getWriter().println()
3.4.4.启动并访问
当前必须在/usr/resty
目录中,然后执行命令
运行启动命令:
nginx -p `pwd` -c conf/nginx.conf
-p `pwd`
:-p 指定运行时路径前缀,pwd代表当前路径-c conf/nginx.conf
:-c 指定运行时配置文件,这里指定了conf/nginx.conf
重新加载配置命令:
nginx -p `pwd` -c conf/nginx.conf -s reload
停止命令:
nginx -p `pwd` -c conf/nginx.conf -s stop
访问你的虚拟机地址,例如:http://192.168.150.101/,看到这个说明成功了
3.5.获取请求参数
Nginx接收到请求后,会帮我们解析并存储到内置变量中,只需要调用这些变量或方法就可以拿到。
-
ngx.var
: nginx变量,如果要赋值如ngx.var.b = 2,此变量必须提前声明;另外对于nginx location中使用正则捕获的捕获组可以使用ngx.var[捕获组数字]获取; -
ngx.req.get_headers()
:获取请求头,获取带中划线的请求头时请使用如headers.user_agent这种方式;如果一个请求头有多个值,则返回的是table; -
ngx.req.get_uri_args()
:获取url请求参数,其用法和get_headers
类似;?name=jack -
ngx.req.get_post_args()
:获取post请求内容体,其用法和get_headers
类似,但是必须提前调用ngx.req.read_body()
来读取body体(也可以选择在nginx配置文件使用lua_need_request_body
on;开启读取body体,但是官方不推荐); -
ngx.req.get_body_data()
:为解析的请求body体内容字符串。
3.5.1.编写映射规则
现在,我们在lua.conf中编写一条规则,来监听请求:
# 采用正则表达式映射路径,有两个(\d+),分别是第1组、第2组正则
location ~ /lua_request/(\d+)/(\d+) {
# $1代表获取第1组正则捕获的内容,set $a $1代表把$1的值赋值给$a这个变量
set $a $1;
# $2代表获取第2组正则捕获的内容,set $b $2代表把$2的值赋值给$b这个变量
set $b $2;
#nginx内容处理
content_by_lua_file lua/test_request.lua;
}
3.5.2.编写脚本
然后在新建test_request.lua文件:
cd /usr/resty
vi lua/test_request.lua
添加内容:
-- 定义一个函数,打印table数据
function sayTables(val)
for k,v in pairs(val) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
else
ngx.say(k, " : ", v, "<br/>")
end
end
end
ngx.say('<header>')
ngx.say('<meta charset="utf-8">')
ngx.say('</header>')
-- 获取路径占位符中通过正则得到的参数
ngx.say("<h1> -----请求路径占位符参数------- </h1>");
ngx.say("<h4>")
ngx.say("ngx.var.a : ", ngx.var.a, "<br/>")
ngx.say("ngx.var.b : ", ngx.var.b, "<br/>")
ngx.say("ngx.var[1] : ", ngx.var[1], "<br/>")
ngx.say("ngx.var[2] : ", ngx.var[2], "<br/>")
ngx.say("</h4>")
-- 获取请求url参数
ngx.say("<h1> -----请求url参数------- </h1>");
ngx.say("<h4>")
local params = ngx.req.get_uri_args()
sayTables(params)
ngx.say("</h4>")
return ngx.exit(200)
3.5.3.重启测试
重启:
nginx -p `pwd` -c conf/nginx.conf -s reload
通过浏览器访问:
http://192.168.150.101/lua_request/110/120?name=jack&age=22
3.6.OpenResty模板渲染模块
动态web网页开发是Web开发中一个常见的场景,我们的商品详情页就需要nginx来完成页面的动态渲染,这要用到模板渲染模块。
我们会使用lua-resty-template来完成。
3.6.1.安装模板渲染模块
模板渲染组件并不是OpenResty自带的,需要我们自己来安装。
确保自己已经安装过OPM命令(参考安装OpenResty部分)。
输入命令:
opm get bungle/lua-resty-template
3.6.2.定义模板位置
模板渲染与服务端的JSP类似,需要知道的信息包括:
- 模板文件的位置
- 模板中的数据(上下文Context)
我们先在lua.conf
文件的server部分,定义全局的模板文件位置信息:
set $template_root "/usr/resty/templates";
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jOrB4Rl0-1608514531622)(assets/image-20200311121942817.png)]
然后需要在/usr/resty
目录下创建templates
目录:
cd /usr/resty
mkdir templates
3.6.3.模板渲染
在/usr/resty/conf/lua.conf
中定义一个location映射:
location ~ /lua_template/(.*) {
# 关闭lua代码缓存
lua_code_cache off;
# 指定请求交给lua/test_template.lua脚本来处理
content_by_lua_file lua/test_template.lua;
}
新建test_template.lua
文件,编写脚本:
cd /usr/resty
vi lua/test_template.lua
内容如下:
-- 导入template模块,类似java导包
local template = require("resty.template")
--渲染模板需要的上下文(数据)
local context = {
title = "template test",
msg = "<h1>hello,"..ngx.var[1].."</h1>"
}
--渲染模板 ,指定模板文件名称,指定所需要的数据,table格式
template.render("t1.html", context)
在/usr/resty/templates
下新建模板文件:t1.html
:
vi /usr/resty/templates/t1.html
内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<p>{{msg}}</p>
<p>{*msg*}</p>
</body>
</html>
3.6.4.测试
重启:
nginx -p `pwd` -c conf/nginx.conf -s reload
通过浏览器访问:
http://192.168.150.101/lua_template/jack
3.7.OpenResty的Redis模块
渲染页面时,需要的数据要从redis中获取,而OpenResty中整合了操作Redis的模块,可以直接使用。
前置条件:你的Linux上已经安装了Redis
如果你的redis装在windows,关闭windows防火墙,知道window的ip地址
3.7.1.定义映射规则
现在,我们在lua.conf
中编写一条规则,来监听请求:
location ~ /lua_redis/(.*) {
# 关闭lua代码缓存
lua_code_cache off;
# 指定请求交给lua/test_template.lua脚本来处理
content_by_lua_file lua/test_redis.lua;
}
3.7.2.使用Redis模块功能
然后在lua
目录下创建文件:test_redis.lua
vi /usr/resty/lua/test_redis.lua
内容如下:
-- 导入redis模块
local redis = require("resty.redis")
-- 定义释放redis连接的方法
local function close_redis(red)
if not red then
return
end
local ok, err = red:close()
if not ok then
ngx.say("close redis error : ", err)
end
end
--创建实例
local red = redis:new()
--设置超时(毫秒)
red:set_timeout(1000)
--建立连接 ,这里要指定redis的安装的ip和端口
local ip = "127.0.0.1"
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then
ngx.say("connect to redis error : ", err)
return close_redis(red)
end
--调用API进行处理
ok, err = red:set("msg", "hello,"..ngx.var[1])
if not ok then
ngx.say("set msg error : ", err)
return close_redis(red)
end
--调用API获取数据
local resp, err = red:get("msg")
if not resp then
ngx.say("get msg error : ", err)
return close_redis(red)
end
ngx.say("msg : ", resp)
close_redis(red)
3.7.3.测试
重启:
nginx -p `pwd` -c conf/nginx.conf -s reload
通过浏览器访问:
http://192.168.150.101/lua_redis/jack
3.8.OpenResty内部请求代理
按照之前分析的实现原理,获取页面渲染数据时,我们先从redis拿,如果获取失败则请求tomcat,也就是之前我们准备的ly-page
服务。
那么如何在OpenResty内部主动发送一个http请求呢?
3.8.1.两种实现方案
在OpenResty中有两种主动发送http请求的方案:
- 利用http模块:利用第三方提供的http模块工具,模拟一个http请求
- 利用内部请求代理:利用nginx自带的capture功能
这里我们使用nginx的capture功能,也就是内部请求代理
3.8.2.内部请求代理
nginx的capture功能语法如下:
local resp = ngx.location.capture("/path",{
method = ngx.HTTP_GET, #请求方式
args = {a=1,b=2}, #get方式传参数
body = "c=3&d=4" #post方式传参数
});
返回的响应内容包括:
- resp.status:响应状态码
- resp.header:响应头,是一个table
- resp.body:响应体,就是响应数据
不过,capture功能发起的请求只能指向nginx的内部,不能访问外部请求(例如百度)。该怎么解决?
我们可以将capture请求的地址指向一个内部的location,然后在这个location中做反向代理,指向目标地址(外部网站地址)。
3.8.3.示例
假设我们希望在nginx内部向百度发请求,然后把结果输出到页面,大概思路是这样的:
- 利用capture向某个路径发请求,指向内部的一个location,比如
/baidu
- 定义个location,接收
/baidu
开头的请求 - 将请求反向代理到 https://www.baidu.com
1)定义location
我们先定义一个内部的location,用来处理内部请求,然后反向代理到百度。修改lua.conf文件,添加内容:
location ~ /baidu/(.*) {
# 限制不允许外部访问
# 重写路径,去掉路径前面的 /baidu
rewrite /baidu(/.*) $1 break;
# 禁止响应体压缩
proxy_set_header Accept-Encoding '';
# 反向代理到百度
proxy_pass https://www.baidu.com;
}
2)内部代理
然后我们再次修改lua.conf,添加一个location,作为测试接口:
location ~ /lua_http/(.*) {
# 关闭lua代码缓存
lua_code_cache off;
# 指定请求交给lua/test_http.lua脚本来处理
content_by_lua_file lua/test_http.lua;
}
这里接收请求,并执行lua/test_http.lua
这个文件。我们新建一个lua/test_http.lua
文件,内容如下:
-- 向 /baidu 这个location发请求,并且携带请求参数
local resp = ngx.location.capture("/baidu/s?wd="..ngx.var[1], {
method = ngx.HTTP_GET
})
-- 查询失败的处理
if not resp then
ngx.say("request error");
end
-- 查询成功的处理,这里是打印响应体
ngx.say(resp.body)
3)测试
打开浏览器,访问:http://192.168.150.101/lua_http/hello,可以看到内容
3.8.实现商品详情页渲染
接下来我们利用OpenResty实现商品详情页渲染,大概需要这样的步骤:
- 监听用户请求,进入定义好的lua脚本
- lua脚本中尝试读取redis数据
- 读取数据失败,尝试从
ly-page
微服务读取数据- 获取数据失败:返回404
- 获取数据成功:开始渲染
- 把数据和html模板交给template模块渲染,然后返回
我们要做的事情包括:
-
配置内部请求代理
-
定义通用工具模块:
- 访问Redis的工具
- 访问
ly-page
的http工具
-
定义商品详情页面html模板
-
编写商品页面请求的路径映射
-
编写处理请求,查询数据,处理数据,渲染模板的lua脚本
3.8.1.内部请求代理配置
我们计划采用内部请求代理实现对微服务的访问,因此需要定义一个location,拦截内部请求,转发到ly-page
微服务。这里我们约定,这个内部的location地址为:/backend/*
为了与之前的demo分离,我们修改nginx.conf
,注释以前的lua.conf
文件,并添加一个新的leyou.conf
:
vi /usr/resty/conf/nginx.conf
内容如下:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; #c模块
lua_shared_dict shared_data 20m; #共享全局变量,在所有worker间共享
default_type text/html; # 默认响应类型是html
#include lua.conf; # 引入一个lua.conf文件
include leyou.conf; # 引入一个leyou.conf文件
}
然后在/usr/resty/conf/leyou.conf
中编写location监听:
vi /usr/resty/conf/leyou.conf
内容如下:
upstream backend {
server 192.168.150.1:8084 max_fails=5 fail_timeout=10s weight=1;
keepalive 100;
}
server {
listen 80;
set $template_root "/usr/resty/templates";
# 我们要求内部请求以 /backend开头,与其他请求区分
location ~ /backend/(.*) {
#internal;
keepalive_timeout 30s;
keepalive_requests 1000;
#支持keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
rewrite /backend(/.*) $1 break;
proxy_pass_request_headers off;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
proxy_pass http://backend;
}
}
注意,上面配置的backend集群中,ip地址必须是你自己的宿主机地址。
请求处理大概过程:
-
假如我们内部访问:/backend/page/spu/1
-
地址被监听到后,会处理成:http//backend/page/spu/1
-
而后进入 backend的upstream集群
- 集群默认利用轮询策略对集群负载均衡,例如本例中的地址:192.168.150.1:8084
- 发送请求到:http//192.168.150.1:8084/page/spu/1,就是宿主机
-
这样就被宿主机的
ly-page
微服务接收到了
重启nginx:
nginx -p `pwd` -c conf/nginx.conf -s reload
测试,在浏览器访问:http://192.168.150.101/backend/page/spu/5
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pyanFZPZ-1608514531626)(assets/image-20200311155222813.png)]
3.8.2.编写通用工具模块
接下来,编写一个通用的工具模块,方便后期连接Redis,查询tomcat
脚本要定义到/usr/local/openresty/lualib
目录,因为这里的lua会被扫描到模块库,供其它脚本共享使用。
新建脚本文件
vi /usr/local/openresty/lualib/common.lua
内容如下:
-- 导入redis模块
local redis = require("resty.redis")
-- 日志
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
-- 关闭redis连接的工具方法
local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx_log(ngx_ERR, "set redis keepalive error : ", err)
end
end
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
-- 获取一个连接
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect(ip, port)
if not ok then
ngx_log(ngx_ERR, "connect to redis error : ", err)
return close_redis(red)
end
-- 查询redis
local resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx_log(ngx_ERR, "get redis content error : ", err)
return close_redis(red)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
end
close_redis(red)
return resp
end
-- 查询http请求的方法,path是请求路径,args是参数,table格式
local function read_http(path, args)
-- 默认查询地址走 /backend/page/,内部转发到8084端口
local resp = ngx.location.capture("/backend/page"..path, {
method = ngx.HTTP_GET,
args = args
})
-- 查询失败的处理
if not resp then
ngx_log(ngx_ERR, "request error")
return
end
-- 返回状态码不是200就报错
if resp.status ~= 200 then
ngx_log(ngx_ERR, "request error, status :", resp.status)
return
end
return resp.body
end
-- 先查询redis,如果失败,再查询http
local function read_data(ip, port, key, path, args)
-- 查询redis
local resp,err = read_redis(ip, port, key);
if not resp then
-- redis查询失败
ngx_log(ngx_ERR, "redis not found, try http query, key: ", key)
-- 查询http
resp,err = read_http(path, args)
end
if not resp then
-- 查询http失败
ngx_log(ngx_ERR, "http not found, path: ", path , ", args: ", args)
return nil
end
-- 查询成功
return resp
end
-- 将方法导出
local _M = {
read_redis = read_redis,
read_http = read_http,
read_data = read_data
}
return _M
核心方法有两个:
- read_redis(ip, port, keys):查询redis数据,参数:
- ip:就是redis的ip地址
- port:就是redis的端口
- keys:查询用到的key,数组,可以同时查询多个
- read_http(path, args):http请求查询,内部会转发到
ly-page
参数:- path:请求路径,方法内部会在path前拼接:/backend/page
- args:请求参数,table类型
3.8.3.页面模板
我已经提前写好了一个页面模板,在课前资料中获取:item.html
其中需要的参数包括:
- name:商品spu,需要的是spu中的name,分类id、品牌id
- skuList:商品spu下的sku
- detail:商品详情
- categories:商品分类
- brand:品牌
- specs:规格组包含规格参数
3.8.4.数据处理脚本
下面,我们编写数据处理的脚本。
脚本要定义到/usr/local/openresty/lualib
目录,因为这里的lua会被扫描到模块库,供其它脚本共享使用。
新建脚本文件
vi /usr/resty/lua/item.lua
内容:
-- 导入模块
local common = require("common")
local read_redis = common.read_redis
local read_http = common.read_http
local cjson = require("cjson")
local template = require("resty.template")
-- 常用变量和方法
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_exit = ngx.exit
local ngx_print = ngx.print
local ngx_re_match = ngx.re.match
-- 获取商品id
local spuId = ngx.var.spuId
-- 获取spu
local spuKey = "page:spu:id:"..spuId
local spuInfoStr = read_redis("127.0.0.1", 6379, spuKey)
if not spuInfoStr then
ngx_log(ngx_ERR, "redis not found spu info, back to http, spuId : ", spuId)
spuInfoStr = read_http("/spu/"..spuId, {})
end
if not spuInfoStr then
ngx_log(ngx_ERR, "http not found spuInfoStr info, spuId : ", spuId)
return ngx_exit(404)
end
-- 获取sku
local skuKey = "page:sku:id:"..spuId
local skuInfoStr = read_redis("127.0.0.1", 6379, skuKey)
if not skuInfoStr then
ngx_log(ngx_ERR, "redis not found sku info, back to http, spuId : ", spuId)
skuInfoStr = read_http("/sku/"..spuId, {})
end
if not skuInfoStr then
ngx_log(ngx_ERR, "http not found skuInfoStr info, spuId : ", spuId)
return ngx_exit(404)
end
-- 获取spuDetail
local detailKey = "page:detail:id:"..spuId
local detailInfoStr = read_redis("127.0.0.1", 6379, detailKey)
if not detailInfoStr then
ngx_log(ngx_ERR, "redis not found detail info, back to http, spuId : ", spuId)
detailInfoStr = read_http("/detail/"..spuId, {})
end
if not detailInfoStr then
ngx_log(ngx_ERR, "http not found detailInfoStr info, spuId : ", spuId)
return ngx_exit(404)
end
-- 获取categories
local spuInfo = cjson.decode(spuInfoStr)
local cid3 = spuInfo["categoryIds"][3]
local categoryKey = "page:category:id:"..cid3
local categoryStr = read_redis("127.0.0.1", 6379, categoryKey)
if not categoryStr then
local idStr = table.concat(spuInfo["categoryIds"],",");
ngx_log(ngx_ERR, "redis not found category info, back to http, categoryIds : ", idStr)
categoryStr = read_http("/categories/", {ids = idStr})
end
if not categoryStr then
ngx_log(ngx_ERR, "http not found categoryStr info, categoryId : ", cid3)
return ngx_exit(404)
end
-- 获取品牌
local brandId = spuInfo["brandId"]
local brandKey = "page:brand:id:"..brandId
local brandStr = read_redis("127.0.0.1", 6379, brandKey)
if not brandStr then
ngx_log(ngx_ERR, "redis not found brand info, back to http, brandId : ", brandId)
brandStr = read_http("/brand/"..brandId, {})
end
if not brandStr then
ngx_log(ngx_ERR, "http not found brandStr info, brandId : ", brandId)
return ngx_exit(404)
end
-- 获取规格
local specKey = "page:spec:id:"..cid3
local specStr = read_redis("127.0.0.1", 6379, specKey)
if not specStr then
ngx_log(ngx_ERR, "redis not found spec info, back to http, cid3 : ", cid3)
specStr = read_http("/spec/"..cid3, {})
end
if not specStr then
ngx_log(ngx_ERR, "http not found specStr info, cid3 : ", cid3)
return ngx_exit(404)
end
-- 组织数据
local context = {
name = spuInfo["name"],
skuList = skuInfoStr,
detail = detailInfoStr,
categories = categoryStr,
brand = brandStr,
specs = specStr
}
--渲染模板
template.render("item.html", context)
或者:
-- 导入模块
local common = require("common")
local read_redis = common.read_redis
local read_http = common.read_http
local read_data = common.read_data
local cjson = require("cjson")
local template = require("resty.template")
-- 常用变量和方法
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_exit = ngx.exit -- 控制返回状态
local ngx_print = ngx.print
local ngx_re_match = ngx.re.match
local ip = "127.0.0.1"
local port = 6379
-- 获取商品id
local spuId = ngx.var.spuId
-- 查询spu
local spuKey = "page:spu:id:"..spuId
local spuInfoStr = read_data(ip, port, spuKey, "/spu/"..spuId, {})
if not spuInfoStr then
ngx_exit(404)
end
-- 查询sku
local skuKey = "page:sku:id:"..spuId
local skuInfoStr = read_data(ip, port, skuKey, "/sku/"..spuId, {})
if not skuInfoStr then
ngx_exit(404)
end
-- 查询spuDetail
local detailKey = "page:detail:id:"..spuId
local detailInfoStr = read_data(ip, port, detailKey, "/detail/"..spuId, {})
if not detailInfoStr then
ngx_exit(404)
end
-- 查询category
local spuInfo = cjson.decode(spuInfoStr)
local cids = spuInfo.categoryIds
local categoryKey = "page:category:id:"..cids[3] -- {ids='1,2,3'}
local categoryInfoStr = read_data(ip, port, categoryKey, "/categories", {ids=table.concat(cids, ",")})
if not categoryInfoStr then
ngx_exit(404)
end
-- 查询brand
local brandKey = "page:brand:id:"..spuInfo.brandId
local brandInfoStr = read_data(ip, port, brandKey, "/brand/"..spuInfo.brandId, {})
if not brandInfoStr then
ngx_exit(404)
end
-- 查询spec
local specKey = "page:spec:id:"..cids[3]
local specInfoStr = read_data(ip, port, specKey, "/spec/"..cids[3], {})
if not specInfoStr then
ngx_exit(404)
end
-- 渲染模板页面
local data = {
skuList=skuInfoStr,
name=spuInfo.name,
categories=categoryInfoStr,
brand=brandInfoStr,
detail=detailInfoStr,
specs=specInfoStr,
}
template.render("item.html", data);
3.8.5.路径映射
最后,我们监听页面请求,把商品页面交给模板来处理.
在/usr/rest/conf/leyou.conf
中添加映射:
location ~ /item/(\d+).html$ {
# 获取路径参数
set $spuId $1;
# 禁止除了www.leyou.com以外的请求访问
#if ($host !~ "^www\.leyou\.com$") {
# return 403;
#}
# 关闭缓存/打开缓存
lua_code_cache on;
default_type 'text/html';
charset utf-8;
# 指定请求交给lua/item.lua脚本来处理
content_by_lua_file lua/item.lua;
}
3.8.6.修改宿主机路径映射
现在,OpenResty已经准备就绪,不过我们在浏览器中输入:
http://www.leyou.com/item/141.html
这个商品地址时,目前依然走的是ly-portal
。我们需要把请求地址修改到你的虚拟机地址,例如我的地址是:192.168.150.101
修改宿主机中的leyou.conf
文件,修改原来的 www.leyou.com的域名解析部分:
server {
listen 80;
server_name www.leyou.com;
location /item {
# 携带hosts地址,避免因代理导致host丢失
proxy_set_header Host $host;
proxy_pass http://192.168.150.101;
}
location / {
root C:\develop\idea-space\leyou3\leyou-portal;
}
}
重启:
nginx -p `pwd` -c conf/nginx.conf -s reload
通过浏览器访问:
http://www.leyou.com/item/127.html
3.8.7.优化(理解即可)
虽然已经实现了页面静态化,不过依然有值的优化的地方:
- 在Nginx中设置本地缓存,把几乎不变的数据直接存储在nginx内部,例如:
- 商品分类数据
- 品牌数据
- 规格参数数据
- 在nginx中对生成的页面做缓存或静态化,做CDN服务,页面不变的时候,减少渲染对CPU的消耗
- 随着商品数据的日益增多,Redis可能难以支持海量商品信息,此时可以用SSDB来代替,SSDB存储基于磁盘存储,查询性能与Redis差不多,因此可以作为海量数据的缓存库
参考:本地缓存的实现:
修改nginx.conf,配置本地缓存大小和名称:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; #c模块
#本地缓存,名称叫做:item_local_cache,大小50m
lua_shared_dict item_local_cache 50m;
default_type text/html; # 默认响应类型是html
#include lua.conf; # 引入一个lua.conf文件
include leyou.conf; # 引入一个leyou.conf文件
}
修改common.lua,添加数据查询方法:
-- 导入redis模块
local redis = require("resty.redis")
-- 配置商品的本地缓存
local local_cache = ngx.shared.item_local_cache
-- 日志
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
-- 读取本地缓存
local function cache_get(key)
if not local_cache then
return nil
end
return local_cache:get(key)
end
-- 写入本地缓存
local function cache_set(key, value)
if not local_cache then
return nil
end
return local_cache:set(key, value, 10 * 60) --10分钟
end
local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx_log(ngx_ERR, "set redis keepalive error : ", err)
end
end
-- 查询本地缓存,没有则查询redis, ip和port是redis地址,key是查询的key
local function read_cache(ip, port, key)
-- 尝试读本地缓存
local resp = cache_get(key)
-- ngx_log(ngx_ERR, "debug local cache data : ", resp)
if not resp then
ngx_log(ngx_ERR, "read local cache fail , key", key)
-- 获取一个redis连接
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect(ip, port)
if not ok then
ngx_log(ngx_ERR, "connect to redis error : ", err)
return close_redis(red)
end
-- 利用get查询
resp, err = red:get(key)
end
-- 查询失败处理
if not resp then
ngx_log(ngx_ERR, "get redis content error : ", err)
return close_redis(red)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
end
cache_set(key, resp)
close_redis(red)
return resp
end
-- 查询redis的方法 ip和port是redis地址,keys是查询的key,数组格式
local function read_redis(ip, port, key)
-- 获取一个连接
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect(ip, port)
if not ok then
ngx_log(ngx_ERR, "connect to redis error : ", err)
return close_redis(red)
end
local resp = nil
-- 利用get查询
resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx_log(ngx_ERR, "get redis content error : ", err)
return close_redis(red)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
end
close_redis(red)
return resp
end
-- 查询http请求的方法,path是请求路径,args是参数,table格式
local function read_http(path, args)
-- 默认查询地址走 /backend/page/,内部转发到8083端口
local resp = ngx.location.capture("/backend/page"..path, {
method = ngx.HTTP_GET,
args = args
})
-- 查询失败的处理
if not resp then
ngx_log(ngx_ERR, "request error")
return
end
-- 返回状态码不是200就报错
if resp.status ~= 200 then
ngx_log(ngx_ERR, "request error, status :", resp.status)
return
end
return resp.body
end
-- 将方法导出
local _M = {
read_redis = read_redis,
read_cache = read_cache,
read_http = read_http
}
return _M
改造item.lua
,将分类、品牌、规格查询走本地缓存
-- 导入模块
local common = require("common")
local read_redis = common.read_redis
local read_http = common.read_http
local read_cache = common.read_cache
local cjson = require("cjson")
local template = require("resty.template")
-- 常用变量和方法
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_exit = ngx.exit
local ngx_print = ngx.print
local ngx_re_match = ngx.re.match
-- 获取商品id
local spuId = ngx.var.spuId
-- 获取spu
local spuKey = "page:spu:id:"..spuId
local spuInfoStr = read_redis("127.0.0.1", 6379, spuKey)
if not spuInfoStr then
ngx_log(ngx_ERR, "redis not found spu info, back to http, spuId : ", spuId)
spuInfoStr = read_http("/spu/"..spuId, {})
end
if not spuInfoStr then
ngx_log(ngx_ERR, "http not found spuInfoStr info, spuId : ", spuId)
return ngx_exit(404)
end
-- 获取sku
local skuKey = "page:sku:id:"..spuId
local skuInfoStr = read_redis("127.0.0.1", 6379, skuKey)
if not skuInfoStr then
ngx_log(ngx_ERR, "redis not found sku info, back to http, spuId : ", spuId)
skuInfoStr = read_http("/sku/"..spuId, {})
end
if not skuInfoStr then
ngx_log(ngx_ERR, "http not found skuInfoStr info, spuId : ", spuId)
return ngx_exit(404)
end
-- 获取spuDetail
local detailKey = "page:detail:id:"..spuId
local detailInfoStr = read_redis("127.0.0.1", 6379, detailKey)
if not detailInfoStr then
ngx_log(ngx_ERR, "redis not found detail info, back to http, spuId : ", spuId)
detailInfoStr = read_http("/detail/"..spuId, {})
end
if not detailInfoStr then
ngx_log(ngx_ERR, "http not found detailInfoStr info, spuId : ", spuId)
return ngx_exit(404)
end
-- 获取categories
local spuInfo = cjson.decode(spuInfoStr)
local cid3 = spuInfo["categoryIds"][3]
local categoryKey = "page:category:id:"..cid3
local categoryStr = read_cache("127.0.0.1", 6379, categoryKey)
if not categoryStr then
local idStr = table.concat(spuInfo["categoryIds"],",");
ngx_log(ngx_ERR, "redis not found category info, back to http, categoryIds : ", idStr)
categoryStr = read_http("/categories/", {ids = idStr})
end
if not categoryStr then
ngx_log(ngx_ERR, "http not found categoryStr info, categoryId : ", cid3)
return ngx_exit(404)
end
-- 获取品牌
local brandId = spuInfo["brandId"]
local brandKey = "page:brand:id:"..brandId
local brandStr = read_cache("127.0.0.1", 6379, brandKey)
if not brandStr then
ngx_log(ngx_ERR, "redis not found brand info, back to http, brandId : ", brandId)
brandStr = read_http("/brand/"..brandId, {})
end
if not brandStr then
ngx_log(ngx_ERR, "http not found brandStr info, brandId : ", brandId)
return ngx_exit(404)
end
-- 获取规格
local specKey = "page:spec:id:"..cid3
local specStr = read_cache("127.0.0.1", 6379, specKey)
if not specStr then
ngx_log(ngx_ERR, "redis not found spec info, back to http, cid3 : ", cid3)
specStr = read_http("/spec/"..cid3, {})
end
if not specStr then
ngx_log(ngx_ERR, "http not found specStr info, cid3 : ", cid3)
return ngx_exit(404)
end
-- 组织数据1
local context = {
name = spuInfo["name"],
skuList = skuInfoStr,
detail = detailInfoStr,
categories = categoryStr,
brand = brandStr,
specs = specStr
}
--渲染模板 1
template.render("item.html", context)