用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/vue@2.1.10/dist/vue.js

https://cdn.jsdelivr.net/npm/vue-resource@1.3.4

主要代码如下:

很粗糙的html

<div id="app">

    <ul>
        <li v-for="product in productList">
            <p>productId: {{product.product_id}}</p>
            <p>title: {{product.title}}</p>
            <p>img: {{product.img}}</p>
        </li>
    </ul>
</div>

很粗糙的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请求到数据,并且渲染出来了。糙图如下:

商品列表.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

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

<p>img: {{product.img}}</p>

更改数据库。(由于我们这里是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 </home/nobody/lua/product.lua:1> 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容