用openResty做一个小功能:实现数据替换跟缓存

导读:demo都是在我本地一个域名下完成的。但是文中分了。m.yanshinian.com,api.yanshinian.com,shopapi.yanshinian.com。别搞晕了。
文中涉及到openResty、vue、laravel、lua、nginx缓存、本地dns配置的使用。都是简单的使用。

数据替换

为什么选择openResty呢?

我们已经有大量页面使用了静态数据。开发动态接口,要兼容以前的数据(比如说:以前目录在a目录下面,我通过变换链接,用location匹配,还是去拿a目录下面的静态数据处理),并且上线紧急。

openResty的好处就不用说了。下面的文章是我根据实际工作,编了一个demo(原理一样)。

描述下业务场景

作为一个运营后台跟商城后台是分开的两套系统。有一天产品经理说,我们要获取商城的数据生成商品静态页展示。那么在运营后台需要创建一张商品表用来保存从商城接口的获取的商品数据。于是小明搞定了,后台生成json文件作为数据源。前台通过VUE去渲染。

有一天产品经理,过来说,原来的纯静态不满足了。比如我要展示库存,要做成动态的。于是,小明用openResty做动态的展示。

实现思路

  1. 获取json文件

2.拿到所有商品的id

3.用id数组请求接口,拿到数据

4.做数据替换并输出

相关demo编写

使用lua要注意的问题

1.lua文件路径

程序中引入了第三方的模块。记得设置文件查找路径。比如,我使用了第三方的http请求库——lua-resty-http。如果没有设置路径。那么会报如下错误,它会从默认的路径去找。

no file '/usr/local/openresty/site/lualib/resty/http.lua'

no file '/usr/local/openresty/site/lualib/resty/http/init.lua'

no file '/usr/local/openresty/lualib/resty/http.lua'

no file '/usr/local/openresty/lualib/resty/http/init.lua'

no file '/usr/local/openresty/site/lualib/resty/http.so'

no file '/usr/local/openresty/lualib/resty/http.so'

no file '/usr/local/openresty/site/lualib/resty.so'

no file '/usr/local/openresty/lualib/resty.so'

no file '/usr/local/openresty/site/lualib/resty/http.lua'

no file '/usr/local/openresty/site/lualib/resty/http/init.lua'

no file '/usr/local/openresty/lualib/resty/http.lua'

能从错误中看出来会去安装后openresty目录中找。/usr/local/openresty/site/lualib/ 和'/usr/local/openresty/lualib/

所以要去nginx.conf配置文件中,在http模块里面去设置如下

lua_package_path "/usr/local/openresty/lualib/?.lua;/usr/local/openresty/lualib/lua-resty-http/lib/?.lua    ;;"; #文件查找路径

lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; # 模块路径

看了上面的配置了吧。我们把lua-resty-http第三方库放到了。openresty/lualib下面了。当然你可以放到项目底下。配置好对应路径也是可以的。

2.location 中 content_by_lua_file (放在它上面)之前需要设置 dns的解析,如果做了内网的限制,那么找个内网的DNS地址(安装配置Dnsmasq )即可,设置如下

resolver 114.114.114.114  8.8.8.8; #针对外网

开始开发吧

涉及到的知识:laravel框架、vue、luajit、openresty、一张简单的商品表

1.准备数据

商品表如下

CREATE TABLE  `yan_product` (
    `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
    `product_id` BIGINT(20) NOT NULL DEFAULT '0' COMMENT '商品ID',
    `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '商品标题',
    `img` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '商品图片',
    `price` FLOAT NOT NULL DEFAULT '0' COMMENT '商品价格',
    `sales_num` INT(11) NOT NULL DEFAULT '0' COMMENT '销量',
    `stock_num` INT(11) NOT NULL DEFAULT '0' COMMENT '库存数量',
    PRIMARY KEY (`id`),
    INDEX `idx_product_id` (`product_id`)
)
COMMENT='商品表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=5
;

随便在淘宝里面随便找了个商品,借用下简单的数据。(假装这条数据是从商城api调用过来的)

INSERT INTO `yan_product` (`product_id`, `title`, `img`, `price`, `sales_num`, `stock_num`) VALUES (542456857815, '甜而不腻回味无穷', 'gju1.alicdn.com/tps/i4/1130806076898038137/TB2nkXyzItnpuFjSZFvXXbcTpXa_!!6000000001308-0-jupush.jpg_360x360Q50.jpg', 28.8, 120, 122);

2.准备php代码

laravel框架创建一个控制器(比如叫做ProductController),写一个生成json的程序,如下:

class ProductController extends Controller
{
    public function buildjson()
    {
        $productList = DB::select('select * from yan_product');
        $json = json_encode([
            'productList' => $productList,
        ]);
        $dir = public_path() . '/product' ;
        if (!file_exists($dir)) {
            mkdir($dir, 0777);
        }
        $path = $dir . '/product.json';
        File::put($path, 'callBack(' . $json . ')');
    }
}

上面代码就是,把json保存到了,public文件夹下面,具体路径为 public/product/product.json (product文件专门存放商品的json)。后面拼接 callBack 是jsonp跨域请求方式。假设我们接口域名是api.yanshinian.com 前台域名是m.yanshinian.com。那么m.yanshinian.com 请求api 网站(链接为 api.yanshinian.com),会涉及到跨域的问题。

配置路由

Route::Any('product/buildjson', ['as' => 'product.buildjson', 'uses' => 'App\Modules\Product\Controllers\ProductController@buildjson']);

然后执行 buildjson,在public/product 目录下生成一个product.json 文件

3.准备前台的页面。使用vue代码

引入相应的 js文件

https://unpkg.com/[email protected]/dist/vue.js

https://cdn.jsdelivr.net/npm/[email protected]

主要代码如下:

很粗糙的html

  • productId: {{product.product_id}}

    title: {{product.title}}

    img: {{product.img}}

很粗糙的vue

var app = new Vue({
        el: '#app',
        mounted: function() {
            alert('加载中....');
            this.$http.jsonp('http://api.yanshinian.com/product/product.json',{
                jsonp:'callback',
                jsonpCallback: 'callback'
            }).then(function(res) {
                console.log(this.productList)
                  this.productList = JSON.parse(res.bodyText).productList
                  console.log(this.productList)

            }) 
        },
        data: {
            productList: ''
        },
        methods: {
            getProductList: function() {
                alert(23);
            }
        }
 });

jsonp请求到数据,并且渲染出来了。糙图如下:

用openResty做一个小功能:实现数据替换跟缓存_第1张图片
商品列表.png

但是,它是纯静态的。接下来我们开始用lua替换静态页面。变成动态的数据。

4.假装开发一个商城的接口(下一步会用lua脚本调用)。

public function getProductList(Request $request)
{
    $ids = trim($request->input('ids'),',');
    $productList = DB::select('select product_id, stock_num from yan_product where product_id in (' . $ids . ')');
    $data = [];
    foreach ($productList as $v) {
        $data[$v->product_id]= [
            'stock_num'=>$v->stock_num
        ];
    }
    return $data;
}

配置路由

Route::Any('product/productlist', ['as' => 'product.productlist', 'uses' => 'App\Modules\Product\Controllers\ProductController@getProductList']);

post请求,参数ids = '542456857815,542456857817'(ids 是product_id)

{"542456857815":{"stock_num":234},"542456857817":{"stock_num":122}}

5.配置location,用lua去过滤一遍

location /product/filter/product.json  { 
    resolver 127.0.0.1; 
    content_by_lua_file /home/nobody/lua/product.lua; #路径自己随意
}

这里有个注意事项就是 resolver,需要配置本地的dns解析。安装Dnsmasq并配置(参考链接:《Dnsmasq安装与配置》http://www.360doc.com/content/14/0913/13/8314158_409140713.shtml) ,在我配置的过程中,没配置对外网的解析,导致了git代码不能提交(报错如下ssh: Could not resolve hostname git.xxxx.org: Name or service not known,需要这样弄下,echo 'nameserver 8.8.8.8' > /etc/resolv.dnsmasq.conf)

  1. lua代码 如下
local json = require "cjson"
-- 获取 body内容
-- local bodyJson = ngx.arg[1] -- 这条语句是我觉得body_filter 阶段可以过滤,但事实上,不能发送http请求。所以还是选择在content_by_lua_file 阶段处理

local productListRes = ngx.location.capture("/product/product.json") -- 子查询
local bodyJson = productListRes.body
-- cjson解析
local data = json.decode(string.sub(bodyJson,10,-2))


-- 获取ids
local ids = "";
-- ngx.log(ngx.ERR, json.encode(data));
for k,v in pairs(data.productList) do
    ids = ids..v.product_id..','
end
-- 发送http请求,拿到数据
local http = require "resty.http"

    local httpc = http.new()
function http_request(method, url, param_str)
    local res, err = httpc:request_uri(url, {
        method = method,
        body = param_str,
        headers = {
            ["Content-Type"] = "application/x-www-form-urlencoded",
        },
        ssl_verify = false,
    })

    if not res then
        ngx.log(ngx.ERR, "failed to request: "..err)
        return false;
    end
    return res;
end

local url = "http://shopapi.yanshinian.com/product/productlist"

local res = http_request("POST", url, "ids="..ids)

local productList = json.decode(res.body)
ngx.log(ngx.ERR, json.encode(productList));
-- 解析数据,然后遍历 body,替换库存
for k,v in pairs(data.productList) do
     v.stock_num = productList[""..v.product_id]['stock_num']
end
-- 输出结果

body = "callback(" .. json.encode(data) .. ")"

ngx.say(body)

7.开始验证这个小功能

我们把js中的链接更换成http://api.yanshinian.com/product/filter/product.json

在增加一个展示的库存字段。

img: {{product.img}}

更改数据库。(由于我们这里是demo,所以我们假设,运营后台的数据库,跟商城是同一个。真实环境,当然是数据库跟代码都分开的)。

验证是ok的。

最后还想说的

lua代码中有一行注意下local bodyJson = ngx.arg[1]。body_filter阶段是可以对响应数据进行过滤,比如截断、替换。ngx.arg[1]是用来获取响应数据(但也可能获取的不全,这是另一个问题了)。但是这个阶段有问题,当我拿到ids参数,实例化http,并发送请求。报错了。报错如下。

2017/08/05 22:08:56 [error] 31159#0: *581 failed to run body_filter_by_lua*: ...local/openresty/lualib/lua-resty-http/lib/resty/http.lua:121: API disabled in the context of body_filter_by_lua*
3797 stack traceback:
3798     [C]: in function 'ngx_socket_tcp'
3799     ...local/openresty/lualib/lua-resty-http/lib/resty/http.lua:121: in function 'new'
3800     /home/nobody/lua/product.lua:20: in function 'http_request'
3801     /home/nobody/lua/product.lua:39: in function  while sending response to client, client: 192.168.95.1, server: 
......................

也就是说在这个阶段不能发送接口请求。另外,执行ngx.say 也是不允许的。所以改用content_by_lua 这个阶段 再深入的细节,可能需要查下。

缓存

缓存是使用 了nginx的特性

修改配置文件即可。

为什么用缓存呢?之前文章说了,调用商城接口,库存通常是实时的,量大的情况容易让数据库垮掉。小明给产品反馈之后。产品说好可以不那么实时。这样,加上缓存数据库压力就小了。

修改 配置文件

nginx.conf 设置 缓存路径

proxy_cache_path /home/nobody/cache levels=1:2 keys_zone=my-cache:8m ina    ctive=1h max_size=10m;proxy_cache_path /home/nobody/cache levels=1:2 keys_zone=my-cache:8m ina    ctive=1h max_size=10m;

api.yanshinian.com.conf,下面增加如下代码。

 location ^~/product/filter/cache/product.json {
    proxy_pass http://127.0.0.1:8086/product/filter/product.json;
    proxy_cache my-cache;
    proxy_cache_lock on;
    proxy_cache_valid 200 304 1m;#设置一分钟的缓存
    proxy_cache_key $uri$is_args$args;
    add_header Nginx-Cache "$upstream_cache_status";
}

我们把js请求的链接改成 http://api.yanshinian.com//product/filter/cache/product.json。进行请求。抓包看下响应头Nginx-Cache。发现Nginx-Cache:HIT。说明缓存成功。

附赠一个函数

在实际开发中,商城的接口,需要批量调用,做了限制,但是lua 没有array_chunk(不像php)的函数,自己封装了一个

function table_chunk(stable, count)
        local newTable = {};
        local tlen = #stable;
        local tempTable = {};
        for k,v in pairs(stable) do
                table.insert(tempTable, v);
                local tempLen = #tempTable;
                if(tempLen % count == 0)  then
                        table.insert(newTable, tempTable);
                        tempTable = {};
                end
        end
        table.insert(newTable, tempTable);
        return newTable;
end

ps.openResty是身边小伙提的方案,我呢,自己编个代码无非就是回顾下所学的。哈哈。

参考链接:

《 nginx的proxy_cache缓存相关配置》http://hnr520.blog.51cto.com/4484939/1686896

感兴趣的话可以关注我的公众号——言十年的日常

qrcode_for_gh_20daf6d0ff9e_430 (2).jpg

你可能感兴趣的:(用openResty做一个小功能:实现数据替换跟缓存)