天天看點

OpenResty學習指南(二)

OpenResty學習指南(二)

我的個人部落格:https://www.luozhiyun.com/

資料結構table

table并沒有區分開數組、哈希、集合等概念,而是揉在了一起。

local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"])                 --> output: red
print(color[1])                         --> output: blue
print(color["third"])                --> output: green
print(color[2])                         --> output: yellow
print(color[3])                         --> output: nil           

複制

table 庫函數

擷取元素個數

對于序列,用table.getn 或者一進制操作符 # ,就可以正确傳回元素的個數。

$ resty -e 'local t = { 1, 2, 3 }
print(table.getn(t)) ' # 傳回3           

複制

不是序列的 table,就無法傳回正确的值。

$ resty -e 'local t = { 1, a = 2 }
print(#t) ' #傳回1           

複制

是以不要使用函數 table.getn 和一進制操作符 # 。

我們可以使用 table.nkeys 來擷取 table 長度,傳回的是 table 的元素個數,包括數組和哈希部分的元素。

local nkeys = require "table.nkeys"
 
print(nkeys({}))  -- 0
print(nkeys({ "a", nil, "b" }))  -- 2
print(nkeys({ dog = 3, cat = 4, bird = nil }))  -- 2
print(nkeys({ "a", dog = 3, cat = 4 }))  -- 3           

複制

删除指定元素

第二個我們來看table.remove 函數,它的作用是在 table 中根據下标來删除元素,也就是說隻能删除 table 中數組部分的元素

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
  table.remove(color, 1)
  for k, v in pairs(color) do
      print(v)
  end'           

複制

這段代碼會把下标為 1 的 blue 删除掉。

如果要删除哈希部分,把 key 對應的 value 設定為 nil 即可。

元素拼接函數

table.concat 以按照下标,把 table 中的元素拼接起來。隻能拼接數組部分

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
print(table.concat(color, ", "))'           

複制

使用table.concat函數後,它輸出的是 blue, yellow,哈希的部分被跳過了。

插入一個元素

table.insert 函數,可以下标插入一個新的元素,自然,影響的還是 table 的數組部分。

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.insert(color, 1,  "orange")
print(color[1])
'           

複制

color 的第一個元素變為了 orange。當然,你也可以不指定下标,這樣就會預設插入隊尾。

優化

預先生成數組

預先生成一個指定大小的數組,避免每次新增和删除數組元素的時候,都會涉及到數組的空間配置設定、resize 和 rehash。

如:

local new_tab = require "table.new"
 local t = new_tab(100, 0)
 for i = 1, 100 do
   t[i] = i
 end           

複制

循環使用單個 table

table.clear 函數它會把數組中的所有資料清空,但數組的大小不會變。

如下:

local local_plugins = {}
 
 
function load()
    core.table.clear(local_plugins)
 
 
    local local_conf = core.config.local_conf()
    local plugin_names = local_conf.plugins
 
 
    local processed = {}
    for _, name in ipairs(plugin_names) do
        if processed[name] == nil then
            processed[name] = true
            insert_tab(local_plugins, name)
        end
    end
 
 
    return local_plugins           

複制

local_plugins 這個數組,是 plugin 這個子產品的 top level 變量。在 load 這個加載插件函數的開始位置, table 就會被清空,然後根據目前的情況生成新的插件清單。

table 池

lua-tablepool,可以用緩存池的方式來儲存多個 table,以便随用随取。

local tablepool = require "tablepool"
 local tablepool_fetch = tablepool.fetch
 local tablepool_release = tablepool.release
 
 
local pool_name = "some_tag" 
local function do_sth()
     local t = tablepool_fetch(pool_name, 10, 0)
     -- -- using t for some purposes
    tablepool_release(pool_name, t) 
end           

複制

緩存

OpenResty 中有兩個緩存的元件:shared dict 緩存和 lru 緩存。前者隻能緩存字元串對象,緩存的資料有且隻有一份,每一個 worker 都可以進行通路,是以常用于 worker 之間的資料通信。後者則可以緩存所有的 Lua 對象,但隻能在單個 worker 程序内通路,有多少個 worker,就會有多少份緩存資料。

OpenResty學習指南(二)
OpenResty學習指南(二)

shared dict

$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
                               dict:set("Tom", 56)
                               print(dict:get("Tom"))'           

複制

需要事先在 Nginx 的配置檔案中,聲明一個記憶體區 dogs,然後在 Lua 代碼中才可以使用。

共享字典中還有一個 get_stale 的讀取資料的方法,相比 get 方法,多了一個過期資料的傳回值:

resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
                            dict:set("Tom", 56, 0.01)
                            ngx.sleep(0.02)
                             local val, flags, stale = dict:get_stale("Tom")
                            print(val)'           

複制

在上面的這個示例中,資料隻在共享字典中緩存了 0.01 秒,在 set 後的 0.02 秒後,資料就已經逾時了。這時候,通過 get 接口就不會擷取到資料了,但通過 get_stale 還可能擷取到過期的資料。

因為,在 shared dict 中存放的是緩存資料,即使緩存資料過期了,也并不意味着源資料就一定有更新。

lru 緩存

lru 緩存的接口隻有 5 個:new、set、get、delete 和 flush_all。

如何使用:

resty -e 'local lrucache = require "resty.lrucache"
local cache, err = lrucache.new(200)
cache:set("dog", 32, 0.01)
ngx.sleep(0.02)
local data, stale_data = cache:get("dog")
print(stale_data)'           

複制

可以看到,在 lru 緩存中, get 接口的第二個傳回值直接就是 stale_data,而不是像 shared dict 那樣分為了 get 和 get_stale 兩個不同的 API。

lua-resty-mlcache

lua-resty-mlcache用 shared dict 和 lua-resty-lrucache ,實作了多層緩存機制。

初始化:

local mlcache = require "resty.mlcache"
 
local cache, err = mlcache.new("cache_name", "cache_dict", {
    lru_size = 500,    -- size of the L1 (Lua VM) cache
    ttl = 3600,   -- 1h ttl for hits
    neg_ttl  = 30,     -- 30s ttl for misses
})
if not cache then
    error("failed to create mlcache: " .. err)
end           

複制

這段代碼的開頭引入了 mlcache 庫,并設定了初始化的參數。第一個參數是緩沖名,第二個參數是字典名,第三個參數是個字典,裡面是12個選填參數。

使用:

local function fetch_user(id)
    return db:query_user(id)
end
 
local id = 123
local user , err = cache:get(id , nil , fetch_user , id)
if err then
    ngx.log(ngx.ERR , "failed to fetch user: ", err)
    return
end
 
if user then
    print(user.id) -- 123
end           

複制

下面再看看這個庫的架構與實作:

OpenResty學習指南(二)

L1 緩存就是 lua-resty-lrucache。每一個 worker 中都有自己獨立的一份,有 N 個 worker,就會有 N 份資料,自然也就存在資料備援。

L2 緩存是 shared dict。所有的 worker 共用一份緩存資料,在 L1 緩存沒有命中的情況下,就會來查詢 L2 緩存。

L3 則是在 L2 緩存也沒有命中的情況下,需要執行回調函數去外部資料庫等資料源查詢後,再緩存到 L2 中。

整體而言,從請求的角度來看:

  1. 首先會去查詢 worker 内的 L1 緩存,如果 L1 命中就直接傳回。
  2. 如果 L1 沒有命中或者緩存失效,就會去查詢 worker 間的 L2 緩存。如果 L2 命中就傳回,并把結果緩存到 L1 中。
  3. 如果 L2 也沒有命中或者緩存失效,就會調用回調函數,從資料源中查到資料,并寫入到 L2 緩存中,這也就是 L3 資料層的功能。

需要做資料序列化的情況:

local mlcache = require "resty.mlcache"
 
local cache, err = mlcache.new("my_mlcache", "cache_shm", {
l1_serializer = function(i)
    return i + 2
end,
})
 
local function callback()
    return 123456
end
 
local data = assert(cache:get("number", nil, callback))
assert(data == 123458)           

複制

在 new 中,我們設定的 l1_serializer 函數會在設定 L1 緩存前,把傳入的數字加 2,也就是變成 123458。