OpenResty学习总结

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)  

版权声明:本文为qq_41656303原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。