Tool (2): Nginx extends OpenResty

  • Introduction to OpenResty
  • Principle of OpenResty
  • nginx module
  • Nginx's lua insertion point
  • the case
    • configuration template
    • nginx.conf
    • Find static files through Lua — product page
    • Get redis via Lua returns only --inventory
    • test
  • Other Demos
    • MysqlOps.lua
    • RedisExtOps.lua
    • redisOps.lua
  • Auxiliary tool class description
    • Automatically generate static pages
    • FTP tool

Nginx_ has 5 major advantages, namely, modularization, event-driven, asynchronous, non-blocking, multi-process single-thread

Nginx is written in C, how to add our own business logic in Nginx — OpenResty

Introduction to OpenResty

OpenResty Chinese official website: https://openresty.org/cn/

Install

  • 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 is a high-performance web platform based on Nginx and Lua, which integrates a large number of excellent Lua libraries, third-party modules and most of its dependencies. It is used to conveniently build dynamic web applications, web services and dynamic gateways that can handle ultra-high concurrency and high scalability

OpenResty effectively turns Nginx into a powerful general-purpose web application platform by bringing together a variety of well-designed Nginx modules (mainly developed by the OpenResty team). In this way, web developers and system engineers can use Lua scripting language to mobilize various C and Lua modules supported by Nginx, and quickly construct a high-performance web application system capable of handling 10K or even more than 1000K single-machine concurrent connections

Why use Lua language for Nginx development?

  • Lua's threading model is a single-threaded multi-coroutine mode, while Nginx happens to be a single-process single-threaded, natural perfect partner
  • Lua is a small scripting language with very simple syntax
  • Redis also uses Lua as a scripting language

Principle of OpenResty

insert image description here

  • The Nginx
    Master process is used to receive signals from the outside world, send signals to each Worker process, and monitor the working status of the Worker process. When the Worker process exits (under abnormal circumstances), the Master process will automatically restart a new Worker process. The Worker process is the real processor of external requests.

    • Worker processes are peer-to-peer, they compete equally for requests from clients, and each process is independent of each other. A request can only be processed in one Worker process, and a Worker process cannot process requests from other processes
    • The number of Worker processes can be set, and generally we will set it to be consistent with the number of CPU cores of the machine. At the same time, in order to make better use of multi-core features, Nginx has a CPU binding option. We can bind a certain process to a certain core, so that the cache will not be invalidated (CPU affinity) due to process switching. All processes are single-threaded (that is, there is only one main thread), and communication between processes is mainly realized through a shared memory mechanism.
  • OpenResty
    OpenResty essentially embeds LuaJITthe virtual machine of Nginx into the management process and worker process of Nginx. All coroutines in the same process will share this virtual machine and execute Lua code in the virtual machine. In terms of performance, OpenResty is close to or exceeds the C module of Nginx, and the development efficiency is higher.

nginx module

Nginx divides the processing of HTTP requests into multiple stages. In this way, many modules can participate in the processing of an HTTP request, and each module only focuses on an independent and simple function processing, which can make the performance better, more stable, and have better scalability.

  1. ngx_http_post_read_phase: The phase of processing after receiving the complete http header, which is before uri rewriting.
  2. ngx_http_server_rewrite_phase: Before the uri matches the location, the phase of modifying the uri is used for redirection.
  3. ngx_http_find_config_phase: Find the matching location block configuration item phase according to uri. This phase uses the rewritten uri to find the corresponding location. It is worth noting that this phase may be executed multiple times, because there may also be location-level rewriting instructions .
  4. ngx_http_rewrite_phase: In the previous stage, the uri is modified after finding the location block. The uri rewriting stage at the location level executes the basic rewriting instructions of the location, and may be executed multiple times.
  5. ngx_http_post_rewrite_phase: To prevent the infinite loop caused by rewriting url, the last stage of location level rewriting is used to check whether there is uri rewriting in the previous stage, and jump to the appropriate stage according to the result.
  6. ngx_http_preaccess_phase: Preparation before the next phase, the previous phase of access control, this phase is generally used for access control before the access control phase, such as limiting access frequency, number of links, etc.
  7. ngx_http_access_phase: Let the http module determine whether this request is allowed to enter the nginx server, access control phase, such as access control based on ip black and white lists, access control based on user name and password, etc.
    The access_by_lua command of standard module ngx_access, third-party module ngx_auth_request and third-party module ngx_lua runs at this stage.
  8. ngx_http_post_access_phase: The latter phase of access control, which will be processed according to the execution result of the permission control phase.
  9. ngx_http_try_files_phase: Set for accessing static file resources, the processing phase of the try_files directive, if the try_files directive is not configured, this phase is skipped.
  10. ngx_http_content_phase: The stage of processing http request content. Most http modules intervene in this stage, the content generation stage, which generates a response and sends it to the client.
    The content phase of Nginx is the most important of all request processing phases, because the configuration directives running in this phase are generally tasked with generating "content" and outputting HTTP responses.
  11. ngx_http_log_phase: log phase processing, such as recording traffic/statistical average response time. The logging phase after log_by_lua has processed the request, which records the access log.

Among the above 11 phases, there are 4 phases where http cannot intervene:
3) ngx_http_find_config_phase
5) ngx_http_post_rewrite_phase
8) ngx_http_post_access_phase
9) ngx_http_try_files_phase.
On the basis of the HTTP processing stage, OpenResty registers its own handlers in the Rewrite/Access stage, Content stage, and Log stage, plus the two stages of the master in the initial stage of the system. There are a total of 11 stages that provide processing intervention capabilities for Lua scripts.

Nginx's lua insertion point

insert image description here

  • init_by_lua*: Run when the Master process loads the Nginx configuration file, generally used to register global variables or preload Lua modules.
  • init_worker_by_lua*: Executed when each worker process starts, usually used to regularly pull configuration/data or perform health checks on backend services.
  • set_by_lua*: variable initialization.
  • rewrite_by_lua*: It can implement complex forwarding and redirection logic.
  • access_by_lua*: Centralized processing of IP access, interface permissions, etc.
  • content_by_lua*: Content processor, receive request processing and output response.
  • header_filter_by_lua*: Response header or cookie processing.
  • body_filter_by_lua*: Filter the response data, such as truncation or replacement.
  • log_by_lua*: Logging is done locally asynchronously after the session is complete

the case

insert image description here

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;
        }

    }
}

configuration template

Templates are placed in the directory:/usr/local/openresty/lualib/resty
insert image description here

URL processing

insert image description here

Find static files through Lua — product page

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

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

dual/product.dua

-- 导入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)

insert image description here

Get redis via Lua returns only --inventory

  #秒杀产品当前库存
  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))

Other Demos

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
insert image description here

Auxiliary tool class description

Automatically generate static pages

  • Generate static pages locally — Freemarker templates
  • Upload static pages to server - 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;
        }
    }
}

document

insert image description here

FTP tool

rely

<!-- 文件上传组件 -->
<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>

use package

@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();

Guess you like

Origin blog.csdn.net/menxu_work/article/details/128400402