實現思路
這里列舉了3種實現的思路,至于實現方案,可能還有更多,但是我們想想,在nginx中編寫邏輯塊貌似不是很多人擅長的;在代碼層面做不是不可以,而是這樣一來,在涉及到高并發的業務高峰期,這必然會對后端服務造成較大的壓力,那么還有沒有其他更好的處理辦法呢?
這就是要說的lua,即nginx作為網關仍然作為代理服務器,由于nginx可以集成lua,于是使用lua進行配合,來完成上面的業務實現的設計;
ngx_lua模塊概念
-
ngx_lua模塊由淘寶技術團隊開發,通過將lua解釋器集成進Nginx;
-
可采用lua腳本實現業務邏輯,由于lua的緊湊、快速以及內建協程,所以在保證高并發服務能力的同時極大地降低了業務邏輯實現成本;
-
OpenRestry
-
openresty是一個基于Nginx與 Lua 的高性能 Web 平臺,其內部集成了大量精良的 Lua庫、第三方模塊以及大多數的依賴項;用于方便地搭建能夠處理超高并發、擴展性極高的動態 Web 應用、Web 服務和動態網關;
-
OpenResty內部已經集成了Nginx和Lua,所以使用起來會更加方便;
簡單來說,直接安裝并使用OpenRestry,就可以達到同時使用Nginx與Lua的效果,同時基于OpenRestry,還可以在內部操作其他中間件,比如mysql,redis,kafka等,這樣就使得業務架構在設計上具備了更大的靈活性;
OpenRestry安裝步驟
1、下載OpenRestry
wget?https://openresty.org/download/openresty-1.15.8.2.tar.gz
2、解壓縮文件
tar?-zxf?openresty-1.15.8.2.tar.gz
3、進入OpenResty目錄執行配置
這一步有點類似于nginx的源碼安裝,進行相關的環境變量的配置,這里直接使用默認的就好;
./configure
4、 執行命令:make && make install
5、進入OpenResty的目錄配置nginx
進入nginx目錄,可以看到里面的目錄和nginx自身安裝完畢后的配置幾乎一樣
進入conf,找到nginx.conf配置文件,添加如下內容:
location?/lua?{? ????default_type?'text/html';? ????content_by_lua?'ngx.say("?<h2>hello,openRestry?lua</h2>")';? }
6、啟動nginx并測試
進入nginx的sbin目錄下啟動nginx
啟動完成后,瀏覽器訪問下服務器即可,可以看到nginx本身服務已啟動
訪問已經配置的Lua地址后,可以發現可以正常訪問,這表明OpenResty模塊已經成功安裝
ngx_lua常用指令
使用Lua編寫Nginx腳本的基本構建塊是指令,指令用于指定何時運行用戶Lua代碼以及如何使用結果,下面針對一些常用的指令做簡單的說明
1、init_by_lua*
該指令在每次Nginx重新加載配置時執行,用來完成一些耗時操作模塊加載,或初始化一些全局配置
2、init_worker_by_lua*
該指令用于啟動一些定時任務,如心跳檢查、定時拉取服務器配置等
3、set_by_lua*
該指令只要用來給變量賦值,這個指令一次只能返回一個值,并將結果 值給Nginx中指定變量
4、rewrite_by_lua*
用于執行內部URL重寫或者外部重定向,典型的如偽靜態化URL重 寫,本階段在rewrite處理階段的最后默認執行(和nginx自身的rewrite功能有類似的地方)
5、access_by_lua*
該指令用于訪問控制,例如,只允許內網IP訪問
6、content_by_lua*
該指令是使用最多的指令,大部分任務是在這個階段完成的,其他過程往往為這個階段準備數據,正式處理往往都在本階段執行
7、header_filter_by_lua*
用于設置應答消息的頭部信息
8、body_filter_by_lua*
該指令對響應數據進行過濾,如截斷、替換
9、log_by_lua*
該指令用于log請求處理階段,用Lua代碼處理日志,但并不替換原有 log處理
10、balancer_by_lua*
11、ssl_certificate_by_*
該指令作用在Nginx和下游服務開始一個SSL握手操作時將允許本配置項的Lua代碼
一個使用指令的需求
接下來針對上面提到的各種指令,來做一個簡單的需求
nginx接收到請求后,根據參數中gender傳入的值,如果gender傳入的是1 則在頁面上展示 “先生” , 如果gender傳入的是0,則在頁面上展示“女士”
代碼實現
注意:使用指令編寫的基本步驟是,在nginx.conf模塊中,自定義localtion塊中編寫lua的相關代碼即可
location?/getByGender?{ ??????????default_type?'text/html'; ??????????set_by_lua?$param?" ????????????????local?uri_args?=?ngx.req.get_uri_args() ????????????????local?gender?=?uri_args['gender'] ????????????????local?name?=?uri_args['name'] ????????????????if?gender?=='1'?then ???????????????????????return?name..':先生' ????????????????elseif?gender=='0'?then ???????????????????????return?name..':女士' ????????????????else ???????????????????????return?name ????????????????end ??????????"; ??????????charset?utf-8; ??????????return?200?$param; ?}
然后啟動nginx做一下測試
1)訪問服務,不攜帶任何參數
這時候無任何返回信息
2)訪問服務,攜帶name參數
3)訪問服務,攜帶name和gender參數
更多的指令可以參照此類方式編寫,但是前提需要掌握一點lua的基本語法
lua操作redis
Redis在系統中經常作為數據緩存、內存數據庫使用,在各類互聯網項目中扮演著非常重要的作用;
Lua-resty-redis庫是OpenResty提供的一個操作Redis的接口庫,可根據自己的業務情況做一些邏輯處理,適合做復雜的業務邏輯。所以下面將以Lua-resty-redis來進行說明。
lua-resty-redis環境準備
1、提前安裝好redis并啟動服務
2、測試下redis客戶端
lua-resty-redis提供了訪問Redis的詳細API,包括創建對接、連 接、操作、數據處理等。這些API基本上與Redis的操作是對應起來的
lua-resty-redis常用API
1、lua中導入redis依賴
redis?=?require?"resty.redis"
2、new,創建一個Redis對象
redis,err?=?redis:new()
3、創建redis連接
-
ok:連接成功返回 1,連接失敗返回nil;
-
err:返回對應的錯誤信息;
ok,err=redis:connect(host,port[,options_table])
4、設置請求操作Redis的超時時間
redis:set_timeout(time)
5、close,關閉連接
-
關閉當前連接,成功返回1;
-
失敗返回nil和錯誤信息;
ok,err = redis:close()
補充說明:
在lua-resty-redis中,所有的Redis命令都有自己的方法;方法名字和命令名字相同,只是全部為小寫;
具體實現效果展示
在nginx.conf模塊下,添加如下的location內容
location?/redis?{ ????default_type?"text/html"; ????content_by_lua_block?{ ????????local?redis?=?require?"resty.redis"?????--?引入?Redis ????????local?redisObj?=?redis:new()????????????--創建Redis對象 ????????redisObj:set_timeout(3000)??????????????--設置超時數據為3s ????????local?ok,err?=?redisObj:connect("IP",6379)????--設置redis連接信息 ????????if?not?ok?then??????????????????????????--判斷是否連接成功 ????????????ngx.say("failed?to?connection?redis",err) ????????????return ????????end ????????ok,err?=?redisObj:set("username","TOM")?????--存入?數據 ????????if?not?ok?then??????????????????????????????--判斷是否存入成功 ????????????ngx.say("failed?to?set?username",err) ????????????return ????????end ????????local?res,err?=?redisObj:get("username")????--從?redis中獲取數據 ????????ngx.say(res)?--將數據寫會消息體中 ????????redisObj:close() ????} }
重啟nginx,進行測試,直接在瀏覽器訪問一下如下地址,可以看到數據成功寫入到redis
ngx_lua操作mysql
MySQL是一個使用廣泛的關系型數據庫。在ngx_lua中,MySQL有兩種訪問模式,分別是
-
用ngx_lua模塊和lua-resty-mysql模塊: 這兩個模塊是安裝OpenResty時默認安裝的;
-
使用drizzle_nginx_module(HttpDrizzleModule)模塊:需要單獨
安裝,這個庫現不在OpenResty中
lua-resty-mysql
lua-resty-mysql是OpenResty開發的模塊,使用靈活、功能強大,適合復雜的業務場景,同時支持存儲過程訪問;
lua-resty-mysql實現數據庫查詢
1、準備好mysql服務
2、提前創建一張表
CREATE?TABLE?`users`?( ??`id`?int(10)?NOT?NULL?AUTO_INCREMENT, ??`username`?varchar(255)?DEFAULT?NULL, ??`birthday`?date?DEFAULT?NULL, ??`salary`?double(10,2)?DEFAULT?NULL, ??PRIMARY?KEY?(`id`) )?ENGINE=InnoDB?DEFAULT?CHARSET=utf8;
并提前準備幾條數據
INSERT?INTO?`mydb`.`users`?(`id`,?`username`,?`birthday`,?`salary`)?VALUES?('1',?'xiaowang',?'1991-03-15',?'9000.00'); INSERT?INTO?`mydb`.`users`?(`id`,?`username`,?`birthday`,?`salary`)?VALUES?('2',?'xiaoma',?'1992-07-15',?'8000.00');
lua-resty-mysql API說明
1、引入”resty.mysql”模塊
local?mysql?=?require?"resty.mysql"
2、創建MySQL連接對象
遇到錯誤時,db為nil,err為錯誤描 述信息
db,err?=?mysql:new()
3、創建連接對象
ok,err=db:connect(options)
options是一個參數的 Lua表結構,里面包含數據庫連接的相關信息
-
host:服務器主機名或IP地址
-
port:服務器監聽端口,默認為3306
-
user:登錄的用戶名
-
password:登錄密碼
-
database:使用的數據庫名
4、設置子請求的超時時間(ms)
包括connect方法
db:set_timeout(time)
5、關閉當前MySQL連接并返回狀態
如果成功,則返回1;如果出現任 何錯誤,則將返回nil和錯誤描述
db:close()
6、異步向遠程MySQL發送一個查詢
如果成功則返回成功發送的字節 數;如果錯誤,則返回nil和錯誤描述
bytes,err=db:send_query(sql)
7、從MySQL服務器返回結果中讀取一行數據
-
res返回一個描述OK包 或結果集包的Lua表
-
rows指定返回結果集的最大值,默認為4
-
如果是查詢,則返回一個容納多行的數組。每行是一個數據列的 key-value對
res,?err,?errcode,?sqlstate?=?db:read_result()?res,?err,?errcode,?sqlstate?=?db:read_result(rows)
返回結果類似下面這樣
{? {id=1,username="TOM",birthday="1988-11-?11",salary=10000.0},?{id=2,username="JERRY",birthday="1989-11-?11",salary=20000.0}? }
如果是增刪改,則返回類似如下數據
{? insert_id?=?0,? server_status=2,? warning_count=1,? affected_rows=2,? message=nil? }
返回值說明:
-
res:操作的結果集
-
err:錯誤信息
-
errcode:MySQL的錯誤碼,比如1064
-
sqlstate:返回由5個字符組成的標準SQL錯誤碼,比如 42000
具體操作案例
將下面的內容添加到server塊,然后重啟nginx
location?/mysql?{ ?????content_by_lua_block{ default_type?"text/html"; ????????local?mysql?=?require?"resty.mysql"? ????????local?db?=?mysql:new()? ????????local?ok,err?=?db:connect{? ????????????host="127.0.0.1",? ????????????port=3306, ????????????user="root",? ????????????password="123456",? ????????????database="mydb" ????????}? ????????????db:set_timeout(3000)? ????????????db:send_query("select?*?from?users?where?id?=1")? ????????????local?res,err,errcode,sqlstate?=?db:read_result()? ????????????ngx.say(res[1].id..","..res[1].username..","..res[1].?birthday..","..res[1].salary) ????????????db:close() ????}? }
可以看到,通過訪問mysql這個路徑,成功查詢到數據庫中ID為1的這條數據
使用cjson對查詢結果進行格式化
從上面的返回結果來看,這種形式的返回數據在解析的時候其實并不是很友好,于是可以使用lua-cjson處理查詢結果
使用步驟
步驟一:引入cjson
local cjson = require “cjson”
步驟二:調用cjson的encode方法進行類型轉換
cjson.encode(res)
下面對上面程序模塊做簡單的改造
location?/mysql-cjson?{ default_type?"text/html"; ?????content_by_lua_block{ ????? local?cjson?=?require?"cjson" ????????local?mysql?=?require?"resty.mysql"? ????????local?db?=?mysql:new()? ????????local?ok,err?=?db:connect{? ????????????host="127.0.0.1",? ????????????port=3306, ????????????user="root",? ????????????password="123456",? ????????????database="mydb" ????????}? ????????????db:set_timeout(3000)? ????????????db:send_query("select?*?from?users")? ????????????local?res,err,errcode,sqlstate?=?db:read_result()? ????????????ngx.say(cjson.encode(res)) ????????????for?i,v?in?ipairs(res)?do ????????????????ngx.say(v.id..","..v.username..","..v.birthday..","..?v.salary) ????????????end ????????????db:close() ????}? }
然后再次進行測試,這時候就以json的格式對數據進行了展現
增刪改操作
location?/mysql-cjson?{ default_type?"text/html"; ?????content_by_lua_block{ ????? local?cjson?=?require?"cjson" ????????local?mysql?=?require?"resty.mysql"? ????????local?db?=?mysql:new()? ????????local?ok,err?=?db:connect{? ????????????host="127.0.0.1",? ????????????port=3306, ????????????user="root",? ????????????password="123456",? ????????????database="mydb" ????????}? ????????????db:set_timeout(3000)? ????????????--?查詢操作 ????????????db:send_query("select?*?from?users?where?id=1")? --?插入數據 --local?res,err,errcode,sqlstate?=?db:query("insert?into?users(id,username,birthday,salary)?values(3,'lifei','1995-10-17',3000)") --?修改數據 --local?res,err,errcode,sqlstate?=?db:query("update?users?set?username='lisi'?where?id?=?1") --?刪除數據 --local?res,err,errcode,sqlstate?=?db:query("delete?from?users?where?id?=?2") ????????????db:close() ????}? }