工具(二):Nginx 扩展 OpenResty

  • OpenResty 介绍
  • OpenResty 原理
  • Nginx 模块
  • Nginx的 lua插载点
  • 案例
    • 配置模板
    • nginx.conf
    • 通过Lua找到静态文件 — 商品页
    • 通过Lua 获取 redis 只返回 — 库存
    • test
  • 其他Demo
    • MysqlOps.lua
    • RedisExtOps.lua
    • redisOps.lua
  • 辅助工具类说明
    • 自动生成静态页
    • FTP 工具

Nginx_有5大优点,即模块化、事件驱动、异步、非阻塞、多进程单线程

Nginx 是C写的, 如何在Nginx中添加我们自己的业务逻辑 — OpenResty

OpenResty 介绍

OpenResty中文官网:https://openresty.org/cn/

安装

  • docker pull openresty/openresty
  • docker run -itd -v /data/openresty/conf:/usr/local/openresty/nginx/conf/:rw --name openresty -p 8000:80 openresty/openresty

OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关

OpenResty 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统

为什么要用Lua语言来做Nginx开发呢?

  • Lua的线程模型是单线程多协程的模式,而Nginx刚好是单进程单线程,天生的完美搭档
  • Lua是一种小巧的脚本语言,语法非常的简单
  • Redis中也是用Lua作为脚本语言

OpenResty 原理

在这里插入图片描述

  • Nginx
    Master 进程用于接收来自外界的信号,并向各 Worker 进程发送信号,同时监控 Worker 进程的工作状态。当 Worker 进程退出后(异常情况下),Master 进程也会自动重新启动新的 Worker 进程。Worker 进程则是外部请求真正的处理者。

    • Worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个 Worker 进程中处理,一个 Worker 进程不可能处理其它进程的请求
    • Worker 进程的个数是可以设置的,一般我们会设置与机器 CPU 核数一致。同时,Nginx 为了更好的利用多核特性,具有 CPU 绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效(CPU affinity)。所有的进程的都是单线程(即只有一个主线程)的,进程之间通信主要是通过共享内存机制实现的。
  • OpenResty
    OpenResty本质上是将 LuaJIT 的虚拟机嵌入到 Nginx 的管理进程和工作进程中,同一个进程内的所有协程都会共享这个虚拟机,并在虚拟机中执行Lua代码。在性能上,OpenResty接近或超过 Nginx 的C模块,而且开发效率更高。

Nginx 模块

Nginx 将HTTP请求的处理过程划分为多个阶段。这样可以使一个HTTP请求的处理过程由很多模块参与处理,每个模块只专注于一个独立而简单的功能处理,可以使性能更好、更稳定,同时拥有更好的扩展性。

  1. ngx_http_post_read_phase:接收到完整的http头部后处理的阶段,它位于uri重写之前。
  2. ngx_http_server_rewrite_phase:uri与location匹配前,修改uri的阶段,用于重定向。
  3. ngx_http_find_config_phase:根据uri寻找匹配的location块配置项阶段,该阶段使用重写之后的uri来查找对应的location,值得注意的是该阶段可能会被执行多次,因为也可能有location级别的重写指令。
  4. ngx_http_rewrite_phase:上一阶段找到location块后再修改uri,location级别的uri重写阶段,该阶段执行location基本的重写指令,也可能会被执行多次。
  5. ngx_http_post_rewrite_phase:防止重写url后导致的死循环,location级别重写的后一阶段,用来检查上阶段是否有uri重写,并根据结果跳转到合适的阶段。
  6. ngx_http_preaccess_phase:下一阶段之前的准备,访问权限控制的前一阶段,该阶段在权限控制阶段之前,一般也用于访问控制,比如限制访问频率,链接数等。
  7. ngx_http_access_phase:让http模块判断是否允许这个请求进入nginx服务器,访问权限控制阶段,比如基于ip黑白名单的权限控制,基于用户名密码的权限控制等。
    标准模块 ngx_access、第三方模块 ngx_auth_request 以及第三方模块 ngx_lua 的 access_by_lua 指令就运行在这个阶段。
  8. ngx_http_post_access_phase:访问权限控制的后一阶段,该阶段根据权限控制阶段的执行结果进行相应处理。
  9. ngx_http_try_files_phase:为访问静态文件资源而设置,try_files指令的处理阶段,如果没有配置try_files指令,则该阶段被跳过。
  10. ngx_http_content_phase:处理http请求内容的阶段,大部分http模块介入这个阶段,内容生成阶段,该阶段产生响应,并发送到客户端。
    Nginx 的 content 阶段是所有请求处理阶段中最为重要的一个,因为运行在这个阶段的配置指令一般都肩负着生成“内容”(content)并输出 HTTP 响应的使命。
  11. ngx_http_log_phase:log阶段处理,比如记录访问量/统计平均响应时间。log_by_lua 处理完请求后的日志记录阶段,该阶段记录访问日志。

以上11个阶段中,http无法介入的阶段有4个:
3)ngx_http_find_config_phase
5)ngx_http_post_rewrite_phase
8)ngx_http_post_access_phase
9)ngx_http_try_files_phase。
OpenResty在HTTP处理阶段基础上分别在Rewrite/Access阶段、Content阶段、Log阶段注册了自己的handler,加上系统初始阶段master的两个阶段,共11个阶段为Lua脚本提供处理介入的能力。

Nginx的 lua插载点

在这里插入图片描述

  • init_by_lua*:Master进程加载 Nginx 配置文件时运行,一般用来注册全局变量或者预加载Lua模块。
  • init_worker_by_lua*:每个worker进程启动时执行,通常用于定时拉取配置/数据或者进行后端服务的健康检查。
  • set_by_lua*:变量初始化。
  • rewrite_by_lua*:可以实现复杂的转发、重定向逻辑。
  • access_by_lua*:IP准入、接口权限等情况集中处理。
  • content_by_lua*:内容处理器,接收请求处理并输出响应。
  • header_filter_by_lua*:响应头部或者cookie处理。
  • body_filter_by_lua*:对响应数据进行过滤,如截断或者替换。
  • log_by_lua*:会话完成后,本地异步完成日志记录

案例

在这里插入图片描述

nginx.conf

worker_processes  7;        #nginx worker 数量
error_log logs/error.log;   #指定错误日志文件路径
events {
    
    
    worker_connections 65535; #进程最大可打开文件数 ulimit -n
}

http {
    
    

    include       mime.types;
    # 这个将为打开文件指定缓存,默认是没有启用的,max 指定缓存数量,
    # 建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。
    open_file_cache max=100 inactive=30s;

    # open_file_cache 指令中的inactive 参数时间内文件的最少使用次数,
    # 如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个
    # 文件在inactive 时间内一次没被使用,它将被移除。
    open_file_cache_min_uses 1;

    # 这个是指多长时间检查一次缓存的有效信息
    open_file_cache_valid 60s;

    #开启高效文件传输模式
    sendfile on;
    #提高I/O性能
    tcp_nodelay on;

    access_log  logs/access.log;

    #lua 模块
    lua_package_path "/usr/local/openresty/lua/?.lua;/usr/local/openresty/lualib/?.lua;;";
    #c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
    lua_code_cache on;

    # 共享字典,也就是本地缓存,名称叫做:stock_cache,大小1m
    lua_shared_dict stock_cache 1m;

    #秒杀确认页相关负载均衡
    upstream confirm {
    
    
         server xx.xx.xx.xx:xx;
    }

    #秒杀订单相关负载均衡
    upstream order {
    
    
         server xx.xx.xx.xx:xx;
    }

    server {
    
    
        #监听端口
        listen 80;

        charset utf-8;

        set $template_root /usr/local/openresty/tpl;

        location /test {
    
    
            default_type text/html;
            content_by_lua_block {
    
    
                ngx.say("泰勒斯说万物充满了神明,是让我们把神明拉下神座,从此诸神迎来了他们的黄昏")
            }
        }

        #产品静态模板化网页访问
        location /product {
    
    
            default_type text/html;
            content_by_lua_file lua/product.lua;
        }

        #静态资源访问
        location /static {
    
    
            root /usr/local/openresty;
            index index.html index.htm;
        }

        #秒杀确认页反向代理
        location /skcart {
    
    
            proxy_pass http://confirm;
        }

        #秒杀订单反向代理
        location /seckillOrder {
    
    
            proxy_pass http://order;
        }

        #秒杀产品当前库存
        location /cache/stock {
    
    
            # 默认的响应类型
            default_type application/json;
            # 响应结果由lua/stock.lua文件来处理
            content_by_lua_file lua/stock.lua;
        }

    }
}

配置模板

模板放在目录: /usr/local/openresty/lualib/resty
在这里插入图片描述

URL 处理

在这里插入图片描述

通过Lua找到静态文件 — 商品页

http://localhost:8000/product?flashPromotionId=9&promotionProductId=29&memberId=1

 #产品静态模板化网页访问
 location /product {
     default_type text/html;
     content_by_lua_file lua/product.lua;
 }

lua/product.lua

-- 导入lua-resty-template函数库
local template = require('resty.template')
local flashPromotionId = ngx.var.arg_flashPromotionId
ngx.log(ngx.ERR, "秒杀活动ID: ", flashPromotionId)
local promotionProductId = ngx.var.arg_promotionProductId
ngx.log(ngx.ERR, "秒杀产品ID: ", promotionProductId)
local templateName = "seckill_"..flashPromotionId.."_"..promotionProductId..".html"
local context = {
    
    
    memberId = ngx.var.arg_memberId,
    productId = promotionProductId,
    flashPromotionId = flashPromotionId
}
ngx.log(ngx.ERR, "渲染页面输出,获得当前用户ID: ", context.memberId)
template.render(templateName, context)

在这里插入图片描述

通过Lua 获取 redis 只返回 — 库存

  #秒杀产品当前库存
  location /cache/stock {
      # 默认的响应类型
      default_type application/json;
      # 响应结果由lua/stock.lua文件来处理
      content_by_lua_file lua/stock.lua;
  }

lua/stock.lua

-- 导入redisOps函数库
local redisOps = require('redisOps')
local read_redis = redisOps.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
-- 本地缓存的主要目的为库存检查,当商品的库存<=0时,提前终止秒杀
-- 这里从业务上来说,同样需要解决退单等引发的库存增加允许重新秒杀的情况,
-- 解决思路:同样可以订阅对应的Redis的channel,本次不做具体实现,Lua订阅Redis的Channel的参考代码写在RedisExtOps.lua中
local item_cache = ngx.shared.stock_cache

-- 封装查询函数
function read_data(key, expire)
    -- 查询本地缓存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
        -- 查询redis
        val = read_redis("x.x.x.x", 6379, "xxxxxx", key)
        -- 判断查询结果
        if not val then
            ngx.log(ngx.ERR, "redis查询失败,key: ", key)
            -- redis查询失败,给一个缺省值
            val = 0
        end
    end
    -- 查询成功,把数据写入本地缓存,expire秒后过期
    if tonumber(val) <= 0 then
        item_cache:set(key, val, expire)
    end
    -- 返回数据
    return val
end

-- 获取请求参数中的productId,也可以使用ngx.req.get_uri_args["productId"],req.get_uri_args在productId有多个时,会返回一个table
local product_id = ngx.var.arg_productId

-- 查询库存信息
local stock = read_data("miaosha:stock:cache:"..product_id, 3600)

-- 返回结果
ngx.say(cjson.encode(stock))

其他Demo

1. MysqlOps.lua

---
--- Desc: 演示对OpenResty中使用Lua对MySQL操作
--- Note:本Lua脚本借鉴了网络,未经测试,仅供参考,也不提供任何技术支持
---
local function close_db(db)
    if not db then
        return
    end
    db:close()
end

local mysql = require("resty.mysql")

local db, err = mysql:new()
if not db then
    ngx.say("new mysql error : ", err)
    return
end

db:set_timeout(1000)

local props = {
    
    
    host = "127.0.0.1",
    port = 3306,
    database = "mysql",
    user = "root",
    password = "123"
}

local res, err, errno, sqlstate = db:connect(props)

if not res then
    ngx.say("connect to mysql error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end

---------------------------------------
-- 执行SQL语句范例

local create_table_sql = "create table test(id int primary key auto_increment, ch varchar(100))"
res, err, errno, sqlstate = db:query(create_table_sql)
if not res then
    ngx.say("create table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end

local drop_table_sql = "drop table if exists test"
res, err, errno, sqlstate = db:query(drop_table_sql)
if not res then
    ngx.say("drop table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end

local insert_sql = "insert into test (ch) values('hello')"
res, err, errno, sqlstate = db:query(insert_sql)
if not res then
    ngx.say("insert error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end
res, err, errno, sqlstate = db:query(insert_sql)
ngx.say("insert rows : ", res.affected_rows, " , id : ", res.insert_id, "<br/>")


local update_sql = "update test set ch = 'hello2' where id =" .. res.insert_id
res, err, errno, sqlstate = db:query(update_sql)
if not res then
    ngx.say("update error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end
ngx.say("update rows : ", res.affected_rows, "<br/>")


local select_sql = "select id, ch from test"
res, err, errno, sqlstate = db:query(select_sql)
if not res then
    ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end
for i, row in ipairs(res) do
    for name, value in pairs(row) do
        ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
    end
end
ngx.say("<br/>")


local ch_param = ngx.req.get_uri_args()["ch"] or ''
local query_sql = "select id, ch from test where ch = " .. ngx.quote_sql_str(ch_param)
res, err, errno, sqlstate = db:query(query_sql)
if not res then
    ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end
for i, row in ipairs(res) do
    for name, value in pairs(row) do
        ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
    end
end


local delete_sql = "delete from test"
res, err, errno, sqlstate = db:query(delete_sql)
if not res then
    ngx.say("delete error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
    return close_db(db)
end
ngx.say("delete rows : ", res.affected_rows, "<br/>")

close_db(db)

2. RedisExtOps.lua

---
--- Desc: 对OpenResty中使用Lua对Redis操作的封装库,支持订阅、管道等功能
--- Note:本Lua脚本借鉴了网络,未经测试,仅供参考,也不提供任何技术支持
---
local redis_c = require "resty.redis"

local ok, new_tab = pcall(require, "table.new")
if not ok or type(new_tab) ~= "function" then
    new_tab = function (narr, nrec) return {
    
    } end
end

local _M = new_tab(0, 155)
_M._VERSION = '0.01'

local commands = {
    
    
    "append",            "auth",              "bgrewriteaof",
    "bgsave",            "bitcount",          "bitop",
    "blpop",             "brpop",
    "brpoplpush",        "client",            "config",
    "dbsize",
    "debug",             "decr",              "decrby",
    "del",               "discard",           "dump",
    "echo",
    "eval",              "exec",              "exists",
    "expire",            "expireat",          "flushall",
    "flushdb",           "get",               "getbit",
    "getrange",          "getset",            "hdel",
    "hexists",           "hget",              "hgetall",
    "hincrby",           "hincrbyfloat",      "hkeys",
    "hlen",
    "hmget",              "hmset",      "hscan",
    "hset",
    "hsetnx",            "hvals",             "incr",
    "incrby",            "incrbyfloat",       "info",
    "keys",
    "lastsave",          "lindex",            "linsert",
    "llen",              "lpop",              "lpush",
    "lpushx",            "lrange",            "lrem",
    "lset",              "ltrim",             "mget",
    "migrate",
    "monitor",           "move",              "mset",
    "msetnx",            "multi",             "object",
    "persist",           "pexpire",           "pexpireat",
    "ping",              "psetex",            "psubscribe",
    "pttl",
    "publish",      --[[ "punsubscribe", ]]   "pubsub",
    "quit",
    "randomkey",         "rename",            "renamenx",
    "restore",
    "rpop",              "rpoplpush",         "rpush",
    "rpushx",            "sadd",              "save",
    "scan",              "scard",             "script",
    "sdiff",             "sdiffstore",
    "select",            "set",               "setbit",
    "setex",             "setnx",             "setrange",
    "shutdown",          "sinter",            "sinterstore",
    "sismember",         "slaveof",           "slowlog",
    "smembers",          "smove",             "sort",
    "spop",              "srandmember",       "srem",
    "sscan",
    "strlen",       --[[ "subscribe",  ]]     "sunion",
    "sunionstore",       "sync",              "time",
    "ttl",
    "type",         --[[ "unsubscribe", ]]    "unwatch",
    "watch",             "zadd",              "zcard",
    "zcount",            "zincrby",           "zinterstore",
    "zrange",            "zrangebyscore",     "zrank",
    "zrem",              "zremrangebyrank",   "zremrangebyscore",
    "zrevrange",         "zrevrangebyscore",  "zrevrank",
    "zscan",
    "zscore",            "zunionstore",       "evalsha"
}

local mt = {
    
     __index = _M }

local function is_redis_null( res )
    if type(res) == "table" then
        for k,v in pairs(res) do
            if v ~= ngx.null then
                return false
            end
        end
        return true
    elseif res == ngx.null then
        return true
    elseif res == nil then
        return true
    end

    return false
end

function _M.close_redis(self, redis)
    if not redis then
        return
    end
    --释放连接(连接池实现)
    local pool_max_idle_time = self.pool_max_idle_time --最大空闲时间 毫秒
    local pool_size = self.pool_size --连接池大小

    local ok, err = redis:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.say("set keepalive error : ", err)
    end
end

-- change connect address as you need
function _M.connect_mod( self, redis )
    redis:set_timeout(self.timeout)

    local ok, err = redis:connect(self.ip, self.port)
    if not ok then
        ngx.say("connect to redis error : ", err)
        return self:close_redis(redis)
    end

    if self.password then ----密码认证
    local count, err = redis:get_reused_times()
        if 0 == count then ----新建连接,需要认证密码
        ok, err = redis:auth(self.password)
            if not ok then
                ngx.say("failed to auth: ", err)
                return
            end
        elseif err then  ----从连接池中获取连接,无需再次认证密码
        ngx.say("failed to get reused times: ", err)
            return
        end
    end

    return ok,err;
end

function _M.init_pipeline( self )
    self._reqs = {
    
    }
end

function _M.commit_pipeline( self )
    local reqs = self._reqs

    if nil == reqs or 0 == #reqs then
        return {
    
    }, "no pipeline"
    else
        self._reqs = nil
    end

    local redis, err = redis_c:new()
    if not redis then
        return nil, err
    end

    local ok, err = self:connect_mod(redis)
    if not ok then
        return {
    
    }, err
    end

    redis:init_pipeline()
    for _, vals in ipairs(reqs) do
        local fun = redis[vals[1]]
        table.remove(vals , 1)

        fun(redis, unpack(vals))
    end

    local results, err = redis:commit_pipeline()
    if not results or err then
        return {
    
    }, err
    end

    if is_redis_null(results) then
        results = {
    
    }
        ngx.log(ngx.WARN, "is null")
    end
    -- table.remove (results , 1)

    --self.set_keepalive_mod(redis)
    self:close_redis(redis)

    for i,value in ipairs(results) do
        if is_redis_null(value) then
            results[i] = nil
        end
    end

    return results, err
end


local function do_command(self, cmd, ... )
    if self._reqs then
        table.insert(self._reqs, {
    
    cmd, ...})
        return
    end

    local redis, err = redis_c:new()
    if not redis then
        return nil, err
    end

    local ok, err = self:connect_mod(redis)
    if not ok or err then
        return nil, err
    end

    redis:select(self.db_index)

    local fun = redis[cmd]
    local result, err = fun(redis, ...)
    if not result or err then
        -- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err)
        return nil, err
    end

    if is_redis_null(result) then
        result = nil
    end

    --self.set_keepalive_mod(redis)
    self:close_redis(redis)

    return result, err
end

for i = 1, #commands do
    local cmd = commands[i]
    _M[cmd] =
    function (self, ...)
        return do_command(self, cmd, ...)
    end
end

function _M.new(self, opts)
    opts = opts or {
    
    }
    local timeout = (opts.timeout and opts.timeout * 1000) or 1000
    local db_index= opts.db_index or 0
    local ip = opts.ip or '127.0.0.1'
    local port = opts.port or 6379
    local password = opts.password
    local pool_max_idle_time = opts.pool_max_idle_time or 60000
    local pool_size = opts.pool_size or 100

    return setmetatable({
    
    
        timeout = timeout,
        db_index = db_index,
        ip = ip,
        port = port,
        password = password,
        pool_max_idle_time = pool_max_idle_time,
        pool_size = pool_size,
        _reqs = nil }, mt)
end

function _M.subscribe( self, channel )
    local redis, err = redis_c:new()
    if not redis then
        return nil, err
    end

    local ok, err = self:connect_mod(redis)
    if not ok or err then
        return nil, err
    end

    local res, err = redis:subscribe(channel)
    if not res then
        return nil, err
    end

    local function do_read_func ( do_read )
        if do_read == nil or do_read == true then
            res, err = redis:read_reply()
            if not res then
                return nil, err
            end
            return res
        end

        redis:unsubscribe(channel)
        self.set_keepalive_mod(redis)
        return
    end

    return do_read_func
end

return _M

---------------------------------------
-- 调用案例

local redis = require "RedisExtOps"
local opts = {
    
    
    ip = "10.11.0.215",
    port = "6379",
    password = "redis123",
    db_index = 1
}
local red = redis:new(opts)
local ok, err = red:set("dog", "an animal")
if not ok then
    ngx.say("failed to set dog: ", err)
    return
end
ngx.say("set result: ", ok)

---------------------------------------
-- 管道
red:init_pipeline()
red:set("cat", "Marry")
red:set("horse", "Bob")
red:get("cat")
red:get("horse")
local results, err = red:commit_pipeline()
if not results then
    ngx.say("failed to commit the pipelined requests: ", err)
    return
end
for i, res in ipairs(results) do
    ngx.say(res,"<br/>");
end

3. redisOps.lua

-- 导入redis的Lua模块
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
    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, "放入redis连接池失败: ", err)
    end
end

-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, password, key)
    -- 获取一个连接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end

    -- 密码认证
    if password ~= '' then
        -- 请注意这里 auth 的调用过程
        local count
        count, err = red:get_reused_times()
        if 0 == count then
            ok, err = red:auth(password)
            if not ok then
                ngx.say("连接redis密码认证失败 : ", err)
                return nil
            end
        elseif err then
            ngx.log("failed to get reused times: ", err)
            return nil
        end
    end

    -- 查询redis
    local resp, err = red:get(key)
    -- 查询失败处理
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end
    --得到的数据为空处理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end
    close_redis(red)
    return resp
end

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
    
    
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {
    
    
    read_http = read_http,
    read_redis = read_redis
}
return _M

4. test

http://openresty.localhost.com:8000/cache/stock?productId=3
在这里插入图片描述

辅助工具类说明

自动生成静态页

  • 在本地生成静态化的页面 — Freemarker 模板
  • 将静态化页面上传至服务器 — FTP

ISecKillStaticHtmlService

/*秒杀静态网页相关服务*/
public interface ISecKillStaticHtmlService {
    
    

    /*在本地生成静态化的页面*/
    List<String> makeStaticHtml(long secKillId) throws TemplateException, IOException;

    /*将静态化页面上传至服务器*/
    int deployHtml(long secKillId) throws TemplateException, IOException, Exception;

}

SecKillStaticHtmlServiceImpl

@Slf4j
@Service
public class SecKillStaticHtmlServiceImpl implements ISecKillStaticHtmlService {
    
    

    /**本地存放模板文件目录*/
    @Value("${seckill.templateDir}")
    private String templateDir;

    /**本地存放模板文件名*/
    @Value("${seckill.templateName:seckill.ftl}")
    private String templateName;

    /**本地存放生成的html文件目录*/
    @Value("${seckill.htmlDir}")
    private String htmlDir;

    /**sftp服务器ip地址列表*/
    @Value("#{'${seckill.serverList}'.split(',')}")
    private List<String> nginxServerList;

    /**端口*/
    @Value("${seckill.sftp.port}")
    private int port;

    /**用户名*/
    @Value("${seckill.sftp.userName}")
    private String userName;

    /**密码*/
    @Value("${seckill.sftp.password}")
    private String password;

    /**Nginx存放文件的根目录*/
    @Value("${seckill.sftp.rootPath}")
    private String rootPath;

    @Autowired
    private HomePromotionService homePromotionService;

    @Autowired
    private SftpUploadService sftpUploadService;

    @PostConstruct
    public void init(){
    
    
        templateDir = System.getProperty("user.home") + templateDir;
        htmlDir = System.getProperty("user.home") + htmlDir;
    }

    /*具体产品页面的静态化*/
    private String toStatic(FlashPromotionProduct flashPromotionProduct) throws IOException, TemplateException {
    
    
        String outPath = "";
        // 第一步:创建一个Configuration对象,直接new一个对象。构造方法的参数就是freemarker对于的版本号。
        Configuration configuration = new Configuration(Configuration.getVersion());
        // 第二步:设置模板文件所在的路径。
        configuration.setDirectoryForTemplateLoading(new File(templateDir));
        // 第三步:设置模板文件使用的字符集。一般就是utf-8.
        configuration.setDefaultEncoding("utf-8");
        // 第四步:加载一个模板,创建一个模板对象。
        Template template = configuration.getTemplate(templateName);
        // 第五步:创建一个模板使用的数据集,可以是pojo也可以是map。一般是Map。
        Map dataModel = new HashMap();
        // 向数据集中添加数据
        dataModel.put("fpp", flashPromotionProduct);

        String images = flashPromotionProduct.getPic();
        if (StringUtils.isNotEmpty(images)) {
    
    
            String[] split = images.split(",");
            List<String> imageList = Arrays.asList(split);
            dataModel.put("imageList", imageList);
        }
        // 第六步:创建一个Writer对象,一般创建一FileWriter对象,指定生成的文件名。
        // 文件名命名规则  seckill_+秒杀活动id + "_" + 秒杀产品ID,如 seckill_1_3.html
        String fileName = "seckill_" + flashPromotionProduct.getFlashPromotionId() + "_" + flashPromotionProduct.getId() + ".html";
        outPath = htmlDir + "/" + fileName;
        Writer out = new FileWriter(new File(outPath));
        // 第七步:调用模板对象的process方法输出文件。
        template.process(dataModel, out);
        // 第八步:关闭流。
        out.close();
        log.info("已在本地生成秒杀产品静态页:{}",outPath);
        return fileName;
    }

    /*根据秒杀活动,静态化该秒杀活动的所有页面*/
    @Override
    public List<String> makeStaticHtml(long secKillId) throws TemplateException, IOException {
    
    
        log.info("本地模板目录:{},本地html目录:{}",templateDir,htmlDir);
        //查询秒杀商品信息
        List<FlashPromotionProduct> flashPromotionProducts =
                homePromotionService.secKillContent(secKillId,ConstantPromotion.SECKILL_OPEN);
        List<String> result = new ArrayList<>();
        if(CollectionUtils.isEmpty(flashPromotionProducts)){
    
    
            log.warn("没有秒杀活动{[]}对应的产品信息,请检查DB中的秒杀数据",secKillId);
        }else{
    
    
            for(FlashPromotionProduct flashPromotionProduct : flashPromotionProducts){
    
    
                result.add(toStatic(flashPromotionProduct));
            }
        }
        return result;
    }

    @Override
    public int deployHtml(long secKillId) throws Exception {
    
    
        List<String> result = makeStaticHtml(secKillId);
        if(!CollectionUtils.isEmpty(result)){
    
    
            for(String host : nginxServerList){
    
    
                ChannelSftp channel = sftpUploadService.getChannel(host, userName, port, password);
                String path = rootPath + "/";
                sftpUploadService.createDir(path,channel);
                for(String fileName : result){
    
    
                    sftpUploadService.putFile(channel,new FileInputStream(htmlDir + "/" + fileName),path,fileName);
                }
                channel.quit();
                channel.exit();
                log.info("服务器:{},静态网页上传完成",host);
            }
            return ConstantPromotion.STATIC_HTML_SUCCESS;
        }else{
    
    
            return ConstantPromotion.STATIC_HTML_FAILURE;
        }
    }
}

文件

在这里插入图片描述

FTP 工具

依赖

<!-- 文件上传组件 -->
<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.3.3</version>
</dependency>
<dependency>
	<groupId>com.jcraft</groupId>
	<artifactId>jsch</artifactId>
	<version>0.1.54</version>
</dependency>
<dependency>
	<groupId>joda-time</groupId>
	<artifactId>joda-time</artifactId>
	<version>2.10.3</version>
</dependency>

使用封装

@Slf4j
@Service
public class SftpUploadService {
    
    

    /** 获取连接 */
    public ChannelSftp getChannel(String host,String userName,int port,String password) throws Exception{
    
    
        JSch jsch = new JSch();
        //->ssh root@host:port
        Session sshSession = jsch.getSession(userName,host,port);
        //密码
        sshSession.setPassword(password);
        Properties sshConfig = new Properties();
        sshConfig.put("StrictHostKeyChecking", "no");
        sshSession.setConfig(sshConfig);
        sshSession.connect();
        Channel channel = sshSession.openChannel("sftp");
        channel.connect();
        log.info("已连接服务器:{},准备上传....",host);
        return (ChannelSftp) channel;
    }

    /**
     * sftp上传文件
     * @param sftp
     * @param inputStream
     * @param fileName 服务器上存放的文件名
     */
    public void putFile(ChannelSftp sftp,InputStream inputStream, String path, String fileName){
    
    
        try {
    
    
            //上传文件
            log.info("准备上传{}.....",path + fileName);
            sftp.put(inputStream, path + fileName);
            log.info("上传{}成功!",path + fileName);

        } catch (Exception e) {
    
    
            log.error("上传{}失败:",path + fileName,e);
        }
    }

    /**
     * 创建目录
     */
    public static void createDir(String path,ChannelSftp sftp) throws SftpException {
    
    
        String[] folders = path.split("/");
        sftp.cd("/");
        for ( String folder : folders ) {
    
    
            if ( folder.length() > 0 ) {
    
    
                try {
    
    
                    sftp.cd( folder );
                }catch ( SftpException e ) {
    
    
                    sftp.mkdir( folder );
                    sftp.cd( folder );
                }
            }
        }
    }

}

demo

ChannelSftp channel = sftpUploadService.getChannel(host, userName, port, password);
String path = rootPath + "/";
sftpUploadService.createDir(path,channel);
sftpUploadService.putFile(channel,new FileInputStream(htmlDir + "/" + fileName),path,'demo.html');
channel.quit();
channel.exit();

猜你喜欢

转载自blog.csdn.net/menxu_work/article/details/128400402
今日推荐