- OpenResty の概要
- OpenRestyの原則
- nginxモジュール
- Nginxのlua挿入ポイント
- ケース
- 構成テンプレート
- nginx.conf
- Lua を通じて静的ファイルを検索 — 製品ページ
- Lua 経由で Redis を取得すると --inventory のみが返されます
- テスト
- その他のデモ
- 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 モジュールを活用し、10,000、さらには 1,000,000 を超える単一マシンの同時接続を処理できる高性能 Web アプリケーション システムを迅速に構築できます。
Nginx 開発に Lua 言語を使用する理由は何ですか?
- Lua のスレッド モデルはシングル スレッドのマルチ コルーチン モードですが、Nginx はシングル プロセス、シングル スレッドの自然な完璧なパートナーです。
- Lua は非常に単純な構文を持つ小さなスクリプト言語です
- Redis はスクリプト言語としても Lua を使用します
OpenRestyの原則
-
Nginx
Master プロセスは、外部からシグナルを受信し、各 Worker プロセスにシグナルを送信し、Worker プロセスの動作状態を監視するために使用されます。ワーカー プロセスが (異常な状況下で) 終了すると、マスター プロセスは自動的に新しいワーカー プロセスを再起動します。ワーカー プロセスは、外部リクエストの実際のプロセッサです。- ワーカー プロセスはピアツーピアであり、クライアントからのリクエストを平等に競合し、各プロセスは互いに独立しています。リクエストは 1 つのワーカー プロセスでのみ処理でき、ワーカー プロセスは他のプロセスからのリクエストを処理できません。
- Worker プロセスの数は設定できますが、通常はマシンの CPU コアの数と一致するように設定します。同時に、マルチコア機能をより有効に活用するために、Nginx には CPU バインド オプションがあり、特定のプロセスを特定のコアにバインドして、プロセスによってキャッシュが無効にならないようにすることができます (CPU アフィニティ)切り替え。すべてのプロセスはシングルスレッド (つまり、メイン スレッドが 1 つだけ存在します) であり、プロセス間の通信は主に共有メモリ メカニズムを通じて実現されます。
-
OpenResty
OpenResty は基本的に Nginx の管理プロセスとワーカープロセスに Nginx の仮想LuaJIT
マシン、同じプロセス内のすべてのコルーチンがこの仮想マシンを共有し、仮想マシン内で Lua コードを実行します。OpenRestyは性能的にはNginxのCモジュールに近いかそれを上回っており、開発効率が高いです。
nginxモジュール
Nginx は、HTTP リクエストの処理を複数の段階に分割します。このようにして、多くのモジュールが HTTP リクエストの処理に参加でき、各モジュールは独立した単純な関数処理のみに焦点を当てるため、パフォーマンスが向上し、安定性が高まり、スケーラビリティが向上します。
- ngx_http_post_read_phase: 完全な http ヘッダーを受信した後の処理フェーズ。URI を書き換える前です。
- ngx_http_server_rewrite_phase: URI が場所と一致する前に、URI を変更するフェーズがリダイレクトに使用されます。
- ngx_http_find_config_phase: uri に従って、一致するロケーション ブロック構成アイテムのフェーズを見つけます。このフェーズでは、書き換えられた uri を使用して、対応するロケーションを見つけます。ロケーション レベルの書き換え命令も存在する可能性があるため、このフェーズは複数回実行される可能性があることに注意してください。
- ngx_http_rewrite_phase: 前のステージでは、ロケーション ブロックを見つけた後に URI が変更されます。ロケーション レベルでの URI 書き換えステージでは、ロケーションの基本的な書き換え命令が実行され、複数回実行される場合があります。
- ngx_http_post_rewrite_phase: URL の書き換えによる無限ループを防ぐため、ロケーション レベルの書き換えの最後のステージで、前のステージで URI の書き換えがあったかどうかを確認し、結果に応じて適切なステージにジャンプします。
- ngx_http_preaccess_phase: 次のフェーズの前の準備、アクセス制御の前のフェーズ。このフェーズは通常、アクセス頻度やリンク数の制限など、アクセス制御フェーズの前のアクセス制御に使用されます。
- ngx_http_access_phase: http モジュールに、このリクエストが nginx サーバーに入るのを許可するかどうか、IP ブラックリストとホワイトリストに基づくアクセス制御、ユーザー名とパスワードに基づくアクセス制御などのアクセス制御フェーズを決定させます。
この段階では、標準モジュール ngx_access、サードパーティ モジュール ngx_auth_request、サードパーティ モジュール ngx_lua の access_by_lua コマンドが実行されます。 - ngx_http_post_access_phase: アクセス制御の後半フェーズ。権限制御フェーズの実行結果に応じて処理されます。
- ngx_http_try_files_phase: 静的ファイル リソースにアクセスするために設定されます。try_files ディレクティブの処理フェーズです。try_files ディレクティブが設定されていない場合、このフェーズはスキップされます。
- ngx_http_content_phase: http リクエスト コンテンツを処理するステージほとんどの http モジュールはこのステージ、つまり応答を生成してクライアントに送信するコンテンツ生成ステージに介入します。
Nginx のコンテンツ フェーズは、すべてのリクエスト処理フェーズの中で最も重要です。このフェーズで実行される構成ディレクティブは一般に、「コンテンツ」の生成と HTTP 応答の出力を担当するためです。 - ngx_http_log_phase: トラフィック/統計的平均応答時間の記録などのログフェーズ処理。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ステージに独自のハンドラーを登録し、さらにシステム初期のマスターの2ステージを加えた合計11ステージあります。 Lua スクリプトの処理介入機能を提供します。
Nginxのlua挿入ポイント
- init_by_lua*: マスター プロセスが Nginx 構成ファイルをロードするときに実行されます。通常、グローバル変数の登録または Lua モジュールのプリロードに使用されます。
- init_worker_by_lua*: 各ワーカー プロセスの開始時に実行され、通常は定期的に構成/データを取得したり、バックエンド サービスのヘルス チェックを実行したりするために使用されます。
- 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;
}
デュアル/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)
Lua 経由で Redis を取得すると --inventory のみが返されます
#秒杀产品当前库存
location /cache/stock {
# 默认的响应类型
default_type application/json;
# 响应结果由lua/stock.lua文件来处理
content_by_lua_file lua/stock.lua;
}
lua/ストック.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))
その他のデモ
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. テスト
http://openresty.localhost.com:8000/cache/stock?productId=3
補助ツールクラスの説明
静的ページを自動的に生成する
- 静的ページをローカルで生成する — Freemark テンプレート
- 静的ページをサーバーにアップロード - 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 );
}
}
}
}
}
デモ
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();