對于開發來說需要有好的生態開發庫來輔助我們快速開發,而Lua中也有大多數我們需要的第三方開發庫如Redis、Memcached、Mysql、Http客戶端、JSON、模板引擎等。
一些常見的Lua庫可以在github上搜索,https://github.com/search?utf8=%E2%9C%93&q=lua+resty。
?
Redis客戶端
lua-resty-redis是為基于cosocket API的ngx_lua提供的Lua redis客戶端,通過它可以完成Redis的操作。默認安裝OpenResty時已經自帶了該模塊,使用文檔可參考https://github.com/openresty/lua-resty-redis。
?
在測試之前請啟動Redis實例:
nohup /usr/servers/redis-2.8.19/src/redis-server? /usr/servers/redis-2.8.19/redis_6660.conf &
?
1、基本操作
?
編輯test_redis_baisc.lua
Java代碼??
local?function?close_redis(red)??????if?not?red?then??????????return??????end??????local?ok,?err?=?red:close()??????if?not?ok?then??????????ngx.say("close?redis?error?:?",?err)??????end??end????local?redis?=?require("resty.redis")????--創建實例??local?red?=?redis:new()??--設置超時(毫秒)??red:set_timeout(1000)??--建立連接??local?ip?=?"127.0.0.1"??local?port?=?6660??local?ok,?err?=?red:connect(ip,?port)??if?not?ok?then??????ngx.say("connect?to?redis?error?:?",?err)??????return?close_redis(red)??end??--調用API進行處理??ok,?err?=?red:set("msg",?"hello?world")??if?not?ok?then??????ngx.say("set?msg?error?:?",?err)??????return?close_redis(red)??end????--調用API獲取數據??local?resp,?err?=?red:get("msg")??if?not?resp?then??????ngx.say("get?msg?error?:?",?err)??????return?close_redis(red)??end??--得到的數據為空處理??if?resp?==?ngx.null?then??????resp?=?''??--比如默認值??end??ngx.say("msg?:?",?resp)????close_redis(red)?? 基本邏輯很簡單,要注意此處判斷是否為nil,需要跟ngx.null比較。
?
2、example.conf配置文件
Java代碼??
?location?/lua_redis_basic?{??????default_type?'text/html';??????lua_code_cache?on;??????content_by_lua_file?/usr/example/lua/test_redis_basic.lua;??}?? ??
3、訪問如http://192.168.1.2/lua_redis_basic進行測試,正常情況得到如下信息
msg : hello world
?
2、連接池
建立TCP連接需要三次握手而釋放TCP連接需要四次握手,而這些往返時延僅需要一次,以后應該復用TCP連接,此時就可以考慮使用連接池,即連接池可以復用連接。
我們只需要將之前的close_redis函數改造為如下即可:?
Java代碼??
local?function?close_redis(red)??????if?not?red?then??????????return??????end??????--釋放連接(連接池實現)??????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.say("set?keepalive?error?:?",?err)??????end??end?? 即設置空閑連接超時時間防止連接一直占用不釋放;設置連接池大小來復用連接。
?
此處假設調用red:set_keepalive(),連接池大小通過nginx.conf中http部分的如下指令定義:
#默認連接池大小,默認30
lua_socket_pool_size 30;
#默認超時時間,默認60s
lua_socket_keepalive_timeout 60s;
?
注意:
1、連接池是每Worker進程的,而不是每Server的;
2、當連接超過最大連接池大小時,會按照LRU算法回收空閑連接為新連接使用;
3、連接池中的空閑連接出現異常時會自動被移除;
4、連接池是通過ip和port標識的,即相同的ip和port會使用同一個連接池(即使是不同類型的客戶端如Redis、Memcached);
5、連接池第一次set_keepalive時連接池大小就確定下了,不會再變更;
5、cosocket的連接池http://wiki.nginx.org/HttpLuaModule#tcpsock:setkeepalive。
?
3、pipeline
pipeline即管道,可以理解為把多個命令打包然后一起發送;MTU(Maxitum Transmission Unit 最大傳輸單元)為二層包大小,一般為1500字節;而MSS(Maximum Segment Size 最大報文分段大小)為四層包大小,其一般是1500-20(IP報頭)-20(TCP報頭)=1460字節;因此假設我們執行的多個Redis命令能在一個報文中傳輸的話,可以減少網絡往返來提高速度。因此可以根據實際情況來選擇走pipeline模式將多個命令打包到一個報文發送然后接受響應,而Redis協議也能很簡單的識別和解決粘包。
?
1、修改之前的代碼片段
Java代碼??
red:init_pipeline()??red:set("msg1",?"hello1")??red:set("msg2",?"hello2")??red:get("msg1")??red:get("msg2")??local?respTable,?err?=?red:commit_pipeline()????--得到的數據為空處理??if?respTable?==?ngx.null?then??????respTable?=?{}??--比如默認值??end????--結果是按照執行順序返回的一個table??for?i,?v?in?ipairs(respTable)?do?????ngx.say("msg?:?",?v,?"<br/>")??end?? 通過init_pipeline()初始化,然后通過commit_pipieline()打包提交init_pipeline()之后的Redis命令;返回結果是一個lua table,可以通過ipairs循環獲取結果;
?
2、配置相應location,測試得到的結果
msg : OK
msg : OK
msg : hello1
msg : hello2
?
?
3、Redis Lua腳本
利用Redis單線程特性,可以通過在Redis中執行Lua腳本實現一些原子操作。如之前的red:get("msg")可以通過如下兩種方式實現:
1、直接eval:
Java代碼??
local?resp,?err?=?red:eval("return?redis.call('get',?KEYS[1])",?1,?"msg");??? 2、script load然后evalsha??SHA1 校驗和,這樣可以節省腳本本身的服務器帶寬:
Java代碼??
local?sha1,?err?=?red:script("load",??"return?redis.call('get',?KEYS[1])");??if?not?sha1?then?????ngx.say("load?script?error?:?",?err)?????return?close_redis(red)??end??ngx.say("sha1?:?",?sha1,?"<br/>")??local?resp,?err?=?red:evalsha(sha1,?1,?"msg");?? 首先通過script load導入腳本并得到一個sha1校驗和(僅需第一次導入即可),然后通過evalsha執行sha1校驗和即可,這樣如果腳本很長通過這種方式可以減少帶寬的消耗。?
?
此處僅介紹了最簡單的redis lua腳本,更復雜的請參考官方文檔學習使用。
?
另外Redis集群分片算法該客戶端沒有提供需要自己實現,當然可以考慮直接使用類似于Twemproxy這種中間件實現。
Memcached客戶端使用方式和本文類似,本文就不介紹了。
?
Mysql客戶端
lua-resty-mysql是為基于cosocket API的ngx_lua提供的Lua Mysql客戶端,通過它可以完成Mysql的操作。默認安裝OpenResty時已經自帶了該模塊,使用文檔可參考https://github.com/openresty/lua-resty-mysql。
?
1、編輯test_mysql.lua
Java代碼??
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?=?"123456"??}????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????--刪除表??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?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?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/>")??--防止sql注入??local?ch_param?=?ngx.req.get_uri_args()["ch"]?or?''??--使用ngx.quote_sql_str防止sql注入??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)?? ?
對于新增/修改/刪除會返回如下格式的響應:
Java代碼??
{??????insert_id?=?0,??????server_status?=?2,??????warning_count?=?1,??????affected_rows?=?32,??????message?=?nil??}?? affected_rows表示操作影響的行數,insert_id是在使用自增序列時產生的id。
?
對于查詢會返回如下格式的響應:
Java代碼??
{??????{?id=?1,?ch=?"hello"},??????{?id=?2,?ch=?"hello2"}??}?? null將返回ngx.null。
?
2、example.conf配置文件
Java代碼??
location?/lua_mysql?{?????default_type?'text/html';?????lua_code_cache?on;?????content_by_lua_file?/usr/example/lua/test_mysql.lua;??}?? ?
3、訪問如http://192.168.1.2/lua_mysql?ch=hello進行測試,得到如下結果
Java代碼??
insert?rows?:?1?,?id?:?2??update?rows?:?1??select?row?1?:?ch?=?hello??select?row?1?:?id?=?1??select?row?2?:?ch?=?hello2??select?row?2?:?id?=?2??select?row?1?:?ch?=?hello??select?row?1?:?id?=?1??delete?rows?:?2?? 客戶端目前還沒有提供預編譯SQL支持(即占位符替換位置變量),這樣在入參時記得使用ngx.quote_sql_str進行字符串轉義,防止sql注入;連接池和之前Redis客戶端完全一樣就不介紹了。
?
對于Mysql客戶端的介紹基本夠用了,更多請參考https://github.com/openresty/lua-resty-mysql。
?
其他如MongoDB等數據庫的客戶端可以從github上查找使用。
?
Http客戶端
OpenResty默認沒有提供Http客戶端,需要使用第三方提供;當然我們可以通過ngx.location.capture?去方式實現,但是有一些限制,后邊我們再做介紹。
?
我們可以從github上搜索相應的客戶端,比如https://github.com/pintsized/lua-resty-http。
?
lua-resty-http
?
1、下載lua-resty-http客戶端到lualib?
Java代碼??
cd?/usr/example/lualib/resty/??wget?https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http_headers.lua??wget?https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http.lua?? ?
2、test_http_1.lua
Java代碼??
local?http?=?require("resty.http")??--創建http客戶端實例??local?httpc?=?http.new()????local?resp,?err?=?httpc:request_uri("http://s.taobao.com",?{??????method?=?"GET",??????path?=?"/search?q=hello",??????headers?=?{??????????["User-Agent"]?=?"Mozilla/5.0?(Windows?NT?6.1;?WOW64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/40.0.2214.111?Safari/537.36"??????}??})????if?not?resp?then??????ngx.say("request?error?:",?err)??????return??end????--獲取狀態碼??ngx.status?=?resp.status????--獲取響應頭??for?k,?v?in?pairs(resp.headers)?do??????if?k?~=?"Transfer-Encoding"?and?k?~=?"Connection"?then??????????ngx.header[k]?=?v??????end??end??--響應體??ngx.say(resp.body)????httpc:close()?? ?
響應頭中的Transfer-Encoding和Connection可以忽略,因為這個數據是當前server輸出的。
?
3、example.conf配置文件
Java代碼??
location?/lua_http_1?{?????default_type?'text/html';?????lua_code_cache?on;?????content_by_lua_file?/usr/example/lua/test_http_1.lua;??}??
4、在nginx.conf中的http部分添加如下指令來做DNS解析
Java代碼??
resolver?8.8.8.8;??
記得要配置DNS解析器resolver 8.8.8.8,否則域名是無法解析的。
5、訪問如http://192.168.1.2/lua_http_1會看到淘寶的搜索界面。
使用方式比較簡單,如超時和連接池設置和之前Redis客戶端一樣,不再闡述。更多客戶端使用規則請參考https://github.com/pintsized/lua-resty-http。
?
ngx.location.capture
ngx.location.capture也可以用來完成http請求,但是它只能請求到相對于當前nginx服務器的路徑,不能使用之前的絕對路徑進行訪問,但是我們可以配合nginx upstream實現我們想要的功能。
?
1、在nginx.cong中的http部分添加如下upstream配置
Java代碼??
upstream?backend?{??????server?s.taobao.com;??????keepalive?100;??}?? 即我們將請求upstream到backend;另外記得一定要添加之前的DNS解析器。
?
2、在example.conf配置如下location
Java代碼??
location?~?/proxy/(.*)?{?????internal;?????proxy_pass?http://backend/$1$is_args$args;??}?? internal表示只能內部訪問,即外部無法通過url訪問進來; 并通過proxy_pass將請求轉發到upstream。
?
3、test_http_2.lua
Java代碼??
local?resp?=?ngx.location.capture("/proxy/search",?{??????method?=?ngx.HTTP_GET,??????args?=?{q?=?"hello"}????})??if?not?resp?then??????ngx.say("request?error?:",?err)??????return??end??ngx.log(ngx.ERR,?tostring(resp.status))????--獲取狀態碼??ngx.status?=?resp.status????--獲取響應頭??for?k,?v?in?pairs(resp.header)?do??????if?k?~=?"Transfer-Encoding"?and?k?~=?"Connection"?then??????????ngx.header[k]?=?v??????end??end??--響應體??if?resp.body?then??????ngx.say(resp.body)??end?? 通過ngx.location.capture發送一個子請求,此處因為是子請求,所有請求頭繼承自當前請求,還有如ngx.ctx和ngx.var是否繼承可以參考官方文檔http://wiki.nginx.org/HttpLuaModule#ngx.location.capture。?另外還提供了ngx.location.capture_multi用于并發發出多個請求,這樣總的響應時間是最慢的一個,批量調用時有用。
?
4、example.conf配置文件
Java代碼??
location?/lua_http_2?{?????default_type?'text/html';?????lua_code_cache?on;?????content_by_lua_file?/usr/example/lua/test_http_2.lua;??}?? ?
5、訪問如http://192.168.1.2/lua_http_2進行測試可以看到淘寶搜索界面。
?
我們通過upstream+ngx.location.capture方式雖然麻煩點,但是得到更好的性能和upstream的連接池、負載均衡、故障轉移、proxy cache等特性。
?
不過因為繼承在當前請求的請求頭,所以可能會存在一些問題,比較常見的就是gzip壓縮問題,ngx.location.capture不會解壓縮后端服務器的GZIP內容,解決辦法可以參考https://github.com/openresty/lua-nginx-module/issues/12;因為我們大部分這種http調用的都是內部服務,因此完全可以在proxy location中添加proxy_pass_request_headers?off;來不傳遞請求頭。
來源:http://jinnianshilongnian.iteye.com/blog/2187328
總結
以上是生活随笔為你收集整理的第五章 常用Lua开发库1-redis、mysql、http客户端的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。