<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # Lua 腳本 [TOC=2,3] Lua 腳本功能是 Reids 2.6 版本的最大亮點,通過內嵌對 Lua 環境的支持,Redis 解決了長久以來不能高效地處理 CAS (check-and-set)命令的缺點,并且可以通過組合使用多個命令,輕松實現以前很難實現或者不能高效實現的模式。 本章先介紹 Lua 環境的初始化步驟,然后對 Lua 腳本的安全性問題、以及解決這些問題的方法進行說明,最后對執行 Lua 腳本的兩個命令 —— [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 和 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 的實現原理進行介紹。 ### 初始化 Lua 環境 在初始化 Redis 服務器時,對 Lua 環境的初始化也會一并進行。 為了讓 Lua 環境符合 Redis 腳本功能的需求,Redis 對 Lua 環境進行了一系列的修改,包括添加函數庫、更換隨機函數、保護全局變量,等等。 整個初始化 Lua 環境的步驟如下: 1. 調用 [lua_open](http://www.lua.org/pil/24.1.html) 函數,創建一個新的 Lua 環境。 1. 載入指定的 Lua 函數庫,包括: - 基礎庫(base lib)。 - 表格庫(table lib)。 - 字符串庫(string lib)。 - 數學庫(math lib)。 - 調試庫(debug lib)。 - 用于處理 JSON 對象的 `cjson` 庫。 - 在 Lua 值和 C 結構(struct)之間進行轉換的 `struct` 庫([http://www.inf.puc-rio.br/~roberto/struct/](http://www.inf.puc-rio.br/~roberto/struct/))。 - 處理 MessagePack 數據的 `cmsgpack` 庫([https://github.com/antirez/lua-cmsgpack](https://github.com/antirez/lua-cmsgpack))。 1. 屏蔽一些可能對 Lua 環境產生安全問題的函數,比如 [loadfile](http://pgl.yoyo.org/luai/i/loadfile) 。 1. 創建一個 Redis 字典,保存 Lua 腳本,并在復制(replication)腳本時使用。字典的鍵為 SHA1 校驗和,字典的值為 Lua 腳本。 1. 創建一個 `redis` 全局表格到 Lua 環境,表格中包含了各種對 Redis 進行操作的函數,包括: - 用于執行 Redis 命令的 `redis.call` 和 `redis.pcall` 函數。 - 用于發送日志(log)的 `redis.log` 函數,以及相應的日志級別(level): - `redis.LOG_DEBUG` - `redis.LOG_VERBOSE` - `redis.LOG_NOTICE` - `redis.LOG_WARNING` - 用于計算 SHA1 校驗和的 `redis.sha1hex` 函數。 - 用于返回錯誤信息的 `redis.error_reply` 函數和 `redis.status_reply` 函數。 1. 用 Redis 自己定義的隨機生成函數,替換 `math` 表原有的 `math.random` 函數和 `math.randomseed` 函數,新的函數具有這樣的性質:每次執行 Lua 腳本時,除非顯式地調用 `math.randomseed` ,否則 `math.random` 生成的偽隨機數序列總是相同的。 1. 創建一個對 Redis 多批量回復(multi bulk reply)進行排序的輔助函數。 1. 對 Lua 環境中的全局變量進行保護,以免被傳入的腳本修改。 1. 因為 Redis 命令必須通過客戶端來執行,所以需要在服務器狀態中創建一個無網絡連接的偽客戶端(fake client),專門用于執行 Lua 腳本中包含的 Redis 命令:當 Lua 腳本需要執行 Redis 命令時,它通過偽客戶端來向服務器發送命令請求,服務器在執行完命令之后,將結果返回給偽客戶端,而偽客戶端又轉而將命令結果返回給 Lua 腳本。 1. 將 Lua 環境的指針記錄到 Redis 服務器的全局狀態中,等候 Redis 的調用。 以上就是 Redis 初始化 Lua 環境的整個過程,當這些步驟都執行完之后,Redis 就可以使用 Lua 環境來處理腳本了。 嚴格來說,步驟 1 至 8 才是初始化 Lua 環境的操作,而步驟 9 和 10 則是將 Lua 環境關聯到服務器的操作,為了按順序觀察整個初始化過程,我們將兩種操作放在了一起。 另外,步驟 6 用于創建無副作用的腳本,而步驟 7 則用于去除部分 Redis 命令中的不確定性(non deterministic),關于這兩點,請看下面一節關于腳本安全性的討論。 ### 腳本的安全性 當將 Lua 腳本復制到附屬節點,或者將 Lua 腳本寫入 AOF 文件時,Redis 需要解決這樣一個問題:如果一段 Lua 腳本帶有隨機性質或副作用,那么當這段腳本在附屬節點運行時,或者從 AOF 文件載入重新運行時,它得到的結果可能和之前運行的結果完全不同。 考慮以下一段代碼,其中的 `get_random_number()` 帶有隨機性質,我們在服務器 SERVER 中執行這段代碼,并將隨機數的結果保存到鍵 `number` 上: ~~~ # 虛構例子,不會真的出現在腳本環境中 redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number OK redis> GET number "10086" ~~~ 現在,假如 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 的代碼被復制到了附屬節點 SLAVE ,因為 `get_random_number()` 的隨機性質,它有很大可能會生成一個和 `10086` 完全不同的值,比如 `65535` : ~~~ # 虛構例子,不會真的出現在腳本環境中 redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number OK redis> GET number "65535" ~~~ 可以看到,帶有隨機性的寫入腳本產生了一個嚴重的問題:它破壞了服務器和附屬節點數據之間的一致性。 當從 AOF 文件中載入帶有隨機性質的寫入腳本時,也會發生同樣的問題。 Note 只有在帶有隨機性的腳本進行寫入時,隨機性才是有害的。 如果一個腳本只是執行只讀操作,那么隨機性是無害的。 比如說,如果腳本只是單純地執行 `RANDOMKEY` 命令,那么它是無害的;但如果在執行 `RANDOMKEY` 之后,基于 `RANDOMKEY` 的結果進行寫入操作,那么這個腳本就是有害的。 和隨機性質類似,如果一個腳本的執行對任何副作用產生了依賴,那么這個腳本每次執行所產生的結果都可能會不一樣。 為了解決這個問題,Redis 對 Lua 環境所能執行的腳本做了一個嚴格的限制 ——所有腳本都必須是無副作用的純函數(pure function)。 為此,Redis 對 Lua 環境做了一些列相應的措施: - 不提供訪問系統狀態狀態的庫(比如系統時間庫)。 - 禁止使用 [loadfile](http://pgl.yoyo.org/luai/i/loadfile) 函數。 - 如果腳本在執行帶有隨機性質的命令(比如 [RANDOMKEY](http://redis.readthedocs.org/en/latest/key/randomkey.html#randomkey "(in Redis 命令參考 v2.8)") ),或者帶有副作用的命令(比如 [TIME](http://redis.readthedocs.org/en/latest/server/time.html#time "(in Redis 命令參考 v2.8)") )之后,試圖執行一個寫入命令(比如 [SET](http://redis.readthedocs.org/en/latest/string/set.html#set "(in Redis 命令參考 v2.8)") ),那么 Redis 將阻止這個腳本繼續運行,并返回一個錯誤。 - 如果腳本執行了帶有隨機性質的讀命令(比如 [SMEMBERS](http://redis.readthedocs.org/en/latest/set/smembers.html#smembers "(in Redis 命令參考 v2.8)") ),那么在腳本的輸出返回給 Redis 之前,會先被執行一個自動的[字典序排序](http://en.wikipedia.org/wiki/Lexicographical_order) ,從而確保輸出結果是有序的。 - 用 Redis 自己定義的隨機生成函數,替換 Lua 環境中 `math` 表原有的 [math.random](http://pgl.yoyo.org/luai/i/math.random) 函數和 [math.randomseed](http://pgl.yoyo.org/luai/i/math.randomseed) 函數,新的函數具有這樣的性質:每次執行 Lua 腳本時,除非顯式地調用 `math.randomseed` ,否則 `math.random` 生成的偽隨機數序列總是相同的。 經過這一系列的調整之后,Redis 可以保證被執行的腳本: 1. 無副作用。 1. 沒有有害的隨機性。 1. 對于同樣的輸入參數和數據集,總是產生相同的寫入命令。 ### 腳本的執行 在腳本環境的初始化工作完成以后,Redis 就可以通過 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令或 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 命令執行 Lua 腳本了。 其中,[EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 直接對輸入的腳本代碼體(body)進行求值: ~~~ redis> EVAL "return 'hello world'" 0 "hello world" ~~~ 而 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 則要求輸入某個腳本的 SHA1 校驗和,這個校驗和所對應的腳本必須至少被 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 執行過一次: ~~~ redis> EVAL "return 'hello world'" 0 "hello world" redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0 // 上一個腳本的校驗和 "hello world" ~~~ 或者曾經使用 [SCRIPT LOAD](http://redis.readthedocs.org/en/latest/script/script_load.html#script-load "(in Redis 命令參考 v2.8)") 載入過這個腳本: ~~~ redis> SCRIPT LOAD "return 'dlrow olleh'" "d569c48906b1f4fca0469ba4eee89149b5148092" redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0 "dlrow olleh" ~~~ 因為 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 是基于 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 構建的,所以下文先用一節講解 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 的實現,之后再講解 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 的實現。 ### EVAL 命令的實現 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令的執行可以分為以下步驟: 1. 為輸入腳本定義一個 Lua 函數。 1. 執行這個 Lua 函數。 以下兩個小節分別介紹這兩個步驟。 ### 定義 Lua 函數 所有被 Redis 執行的 Lua 腳本,在 Lua 環境中都會有一個和該腳本相對應的無參數函數:當調用 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令執行腳本時,程序第一步要完成的工作就是為傳入的腳本創建一個相應的 Lua 函數。 舉個例子,當執行命令 `EVAL "return 'hello world'" 0` 時,Lua 會為腳本 `"return 'hello world'"` 創建以下函數: ~~~ function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91() return 'hello world' end ~~~ 其中,函數名以 `f_` 為前綴,后跟腳本的 SHA1 校驗和(一個 40 個字符長的字符串)拼接而成。而函數體(body)則是用戶輸入的腳本。 以函數為單位保存 Lua 腳本有以下好處: - 執行腳本的步驟非常簡單,只要調用和腳本相對應的函數即可。 - Lua 環境可以保持清潔,已有的腳本和新加入的腳本不會互相干擾,也可以將重置 Lua 環境和調用 Lua GC 的次數降到最低。 - 如果某個腳本所對應的函數在 Lua 環境中被定義過至少一次,那么只要記得這個腳本的 SHA1 校驗和,就可以直接執行該腳本 —— 這是實現 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 命令的基礎,稍后在介紹 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 的時候就會說到這一點。 在為腳本創建函數前,程序會先用函數名檢查 Lua 環境,只有在函數定義未存在時,程序才創建函數。重復定義函數一般并沒有什么副作用,這算是一個小優化。 另外,如果定義的函數在編譯過程中出錯(比如,腳本的代碼語法有錯),那么程序向用戶返回一個腳本錯誤,不再執行后面的步驟。 ### 執行 Lua 函數 在定義好 Lua 函數之后,程序就可以通過運行這個函數來達到運行輸入腳本的目的了。 不過,在此之前,為了確保腳本的正確和安全執行,還需要執行一些設置鉤子、傳入參數之類的操作,整個執行函數的過程如下: 1. 將 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令中輸入的 `KEYS` 參數和 `ARGV` 參數以全局數組的方式傳入到 Lua 環境中。 1. 設置偽客戶端的目標數據庫為調用者客戶端的目標數據庫: `fake_client->db = caller_client->db` ,確保腳本中執行的 Redis 命令訪問的是正確的數據庫。 1. 為 Lua 環境裝載超時鉤子,保證在腳本執行出現超時時可以殺死腳本,或者停止 Redis 服務器。 1. 執行腳本對應的 Lua 函數。 1. 如果被執行的 Lua 腳本中帶有 `SELECT` 命令,那么在腳本執行完畢之后,偽客戶端中的數據庫可能已經有所改變,所以需要對調用者客戶端的目標數據庫進行更新: `caller_client->db = fake_client->db` 。 1. 執行清理操作:清除鉤子;清除指向調用者客戶端的指針;等等。 1. 將 Lua 函數執行所得的結果轉換成 Redis 回復,然后傳給調用者客戶端。 1. 對 Lua 環境進行一次單步的漸進式 GC 。 以下是執行 `EVAL "return 'hello world'" 0` 的過程中,調用者客戶端(caller)、Redis 服務器和 Lua 環境之間的數據流表示圖: ~~~ 發送命令請求 EVAL "return 'hello world'" 0 Caller ----------------------------------------> Redis 為腳本 "return 'hello world'" 創建 Lua 函數 Redis ----------------------------------------> Lua 綁定超時處理鉤子 Redis ----------------------------------------> Lua 執行腳本函數 Redis ----------------------------------------> Lua 返回函數執行結果(一個 Lua 值) Redis <---------------------------------------- Lua 將 Lua 值轉換為 Redis 回復 并將結果返回給客戶端 Caller <---------------------------------------- Redis ~~~ 上面這個圖可以作為所有 Lua 腳本的基本執行流程圖,不過它展示的 Lua 腳本中不帶有 Redis 命令調用:當 Lua 腳本里本身有調用 Redis 命令時(執行 `redis.call` 或者 `redis.pcall` ),Redis 和 Lua 腳本之間的數據交互會更復雜一些。 舉個例子,以下是執行命令 `EVAL "return redis.call('DBSIZE')" 0` 時,調用者客戶端(caller)、偽客戶端(fake client)、Redis 服務器和 Lua 環境之間的數據流表示圖: ~~~ 發送命令請求 EVAL "return redis.call('DBSIZE')" 0 Caller ------------------------------------------> Redis 為腳本 "return redis.call('DBSIZE')" 創建 Lua 函數 Redis ------------------------------------------> Lua 綁定超時處理鉤子 Redis ------------------------------------------> Lua 執行腳本函數 Redis ------------------------------------------> Lua 執行 redis.call('DBSIZE') Fake Client <------------------------------------- Lua 偽客戶端向服務器發送 DBSIZE 命令請求 Fake Client -------------------------------------> Redis 服務器將 DBSIZE 的結果 (Redis 回復)返回給偽客戶端 Fake Client <------------------------------------- Redis 將命令回復轉換為 Lua 值 并返回給 Lua 環境 Fake Client -------------------------------------> Lua 返回函數執行結果(一個 Lua 值) Redis <------------------------------------------ Lua 將 Lua 值轉換為 Redis 回復 并將該回復返回給客戶端 Caller <------------------------------------------ Redis ~~~ 因為 `EVAL "return redis.call('DBSIZE')"` 只是簡單地調用了一次 `DBSIZE` 命令,所以 Lua 和偽客戶端只進行了一趟交互,當腳本中的 `redis.call` 或者 `redis.pcall` 次數增多時,Lua 和偽客戶端的交互趟數也會相應地增多,不過總體的交互方法和上圖展示的一樣。 ### EVALSHA 命令的實現 前面介紹 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令的實現時說過,每個被執行過的 Lua 腳本,在 Lua 環境中都有一個和它相對應的函數,函數的名字由 `f_` 前綴加上 40 個字符長的 SHA1 校驗和構成:比如 `f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91` 。 只要腳本所對應的函數曾經在 Lua 里面定義過,那么即使用戶不知道腳本的內容本身,也可以直接通過腳本的 SHA1 校驗和來調用腳本所對應的函數,從而達到執行腳本的目的 ——這就是 [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 命令的實現原理。 可以用偽代碼來描述這一原理: ~~~ def EVALSHA(sha1): # 拼接出 Lua 函數名字 func_name = "f_" + sha1 # 查看該函數是否已經在 Lua 中定義 if function_defined_in_lua(func_name): # 如果已經定義過的話,執行函數 return exec_lua_function(func_name) else: # 沒有找到和輸入 SHA1 值相對應的函數則返回一個腳本未找到錯誤 return script_error("SCRIPT NOT FOUND") ~~~ 除了執行 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令之外,[SCRIPT LOAD](http://redis.readthedocs.org/en/latest/script/script_load.html#script-load "(in Redis 命令參考 v2.8)") 命令也可以為腳本在 Lua 環境中創建函數: ~~~ redis> SCRIPT LOAD "return 'hello world'" "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0 "hello world" ~~~ [SCRIPT LOAD](http://redis.readthedocs.org/en/latest/script/script_load.html#script-load "(in Redis 命令參考 v2.8)") 執行的操作和前面《[定義 Lua 函數](#)》小節描述的一樣。 ### 小結 - 初始化 Lua 腳本環境需要一系列步驟,其中最重要的包括: - 創建 Lua 環境。 - 載入 Lua 庫,比如字符串庫、數學庫、表格庫,等等。 - 創建 `redis` 全局表格,包含各種對 Redis 進行操作的函數,比如 `redis.call` 和 `redis.log` ,等等。 - 創建一個無網絡連接的偽客戶端,專門用于執行 Lua 腳本中的 Redis 命令。 - Reids 通過一系列措施保證被執行的 Lua 腳本無副作用,也沒有有害的寫隨機性:對于同樣的輸入參數和數據集,總是產生相同的寫入命令。 - [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html#eval "(in Redis 命令參考 v2.8)") 命令為輸入腳本定義一個 Lua 函數,然后通過執行這個函數來執行腳本。 - [EVALSHA](http://redis.readthedocs.org/en/latest/script/evalsha.html#evalsha "(in Redis 命令參考 v2.8)") 通過構建函數名,直接調用 Lua 中已定義的函數,從而執行相應的腳本。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看