Redis創(chuàng)建并修改Lua 環(huán)境的實現(xiàn)方法
為了在Redis 服務器中執(zhí)行 Lua 腳本, Redis 在服務器內嵌了一個 Lua 環(huán)境(environment), 并對這個 Lua 環(huán)境進行了一系列修改, 從而確保這個 Lua 環(huán)境可以滿足 Redis 服務器的需要。
Redis 服務器創(chuàng)建并修改 Lua 環(huán)境的整個過程由以下步驟組成:
1、 創(chuàng)建一個基礎的Lua環(huán)境,之后的所有修改都是針對這個環(huán)境進行的;
2、 載入多個函數(shù)庫到Lua環(huán)境里面,讓Lua腳本可以使用這些函數(shù)庫來進行數(shù)據(jù)操作;
3、 創(chuàng)建全局表格redis,這個表格包含了對Redis進行操作的函數(shù),比如用于在Lua腳本中執(zhí)行Redis命令的redis.call函數(shù);
4、 使用Redis自制的隨機函數(shù)來替換Lua原有的帶有副作用的隨機函數(shù),從而避免在腳本中引入副作用;
5、 創(chuàng)建排序輔助函數(shù),Lua環(huán)境使用這個輔佐函數(shù)來對一部分Redis命令的結果進行排序,從而消除這些命令的不確定性;
6、 創(chuàng)建redis.pcall函數(shù)的錯誤報告輔助函數(shù),這個函數(shù)可以提供更詳細的出錯信息;
7、 對Lua環(huán)境里面的全局環(huán)境進行保護,防止用戶在執(zhí)行Lua腳本的過程中,將額外的全局變量添加到了Lua環(huán)境里面;
8、 將完成修改的Lua環(huán)境保存到服務器狀態(tài)的lua屬性里面,等待執(zhí)行服務器傳來的Lua腳本;
接下來的各個小節(jié)將分別介紹這些步驟。
創(chuàng)建 Lua 環(huán)境
在最開始的這一步, 服務器首先調用 Lua 的 C API 函數(shù) lua_open , 創(chuàng)建一個新的 Lua 環(huán)境。
因為lua_open 函數(shù)創(chuàng)建的只是一個基本的 Lua 環(huán)境, 為了讓這個 Lua 環(huán)境可以滿足 Redis 的操作要求, 接下來服務器將對這個 Lua 環(huán)境進行一系列修改。
載入函數(shù)庫
Redis 修改 Lua 環(huán)境的第一步, 就是將以下函數(shù)庫載入到 Lua 環(huán)境里面:
- 基礎庫(base library): 這個庫包含 Lua 的核心(core)函數(shù), 比如 assert 、 error 、 pairs 、 tostring 、 pcall , 等等。 另外, 為了防止用戶從外部文件中引入不安全的代碼, 庫中的 loadfile 函數(shù)會被刪除。
- 表格庫(table library): 這個庫包含用于處理表格的通用函數(shù), 比如 table.concat 、 table.insert 、 table.remove 、 table.sort, 等等。
- 字符串庫(string library): 這個庫包含用于處理字符串的通用函數(shù), 比如用于對字符串進行查找的 string.find 函數(shù), 對字符串進行格式化的 string.format 函數(shù), 查看字符串長度的 string.len 函數(shù), 對字符串進行翻轉的 string.reverse 函數(shù), 等等。
- 數(shù)學庫(math library): 這個庫是標準 C 語言數(shù)學庫的接口, 它包括計算絕對值的 math.abs 函數(shù), 返回多個數(shù)中的最大值和最小值的 math.max 函數(shù)和 math.min 函數(shù), 計算二次方根的 math.sqrt 函數(shù), 計算對數(shù)的 math.log 函數(shù), 等等。
- 調試庫(debug library): 這個庫提供了對程序進行調試所需的函數(shù), 比如對程序設置鉤子和取得鉤子的 debug.sethook 函數(shù)和debug.gethook 函數(shù), 返回給定函數(shù)相關信息的 debug.getinfo 函數(shù), 為對象設置元數(shù)據(jù)的 debug.setmetatable 函數(shù), 獲取對象元數(shù)據(jù)的debug.getmetatable 函數(shù), 等等。
- Lua CJSON 庫(http://www.kyne.com.au/~mark/software/lua-cjson.php): 這個庫用于處理 UTF-8 編碼的 JSON 格式, 其中cjson.decode 函數(shù)將一個 JSON 格式的字符串轉換為一個 Lua 值, 而 cjson.encode 函數(shù)將一個 Lua 值序列化為 JSON 格式的字符串。
- Struct 庫(http://www.inf.puc-rio.br/~roberto/struct/): 這個庫用于在 Lua 值和 C 結構(struct)之間進行轉換, 函數(shù)struct.pack 將多個 Lua 值打包成一個類結構(struct-like)字符串, 而函數(shù) struct.unpack 則從一個類結構字符串中解包出多個 Lua 值。
- Lua cmsgpack 庫(https://github.com/antirez/lua-cmsgpack): 這個庫用于處理 MessagePack 格式的數(shù)據(jù), 其中 cmsgpack.pack 函數(shù)將 Lua 值轉換為 MessagePack 數(shù)據(jù), 而 cmsgpack.unpack 函數(shù)則將 MessagePack 數(shù)據(jù)轉換為 Lua 值。
通過使用這些功能強大的函數(shù)庫, Lua 腳本可以直接對執(zhí)行 Redis 命令獲得的數(shù)據(jù)進行復雜的操作。
創(chuàng)建 redis 全局表格
在這一步, 服務器將在 Lua 環(huán)境中創(chuàng)建一個 redis 表格(table), 并將它設為全局變量。
這個redis 表格包含以下函數(shù):
- 用于執(zhí)行 Redis 命令的 redis.call 和 redis.pcall 函數(shù)。
- 用于記錄 Redis 日志(log)的 redis.log 函數(shù), 以及相應的日志級別(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ù)。
在這些函數(shù)里面, 最常用也最重要的要數(shù) redis.call 函數(shù)和 redis.pcall 函數(shù) —— 通過這兩個函數(shù), 用戶可以直接在 Lua 腳本中執(zhí)行 Redis 命令:
redis> EVAL "return redis.call('PING')" 0 PONG
使用 Redis 自制的隨機函數(shù)來替換 Lua 原有的隨機函數(shù)
為了保證相同的腳本可以在不同的機器上產生相同的結果, Redis 要求所有傳入服務器的 Lua 腳本, 以及 Lua 環(huán)境中的所有函數(shù), 都必須是無副作用(side effect)的純函數(shù)(pure function)。
但是,在之前載入到 Lua 環(huán)境的 math 函數(shù)庫中, 用于生成隨機數(shù)的 math.random 函數(shù)和 math.randomseed 函數(shù)都是帶有副作用的, 它們不符合 Redis 對 Lua 環(huán)境的無副作用要求。
因為這個原因, Redis 使用自制的函數(shù)替換了 math 庫中原有的 math.random 函數(shù)和 math.randomseed 函數(shù), 替換之后的兩個函數(shù)有以下特征:
對于相同的 seed 來說, math.random 總產生相同的隨機數(shù)序列, 這個函數(shù)是一個純函數(shù)。
除非在腳本中使用 math.randomseed 顯式地修改 seed , 否則每次運行腳本時, Lua 環(huán)境都使用固定的 math.randomseed(0) 語句來初始化 seed 。
比如說, 使用以下腳本, 我們可以打印 seed 值為 0 時, math.random 對于輸入 10 至 1 所產生的隨機序列:
無論執(zhí)行這個腳本多少次, 產生的值都是相同的:
$ redis-cli --eval random-with-default-seed.lua 1) (integer) 1 2) (integer) 2 3) (integer) 2 4) (integer) 3 5) (integer) 4 6) (integer) 4 7) (integer) 7 8) (integer) 1 9) (integer) 7 10) (integer) 2
但是,如果我們在另一個腳本里面, 調用 math.randomseed 將 seed 修改為 10086 :
那么這個腳本生成的隨機數(shù)序列將和使用默認 seed 值 0 時生成的隨機序列不同:
$ redis-cli --eval random-with-new-seed.lua 1) (integer) 1 2) (integer) 1 3) (integer) 2 4) (integer) 1 5) (integer) 1 6) (integer) 3 7) (integer) 1 8) (integer) 1 9) (integer) 3 10) (integer) 1
創(chuàng)建排序輔助函數(shù)
上一個小節(jié)說到, 為了防止帶有副作用的函數(shù)令腳本產生不一致的數(shù)據(jù), Redis 對 math 庫的 math.random 函數(shù)和 math.randomseed 函數(shù)進行了替換。
對于Lua 腳本來說, 另一個可能產生不一致數(shù)據(jù)的地方是那些帶有不確定性質的命令。
比如對于一個集合鍵來說, 因為集合元素的排列是無序的, 所以即使兩個集合的元素完全相同, 它們的輸出結果也可能并不相同。
考慮下面這個集合例子:
redis> SADD fruit apple banana cherry (integer) 3 redis> SMEMBERS fruit 1) "cherry" 2) "banana" 3) "apple" redis> SADD another-fruit cherry banana apple (integer) 3 redis> SMEMBERS another-fruit 1) "apple" 2) "banana" 3) "cherry"
這個例子中的 fruit 集合和 another-fruit 集合包含的元素是完全相同的, 只是因為集合添加元素的順序不同, SMEMBERS 命令的輸出就產生了不同的結果。
Redis 將 SMEMBERS 這種在相同數(shù)據(jù)集上可能會產生不同輸出的命令稱為“帶有不確定性的命令”, 這些命令包括:
SINTER SUNION SDIFF SMEMBERS HKEYS HVALS KEYS
為了消除這些命令帶來的不確定性, 服務器會為 Lua 環(huán)境創(chuàng)建一個排序輔助函數(shù) __redis__compare_helper , 當 Lua 腳本執(zhí)行完一個帶有不確定性的命令之后, 程序會使用 __redis__compare_helper 作為對比函數(shù), 自動調用 table.sort 函數(shù)對命令的返回值做一次排序, 以此來保證相同的數(shù)據(jù)集總是產生相同的輸出。
舉個例子, 如果我們在 Lua 腳本中對 fruit 集合和 another-fruit 集合執(zhí)行 SMEMBERS 命令, 那么兩個腳本將得出相同的結果 —— 因為腳本已經對 SMEMBERS 命令的輸出進行過排序了:
redis> EVAL "return redis.call('SMEMBERS', KEYS[1])" 1 fruit 1) "apple" 2) "banana" 3) "cherry" redis> EVAL "return redis.call('SMEMBERS', KEYS[1])" 1 another-fruit 1) "apple" 2) "banana" 3) "cherry
創(chuàng)建 redis.pcall 函數(shù)的錯誤報告輔助函數(shù)
在這一步, 服務器將為 Lua 環(huán)境創(chuàng)建一個名為 __redis__err__handler 的錯誤處理函數(shù), 當腳本調用 redis.pcall 函數(shù)執(zhí)行 Redis 命令, 并且被執(zhí)行的命令出現(xiàn)錯誤時, __redis__err__handler 就會打印出錯代碼的來源和發(fā)生錯誤的行數(shù), 為程序的調試提供方便。
舉個例子, 如果客戶端要求服務器執(zhí)行以下 Lua 腳本:
那么服務器將向客戶端返回一個錯誤:
$ redis-cli --eval wrong-command.lua (error) @user_script: 4: Unknown Redis command called from Lua script
其中@user_script 說明這是一個用戶定義的函數(shù), 而之后的 4 則說明出錯的代碼位于 Lua 腳本的第四行。
保護 Lua 的全局環(huán)境
在這一步, 服務器將對 Lua 環(huán)境中的全局環(huán)境進行保護, 確保傳入服務器的腳本不會因為忘記使用 local 關鍵字而將額外的全局變量添加到了 Lua 環(huán)境里面。
因為全局變量保護的原因, 當一個腳本試圖創(chuàng)建一個全局變量時, 服務器將報告一個錯誤:
redis> EVAL "x = 10" 0 (error) ERR Error running script (call to f_df1ad3745c2d2f078f0f41377a92bb6f8ac79af0): @enable_strict_lua:7: user_script:1: Script attempted to create global variable 'x'
除此之外, 試圖獲取一個不存在的全局變量也會引發(fā)一個錯誤:
redis> EVAL "return x" 0 (error) ERR Error running script (call to f_03c387736bb5cc009ff35151572cee04677aa374): @enable_strict_lua:14: user_script:1: Script attempted to access unexisting global variable 'x'
不過Redis 并未禁止用戶修改已存在的全局變量, 所以在執(zhí)行 Lua 腳本的時候, 必須非常小心, 以免錯誤地修改了已存在的全局變量:
redis> EVAL "redis = 10086; return redis" 0 (integer) 10086
將 Lua 環(huán)境保存到服務器狀態(tài)的 lua 屬性里面
經過以上的一系列修改, Redis 服務器對 Lua 環(huán)境的修改工作到此就結束了, 在最后的這一步, 服務器會將 Lua 環(huán)境和服務器狀態(tài)的 lua屬性關聯(lián)起來,因為Redis 使用串行化的方式來執(zhí)行 Redis 命令, 所以在任何特定時間里, 最多都只會有一個腳本能夠被放進 Lua 環(huán)境里面運行, 因此, 整個 Redis 服務器只需要創(chuàng)建一個 Lua 環(huán)境即可。
總結
Redis 服務器在啟動時, 會對內嵌的 Lua 環(huán)境執(zhí)行一系列修改操作, 從而確保內嵌的 Lua 環(huán)境可以滿足 Redis 在功能性、安全性等方面的需要。
Redis 服務器專門使用一個偽客戶端來執(zhí)行 Lua 腳本中包含的 Redis 命令。
Redis 使用腳本字典來保存所有被 EVAL 命令執(zhí)行過, 或者被 SCRIPT_LOAD 命令載入過的 Lua 腳本, 這些腳本可以用于實現(xiàn)SCRIPT_EXISTS 命令, 以及實現(xiàn)腳本復制功能。
EVAL 命令為客戶端輸入的腳本在 Lua 環(huán)境中定義一個函數(shù), 并通過調用這個函數(shù)來執(zhí)行腳本。
EVALSHA 命令通過直接調用 Lua 環(huán)境中已定義的函數(shù)來執(zhí)行腳本。
SCRIPT_FLUSH 命令會清空服務器 lua_scripts 字典中保存的腳本, 并重置 Lua 環(huán)境。
SCRIPT_EXISTS 命令接受一個或多個 SHA1 校驗和為參數(shù), 并通過檢查 lua_scripts 字典來確認校驗和對應的腳本是否存在。
SCRIPT_LOAD 命令接受一個 Lua 腳本為參數(shù), 為該腳本在 Lua 環(huán)境中創(chuàng)建函數(shù), 并將腳本保存到 lua_scripts 字典中。
服務器在執(zhí)行腳本之前, 會為 Lua 環(huán)境設置一個超時處理鉤子, 當腳本出現(xiàn)超時運行情況時, 客戶端可以通過向服務器發(fā)送SCRIPT_KILL 命令來讓鉤子停止正在執(zhí)行的腳本, 或者發(fā)送 SHUTDOWN nosave 命令來讓鉤子關閉整個服務器。
主服務器復制 EVAL 、 SCRIPT_FLUSH 、 SCRIPT_LOAD 三個命令的方法和復制普通 Redis 命令一樣 —— 只要將相同的命令傳播給從服務器就可以了。
主服務器在復制 EVALSHA 命令時, 必須確保所有從服務器都已經載入了 EVALSHA 命令指定的 SHA1 校驗和所對應的 Lua 腳本, 如果不能確保這一點的話, 主服務器會將 EVALSHA 命令轉換成等效的 EVAL 命令, 并通過傳播 EVAL 命令來獲得相同的腳本執(zhí)行效果
到此這篇關于Redis創(chuàng)建并修改Lua 環(huán)境的實現(xiàn)方法的文章就介紹到這了,更多相關Redis創(chuàng)建并修改Lua 環(huán)境內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于Redis實現(xiàn)分布式鎖的方法(lua腳本版)
這篇文章主要介紹了基于Redis實現(xiàn)分布式鎖的方法(lua腳本版),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-05-05Redisson實現(xiàn)分布式鎖、鎖續(xù)約的案例
這篇文章主要介紹了Redisson如何實現(xiàn)分布式鎖、鎖續(xù)約,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03