国产gaysexchina男同gay,japanrcep老熟妇乱子伦视频,吃奶呻吟打开双腿做受动态图,成人色网站,国产av一区二区三区最新精品

Lua 腳本

2018-02-24 15:46 更新

Lua 腳本

Lua 腳本功能是 Reids 2.6 版本的最大亮點,通過內(nèi)嵌對 Lua 環(huán)境的支持,Redis 解決了長久以來不能高效地處理 CAS (check-and-set)命令的缺點,并且可以通過組合使用多個命令,輕松實現(xiàn)以前很難實現(xiàn)或者不能高效實現(xiàn)的模式。

本章先介紹 Lua 環(huán)境的初始化步驟,然后對 Lua 腳本的安全性問題、以及解決這些問題的方法進行說明,最后對執(zhí)行 Lua 腳本的兩個命令 —— EVALEVALSHA 的實現(xiàn)原理進行介紹。

初始化 Lua 環(huán)境

在初始化 Redis 服務(wù)器時,對 Lua 環(huán)境的初始化也會一并進行。

為了讓 Lua 環(huán)境符合 Redis 腳本功能的需求,Redis 對 Lua 環(huán)境進行了一系列的修改,包括添加函數(shù)庫、更換隨機函數(shù)、保護全局變量,等等。

整個初始化 Lua 環(huán)境的步驟如下:

  1. 調(diào)用 lua_open 函數(shù),創(chuàng)建一個新的 Lua 環(huán)境。
  2. 載入指定的 Lua 函數(shù)庫,包括:
  • 基礎(chǔ)庫(base lib)。
  • 表格庫(table lib)。
  • 字符串庫(string lib)。
  • 數(shù)學(xué)庫(math lib)。
  • 調(diào)試庫(debug lib)。
  • 用于處理 JSON 對象的 cjson 庫。
  • 在 Lua 值和 C 結(jié)構(gòu)(struct)之間進行轉(zhuǎn)換的 struct 庫(http://www.inf.puc-rio.br/~roberto/struct/)。
  • 處理 MessagePack 數(shù)據(jù)的 cmsgpack 庫(https://github.com/antirez/lua-cmsgpack)。
  1. 屏蔽一些可能對 Lua 環(huán)境產(chǎn)生安全問題的函數(shù),比如 loadfile
  2. 創(chuàng)建一個 Redis 字典,保存 Lua 腳本,并在復(fù)制(replication)腳本時使用。字典的鍵為 SHA1 校驗和,字典的值為 Lua 腳本。
  3. 創(chuàng)建一個 redis 全局表格到 Lua 環(huán)境,表格中包含了各種對 Redis 進行操作的函數(shù),包括:
  • 用于執(zhí)行 Redis 命令的 redis.callredis.pcall 函數(shù)。
  • 用于發(fā)送日志(log)的 redis.log 函數(shù),以及相應(yīng)的日志級別(level):

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

  • 用于計算 SHA1 校驗和的 redis.sha1hex 函數(shù)。
  • 用于返回錯誤信息的 redis.error_reply 函數(shù)和 redis.status_reply 函數(shù)。
  1. 用 Redis 自己定義的隨機生成函數(shù),替換 math 表原有的 math.random 函數(shù)和 math.randomseed 函數(shù),新的函數(shù)具有這樣的性質(zhì):每次執(zhí)行 Lua 腳本時,除非顯式地調(diào)用 math.randomseed ,否則 math.random 生成的偽隨機數(shù)序列總是相同的。
  2. 創(chuàng)建一個對 Redis 多批量回復(fù)(multi bulk reply)進行排序的輔助函數(shù)。
  3. 對 Lua 環(huán)境中的全局變量進行保護,以免被傳入的腳本修改。
  4. 因為 Redis 命令必須通過客戶端來執(zhí)行,所以需要在服務(wù)器狀態(tài)中創(chuàng)建一個無網(wǎng)絡(luò)連接的偽客戶端(fake client),專門用于執(zhí)行 Lua 腳本中包含的 Redis 命令:當(dāng) Lua 腳本需要執(zhí)行 Redis 命令時,它通過偽客戶端來向服務(wù)器發(fā)送命令請求,服務(wù)器在執(zhí)行完命令之后,將結(jié)果返回給偽客戶端,而偽客戶端又轉(zhuǎn)而將命令結(jié)果返回給 Lua 腳本。
  5. 將 Lua 環(huán)境的指針記錄到 Redis 服務(wù)器的全局狀態(tài)中,等候 Redis 的調(diào)用。

以上就是 Redis 初始化 Lua 環(huán)境的整個過程,當(dāng)這些步驟都執(zhí)行完之后,Redis 就可以使用 Lua 環(huán)境來處理腳本了。

嚴格來說,步驟 1 至 8 才是初始化 Lua 環(huán)境的操作,而步驟 9 和 10 則是將 Lua 環(huán)境關(guān)聯(lián)到服務(wù)器的操作,為了按順序觀察整個初始化過程,我們將兩種操作放在了一起。

另外,步驟 6 用于創(chuàng)建無副作用的腳本,而步驟 7 則用于去除部分 Redis 命令中的不確定性(non deterministic),關(guān)于這兩點,請看下面一節(jié)關(guān)于腳本安全性的討論。

腳本的安全性

當(dāng)將 Lua 腳本復(fù)制到附屬節(jié)點,或者將 Lua 腳本寫入 AOF 文件時,Redis 需要解決這樣一個問題:如果一段 Lua 腳本帶有隨機性質(zhì)或副作用,那么當(dāng)這段腳本在附屬節(jié)點運行時,或者從 AOF 文件載入重新運行時,它得到的結(jié)果可能和之前運行的結(jié)果完全不同。

考慮以下一段代碼,其中的 get_random_number() 帶有隨機性質(zhì),我們在服務(wù)器 SERVER 中執(zhí)行這段代碼,并將隨機數(shù)的結(jié)果保存到鍵 number 上:

# 虛構(gòu)例子,不會真的出現(xiàn)在腳本環(huán)境中

redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK

redis> GET number
"10086"

現(xiàn)在,假如 EVAL 的代碼被復(fù)制到了附屬節(jié)點 SLAVE ,因為 get_random_number() 的隨機性質(zhì),它有很大可能會生成一個和 10086 完全不同的值,比如 65535

# 虛構(gòu)例子,不會真的出現(xiàn)在腳本環(huán)境中

redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK

redis> GET number
"65535"

可以看到,帶有隨機性的寫入腳本產(chǎn)生了一個嚴重的問題:它破壞了服務(wù)器和附屬節(jié)點數(shù)據(jù)之間的一致性。

當(dāng)從 AOF 文件中載入帶有隨機性質(zhì)的寫入腳本時,也會發(fā)生同樣的問題。

Note

只有在帶有隨機性的腳本進行寫入時,隨機性才是有害的。

如果一個腳本只是執(zhí)行只讀操作,那么隨機性是無害的。

比如說,如果腳本只是單純地執(zhí)行 RANDOMKEY 命令,那么它是無害的;但如果在執(zhí)行 RANDOMKEY 之后,基于 RANDOMKEY 的結(jié)果進行寫入操作,那么這個腳本就是有害的。

和隨機性質(zhì)類似,如果一個腳本的執(zhí)行對任何副作用產(chǎn)生了依賴,那么這個腳本每次執(zhí)行所產(chǎn)生的結(jié)果都可能會不一樣。

為了解決這個問題,Redis 對 Lua 環(huán)境所能執(zhí)行的腳本做了一個嚴格的限制 ——所有腳本都必須是無副作用的純函數(shù)(pure function)。

為此,Redis 對 Lua 環(huán)境做了一些列相應(yīng)的措施:

  • 不提供訪問系統(tǒng)狀態(tài)狀態(tài)的庫(比如系統(tǒng)時間庫)。
  • 禁止使用 loadfile 函數(shù)。
  • 如果腳本在執(zhí)行帶有隨機性質(zhì)的命令(比如 RANDOMKEY ),或者帶有副作用的命令(比如 TIME )之后,試圖執(zhí)行一個寫入命令(比如 SET ),那么 Redis 將阻止這個腳本繼續(xù)運行,并返回一個錯誤。
  • 如果腳本執(zhí)行了帶有隨機性質(zhì)的讀命令(比如 SMEMBERS ),那么在腳本的輸出返回給 Redis 之前,會先被執(zhí)行一個自動的字典序排序 ,從而確保輸出結(jié)果是有序的。
  • 用 Redis 自己定義的隨機生成函數(shù),替換 Lua 環(huán)境中 math 表原有的 math.random 函數(shù)和 math.randomseed 函數(shù),新的函數(shù)具有這樣的性質(zhì):每次執(zhí)行 Lua 腳本時,除非顯式地調(diào)用 math.randomseed ,否則 math.random 生成的偽隨機數(shù)序列總是相同的。

經(jīng)過這一系列的調(diào)整之后,Redis 可以保證被執(zhí)行的腳本:

  1. 無副作用。
  2. 沒有有害的隨機性。
  3. 對于同樣的輸入?yún)?shù)和數(shù)據(jù)集,總是產(chǎn)生相同的寫入命令。

腳本的執(zhí)行

在腳本環(huán)境的初始化工作完成以后,Redis 就可以通過 EVAL 命令或 EVALSHA 命令執(zhí)行 Lua 腳本了。

其中,EVAL 直接對輸入的腳本代碼體(body)進行求值:

redis> EVAL "return 'hello world'" 0
"hello world"

EVALSHA 則要求輸入某個腳本的 SHA1 校驗和,這個校驗和所對應(yīng)的腳本必須至少被 EVAL 執(zhí)行過一次:

redis> EVAL "return 'hello world'" 0
"hello world"

redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0    // 上一個腳本的校驗和
"hello world"

或者曾經(jīng)使用 SCRIPT LOAD 載入過這個腳本:

redis> SCRIPT LOAD "return 'dlrow olleh'"
"d569c48906b1f4fca0469ba4eee89149b5148092"

redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0
"dlrow olleh"

因為 EVALSHA 是基于 EVAL 構(gòu)建的,所以下文先用一節(jié)講解 EVAL 的實現(xiàn),之后再講解 EVALSHA 的實現(xiàn)。

EVAL 命令的實現(xiàn)

EVAL 命令的執(zhí)行可以分為以下步驟:

  1. 為輸入腳本定義一個 Lua 函數(shù)。
  2. 執(zhí)行這個 Lua 函數(shù)。

以下兩個小節(jié)分別介紹這兩個步驟。

定義 Lua 函數(shù)

所有被 Redis 執(zhí)行的 Lua 腳本,在 Lua 環(huán)境中都會有一個和該腳本相對應(yīng)的無參數(shù)函數(shù):當(dāng)調(diào)用 EVAL 命令執(zhí)行腳本時,程序第一步要完成的工作就是為傳入的腳本創(chuàng)建一個相應(yīng)的 Lua 函數(shù)。

舉個例子,當(dāng)執(zhí)行命令 EVAL "return 'hello world'" 0 時,Lua 會為腳本 "return 'hello world'" 創(chuàng)建以下函數(shù):

function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
    return 'hello world'
end

其中,函數(shù)名以 f_ 為前綴,后跟腳本的 SHA1 校驗和(一個 40 個字符長的字符串)拼接而成。而函數(shù)體(body)則是用戶輸入的腳本。

以函數(shù)為單位保存 Lua 腳本有以下好處:

  • 執(zhí)行腳本的步驟非常簡單,只要調(diào)用和腳本相對應(yīng)的函數(shù)即可。
  • Lua 環(huán)境可以保持清潔,已有的腳本和新加入的腳本不會互相干擾,也可以將重置 Lua 環(huán)境和調(diào)用 Lua GC 的次數(shù)降到最低。
  • 如果某個腳本所對應(yīng)的函數(shù)在 Lua 環(huán)境中被定義過至少一次,那么只要記得這個腳本的 SHA1 校驗和,就可以直接執(zhí)行該腳本 —— 這是實現(xiàn) EVALSHA 命令的基礎(chǔ),稍后在介紹 EVALSHA 的時候就會說到這一點。

在為腳本創(chuàng)建函數(shù)前,程序會先用函數(shù)名檢查 Lua 環(huán)境,只有在函數(shù)定義未存在時,程序才創(chuàng)建函數(shù)。重復(fù)定義函數(shù)一般并沒有什么副作用,這算是一個小優(yōu)化。

另外,如果定義的函數(shù)在編譯過程中出錯(比如,腳本的代碼語法有錯),那么程序向用戶返回一個腳本錯誤,不再執(zhí)行后面的步驟。

執(zhí)行 Lua 函數(shù)

在定義好 Lua 函數(shù)之后,程序就可以通過運行這個函數(shù)來達到運行輸入腳本的目的了。

不過,在此之前,為了確保腳本的正確和安全執(zhí)行,還需要執(zhí)行一些設(shè)置鉤子、傳入?yún)?shù)之類的操作,整個執(zhí)行函數(shù)的過程如下:

  1. EVAL 命令中輸入的 KEYS 參數(shù)和 ARGV 參數(shù)以全局數(shù)組的方式傳入到 Lua 環(huán)境中。
  2. 設(shè)置偽客戶端的目標數(shù)據(jù)庫為調(diào)用者客戶端的目標數(shù)據(jù)庫: fake_client->db = caller_client->db ,確保腳本中執(zhí)行的 Redis 命令訪問的是正確的數(shù)據(jù)庫。
  3. 為 Lua 環(huán)境裝載超時鉤子,保證在腳本執(zhí)行出現(xiàn)超時時可以殺死腳本,或者停止 Redis 服務(wù)器。
  4. 執(zhí)行腳本對應(yīng)的 Lua 函數(shù)。
  5. 如果被執(zhí)行的 Lua 腳本中帶有 SELECT 命令,那么在腳本執(zhí)行完畢之后,偽客戶端中的數(shù)據(jù)庫可能已經(jīng)有所改變,所以需要對調(diào)用者客戶端的目標數(shù)據(jù)庫進行更新: caller_client->db = fake_client->db 。
  6. 執(zhí)行清理操作:清除鉤子;清除指向調(diào)用者客戶端的指針;等等。
  7. 將 Lua 函數(shù)執(zhí)行所得的結(jié)果轉(zhuǎn)換成 Redis 回復(fù),然后傳給調(diào)用者客戶端。
  8. 對 Lua 環(huán)境進行一次單步的漸進式 GC 。

以下是執(zhí)行 EVAL "return 'hello world'" 0 的過程中,調(diào)用者客戶端(caller)、Redis 服務(wù)器和 Lua 環(huán)境之間的數(shù)據(jù)流表示圖:

          發(fā)送命令請求
          EVAL "return 'hello world'" 0
Caller ----------------------------------------> Redis

          為腳本 "return 'hello world'"
          創(chuàng)建 Lua 函數(shù)
Redis  ----------------------------------------> Lua

          綁定超時處理鉤子
Redis  ----------------------------------------> Lua

          執(zhí)行腳本函數(shù)
Redis  ----------------------------------------> Lua

          返回函數(shù)執(zhí)行結(jié)果(一個 Lua 值)
Redis  <---------------------------------------- Lua

          將 Lua 值轉(zhuǎn)換為 Redis 回復(fù)
          并將結(jié)果返回給客戶端
Caller <---------------------------------------- Redis

上面這個圖可以作為所有 Lua 腳本的基本執(zhí)行流程圖,不過它展示的 Lua 腳本中不帶有 Redis 命令調(diào)用:當(dāng) Lua 腳本里本身有調(diào)用 Redis 命令時(執(zhí)行 redis.call 或者 redis.pcall ),Redis 和 Lua 腳本之間的數(shù)據(jù)交互會更復(fù)雜一些。

舉個例子,以下是執(zhí)行命令 EVAL "return redis.call('DBSIZE')" 0 時,調(diào)用者客戶端(caller)、偽客戶端(fake client)、Redis 服務(wù)器和 Lua 環(huán)境之間的數(shù)據(jù)流表示圖:

          發(fā)送命令請求
          EVAL "return redis.call('DBSIZE')" 0
Caller ------------------------------------------> Redis

          為腳本 "return redis.call('DBSIZE')"
          創(chuàng)建 Lua 函數(shù)
Redis  ------------------------------------------> Lua

          綁定超時處理鉤子
Redis  ------------------------------------------> Lua

          執(zhí)行腳本函數(shù)
Redis  ------------------------------------------> Lua

               執(zhí)行 redis.call('DBSIZE')
Fake Client <------------------------------------- Lua

               偽客戶端向服務(wù)器發(fā)送
               DBSIZE 命令請求
Fake Client -------------------------------------> Redis

               服務(wù)器將 DBSIZE 的結(jié)果
               (Redis 回復(fù))返回給偽客戶端
Fake Client <------------------------------------- Redis

               將命令回復(fù)轉(zhuǎn)換為 Lua 值
               并返回給 Lua 環(huán)境
Fake Client -------------------------------------> Lua

          返回函數(shù)執(zhí)行結(jié)果(一個 Lua 值)
Redis  <------------------------------------------ Lua

          將 Lua 值轉(zhuǎn)換為 Redis 回復(fù)
          并將該回復(fù)返回給客戶端
Caller <------------------------------------------ Redis

因為 EVAL "return redis.call('DBSIZE')" 只是簡單地調(diào)用了一次 DBSIZE 命令,所以 Lua 和偽客戶端只進行了一趟交互,當(dāng)腳本中的 redis.call 或者 redis.pcall 次數(shù)增多時,Lua 和偽客戶端的交互趟數(shù)也會相應(yīng)地增多,不過總體的交互方法和上圖展示的一樣。

EVALSHA 命令的實現(xiàn)

前面介紹 EVAL 命令的實現(xiàn)時說過,每個被執(zhí)行過的 Lua 腳本,在 Lua 環(huán)境中都有一個和它相對應(yīng)的函數(shù),函數(shù)的名字由 f_ 前綴加上 40 個字符長的 SHA1 校驗和構(gòu)成:比如 f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91 。

只要腳本所對應(yīng)的函數(shù)曾經(jīng)在 Lua 里面定義過,那么即使用戶不知道腳本的內(nèi)容本身,也可以直接通過腳本的 SHA1 校驗和來調(diào)用腳本所對應(yīng)的函數(shù),從而達到執(zhí)行腳本的目的 ——這就是 EVALSHA 命令的實現(xiàn)原理。

可以用偽代碼來描述這一原理:

def EVALSHA(sha1):

    # 拼接出 Lua 函數(shù)名字
    func_name = "f_" + sha1

    # 查看該函數(shù)是否已經(jīng)在 Lua 中定義
    if function_defined_in_lua(func_name):

        # 如果已經(jīng)定義過的話,執(zhí)行函數(shù)
        return exec_lua_function(func_name)

    else:

        # 沒有找到和輸入 SHA1 值相對應(yīng)的函數(shù)則返回一個腳本未找到錯誤
        return script_error("SCRIPT NOT FOUND")

除了執(zhí)行 EVAL 命令之外,SCRIPT LOAD 命令也可以為腳本在 Lua 環(huán)境中創(chuàng)建函數(shù):

redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"

redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"

SCRIPT LOAD 執(zhí)行的操作和前面《定義 Lua 函數(shù)》小節(jié)描述的一樣。

小結(jié)

  • 初始化 Lua 腳本環(huán)境需要一系列步驟,其中最重要的包括:

  • 創(chuàng)建 Lua 環(huán)境。
  • 載入 Lua 庫,比如字符串庫、數(shù)學(xué)庫、表格庫,等等。
  • 創(chuàng)建 redis 全局表格,包含各種對 Redis 進行操作的函數(shù),比如 redis.callredis.log ,等等。
  • 創(chuàng)建一個無網(wǎng)絡(luò)連接的偽客戶端,專門用于執(zhí)行 Lua 腳本中的 Redis 命令。

  • Reids 通過一系列措施保證被執(zhí)行的 Lua 腳本無副作用,也沒有有害的寫隨機性:對于同樣的輸入?yún)?shù)和數(shù)據(jù)集,總是產(chǎn)生相同的寫入命令。
  • EVAL 命令為輸入腳本定義一個 Lua 函數(shù),然后通過執(zhí)行這個函數(shù)來執(zhí)行腳本。
  • EVALSHA 通過構(gòu)建函數(shù)名,直接調(diào)用 Lua 中已定義的函數(shù),從而執(zhí)行相應(yīng)的腳本。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號