- 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
-
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 embedsLuaJIT
the 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.
- ngx_http_post_read_phase: The phase of processing after receiving the complete http header, which is before uri rewriting.
- ngx_http_server_rewrite_phase: Before the uri matches the location, the phase of modifying the uri is used for redirection.
- 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 .
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
- 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. - 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
- 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
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
URL processing
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)
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
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
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();