工具(二):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请求的处理过程由很多模块参与处理,每个模块只专注于一个独立而简单的功能处理,可以使性能更好、更稳定,同时拥有更好的扩展性。
- ngx_http_post_read_phase:接收到完整的http头部后处理的阶段,它位于uri重写之前。
- ngx_http_server_rewrite_phase:uri与location匹配前,修改uri的阶段,用于重定向。
- ngx_http_find_config_phase:根据uri寻找匹配的location块配置项阶段,该阶段使用重写之后的uri来查找对应的location,值得注意的是该阶段可能会被执行多次,因为也可能有location级别的重写指令。
- ngx_http_rewrite_phase:上一阶段找到location块后再修改uri,location级别的uri重写阶段,该阶段执行location基本的重写指令,也可能会被执行多次。
- ngx_http_post_rewrite_phase:防止重写url后导致的死循环,location级别重写的后一阶段,用来检查上阶段是否有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 的 content 阶段是所有请求处理阶段中最为重要的一个,因为运行在这个阶段的配置指令一般都肩负着生成“内容”(content)并输出 HTTP 响应的使命。 - 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();