Redis 創(chuàng)建并修改 Lua 環(huán)境

2018-08-02 14:57 更新

為了在 Redis 服務(wù)器中執(zhí)行 Lua 腳本, Redis 在服務(wù)器內(nèi)嵌了一個(gè) Lua 環(huán)境(environment), 并對這個(gè) Lua 環(huán)境進(jìn)行了一系列修改, 從而確保這個(gè) Lua 環(huán)境可以滿足 Redis 服務(wù)器的需要。

Redis 服務(wù)器創(chuàng)建并修改 Lua 環(huán)境的整個(gè)過程由以下步驟組成:

  1. 創(chuàng)建一個(gè)基礎(chǔ)的 Lua 環(huán)境, 之后的所有修改都是針對這個(gè)環(huán)境進(jìn)行的。
  2. 載入多個(gè)函數(shù)庫到 Lua 環(huán)境里面, 讓 Lua 腳本可以使用這些函數(shù)庫來進(jìn)行數(shù)據(jù)操作。
  3. 創(chuàng)建全局表格 redis , 這個(gè)表格包含了對 Redis 進(jìn)行操作的函數(shù), 比如用于在 Lua 腳本中執(zhí)行 Redis 命令的 redis.call 函數(shù)。
  4. 使用 Redis 自制的隨機(jī)函數(shù)來替換 Lua 原有的帶有副作用的隨機(jī)函數(shù), 從而避免在腳本中引入副作用。
  5. 創(chuàng)建排序輔助函數(shù), Lua 環(huán)境使用這個(gè)輔佐函數(shù)來對一部分 Redis 命令的結(jié)果進(jìn)行排序, 從而消除這些命令的不確定性。
  6. 創(chuàng)建 redis.pcall 函數(shù)的錯(cuò)誤報(bào)告輔助函數(shù), 這個(gè)函數(shù)可以提供更詳細(xì)的出錯(cuò)信息。
  7. 對 Lua 環(huán)境里面的全局環(huán)境進(jìn)行保護(hù), 防止用戶在執(zhí)行 Lua 腳本的過程中, 將額外的全局變量添加到了 Lua 環(huán)境里面。
  8. 將完成修改的 Lua 環(huán)境保存到服務(wù)器狀態(tài)的 lua 屬性里面, 等待執(zhí)行服務(wù)器傳來的 Lua 腳本。

接下來的各個(gè)小節(jié)將分別介紹這些步驟。

創(chuàng)建 Lua 環(huán)境

在最開始的這一步, 服務(wù)器首先調(diào)用 Lua 的 C API 函數(shù) lua_open , 創(chuàng)建一個(gè)新的 Lua 環(huán)境。

因?yàn)?lua_open 函數(shù)創(chuàng)建的只是一個(gè)基本的 Lua 環(huán)境, 為了讓這個(gè) Lua 環(huán)境可以滿足 Redis 的操作要求, 接下來服務(wù)器將對這個(gè) Lua 環(huán)境進(jìn)行一系列修改。

載入函數(shù)庫

Redis 修改 Lua 環(huán)境的第一步, 就是將以下函數(shù)庫載入到 Lua 環(huán)境里面:

  • 基礎(chǔ)庫(base library): 這個(gè)庫包含 Lua 的核心(core)函數(shù), 比如 assert 、 error 、 pairs 、 tostring 、 pcall , 等等。 另外, 為了防止用戶從外部文件中引入不安全的代碼, 庫中的 loadfile 函數(shù)會(huì)被刪除。
  • 表格庫(table library): 這個(gè)庫包含用于處理表格的通用函數(shù), 比如 table.concat 、 table.insert 、 table.remove 、 table.sort, 等等。
  • 字符串庫(string library): 這個(gè)庫包含用于處理字符串的通用函數(shù), 比如用于對字符串進(jìn)行查找的 string.find 函數(shù), 對字符串進(jìn)行格式化的 string.format 函數(shù), 查看字符串長度的 string.len 函數(shù), 對字符串進(jìn)行翻轉(zhuǎn)的 string.reverse 函數(shù), 等等。
  • 數(shù)學(xué)庫(math library): 這個(gè)庫是標(biāo)準(zhǔn) C 語言數(shù)學(xué)庫的接口, 它包括計(jì)算絕對值的 math.abs 函數(shù), 返回多個(gè)數(shù)中的最大值和最小值的 math.max 函數(shù)和 math.min 函數(shù), 計(jì)算二次方根的 math.sqrt 函數(shù), 計(jì)算對數(shù)的 math.log 函數(shù), 等等。
  • 調(diào)試庫(debug library): 這個(gè)庫提供了對程序進(jìn)行調(diào)試所需的函數(shù), 比如對程序設(shè)置鉤子和取得鉤子的 debug.sethook 函數(shù)和debug.gethook 函數(shù), 返回給定函數(shù)相關(guān)信息的 debug.getinfo 函數(shù), 為對象設(shè)置元數(shù)據(jù)的 debug.setmetatable 函數(shù), 獲取對象元數(shù)據(jù)的debug.getmetatable 函數(shù), 等等。
  • Lua CJSON 庫(http://www.kyne.com.au/~mark/software/lua-cjson.php): 這個(gè)庫用于處理 UTF-8 編碼的 JSON 格式, 其中cjson.decode 函數(shù)將一個(gè) JSON 格式的字符串轉(zhuǎn)換為一個(gè) Lua 值, 而 cjson.encode 函數(shù)將一個(gè) Lua 值序列化為 JSON 格式的字符串。
  • Struct 庫(http://www.inf.puc-rio.br/~roberto/struct/): 這個(gè)庫用于在 Lua 值和 C 結(jié)構(gòu)(struct)之間進(jìn)行轉(zhuǎn)換, 函數(shù)struct.pack 將多個(gè) Lua 值打包成一個(gè)類結(jié)構(gòu)(struct-like)字符串, 而函數(shù) struct.unpack 則從一個(gè)類結(jié)構(gòu)字符串中解包出多個(gè) Lua 值。
  • Lua cmsgpack 庫(https://github.com/antirez/lua-cmsgpack): 這個(gè)庫用于處理 MessagePack 格式的數(shù)據(jù), 其中 cmsgpack.pack 函數(shù)將 Lua 值轉(zhuǎn)換為 MessagePack 數(shù)據(jù), 而 cmsgpack.unpack 函數(shù)則將 MessagePack 數(shù)據(jù)轉(zhuǎn)換為 Lua 值。

通過使用這些功能強(qiáng)大的函數(shù)庫, Lua 腳本可以直接對執(zhí)行 Redis 命令獲得的數(shù)據(jù)進(jìn)行復(fù)雜的操作。

創(chuàng)建 redis 全局表格

在這一步, 服務(wù)器將在 Lua 環(huán)境中創(chuàng)建一個(gè) redis 表格(table), 并將它設(shè)為全局變量。

這個(gè) redis 表格包含以下函數(shù):

  • 用于執(zhí)行 Redis 命令的 redis.call 和 redis.pcall 函數(shù)。
  • 用于記錄 Redis 日志(log)的 redis.log 函數(shù), 以及相應(yīng)的日志級別(level)常量: redis.LOG_DEBUG , redis.LOG_VERBOSE ,redis.LOG_NOTICE , 以及 redis.LOG_WARNING 。
  • 用于計(jì)算 SHA1 校驗(yàn)和的 redis.sha1hex 函數(shù)。
  • 用于返回錯(cuò)誤信息的 redis.error_reply 函數(shù)和 redis.status_reply 函數(shù)。

在這些函數(shù)里面, 最常用也最重要的要數(shù) redis.call 函數(shù)和 redis.pcall 函數(shù) —— 通過這兩個(gè)函數(shù), 用戶可以直接在 Lua 腳本中執(zhí)行 Redis 命令:

redis> EVAL "return redis.call('PING')" 0
PONG

使用 Redis 自制的隨機(jī)函數(shù)來替換 Lua 原有的隨機(jī)函數(shù)

為了保證相同的腳本可以在不同的機(jī)器上產(chǎn)生相同的結(jié)果, Redis 要求所有傳入服務(wù)器的 Lua 腳本, 以及 Lua 環(huán)境中的所有函數(shù), 都必須是無副作用(side effect)的純函數(shù)(pure function)。

但是, 在之前載入到 Lua 環(huán)境的 math 函數(shù)庫中, 用于生成隨機(jī)數(shù)的 math.random 函數(shù)和 math.randomseed 函數(shù)都是帶有副作用的, 它們不符合 Redis 對 Lua 環(huán)境的無副作用要求。

因?yàn)檫@個(gè)原因, Redis 使用自制的函數(shù)替換了 math 庫中原有的 math.random 函數(shù)和 math.randomseed 函數(shù), 替換之后的兩個(gè)函數(shù)有以下特征:

  • 對于相同的 seed 來說, math.random 總產(chǎn)生相同的隨機(jī)數(shù)序列, 這個(gè)函數(shù)是一個(gè)純函數(shù)。
  • 除非在腳本中使用 math.randomseed 顯式地修改 seed , 否則每次運(yùn)行腳本時(shí), Lua 環(huán)境都使用固定的 math.randomseed(0) 語句來初始化 seed 。

比如說, 使用以下腳本, 我們可以打印 seed 值為 0 時(shí), math.random 對于輸入 10 至 1 所產(chǎn)生的隨機(jī)序列:

無論執(zhí)行這個(gè)腳本多少次, 產(chǎn)生的值都是相同的:

$ 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

但是, 如果我們在另一個(gè)腳本里面, 調(diào)用 math.randomseed 將 seed 修改為 10086 :

那么這個(gè)腳本生成的隨機(jī)數(shù)序列將和使用默認(rèn) seed 值 0 時(shí)生成的隨機(jī)序列不同:

$ 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ù)

上一個(gè)小節(jié)說到, 為了防止帶有副作用的函數(shù)令腳本產(chǎn)生不一致的數(shù)據(jù), Redis 對 math 庫的 math.random 函數(shù)和 math.randomseed 函數(shù)進(jìn)行了替換。

對于 Lua 腳本來說, 另一個(gè)可能產(chǎn)生不一致數(shù)據(jù)的地方是那些帶有不確定性質(zhì)的命令。

比如對于一個(gè)集合鍵來說, 因?yàn)榧显氐呐帕惺菬o序的, 所以即使兩個(gè)集合的元素完全相同, 它們的輸出結(jié)果也可能并不相同。

考慮下面這個(gè)集合例子:

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"

這個(gè)例子中的 fruit 集合和 another-fruit 集合包含的元素是完全相同的, 只是因?yàn)榧咸砑釉氐捻樞虿煌?nbsp;SMEMBERS 命令的輸出就產(chǎn)生了不同的結(jié)果。

Redis 將 SMEMBERS 這種在相同數(shù)據(jù)集上可能會(huì)產(chǎn)生不同輸出的命令稱為“帶有不確定性的命令”, 這些命令包括:

  • SINTER
  • SUNION
  • SDIFF
  • SMEMBERS
  • HKEYS
  • HVALS
  • KEYS

為了消除這些命令帶來的不確定性, 服務(wù)器會(huì)為 Lua 環(huán)境創(chuàng)建一個(gè)排序輔助函數(shù) __redis__compare_helper , 當(dāng) Lua 腳本執(zhí)行完一個(gè)帶有不確定性的命令之后, 程序會(huì)使用 __redis__compare_helper 作為對比函數(shù), 自動(dòng)調(diào)用 table.sort 函數(shù)對命令的返回值做一次排序, 以此來保證相同的數(shù)據(jù)集總是產(chǎn)生相同的輸出。

舉個(gè)例子, 如果我們在 Lua 腳本中對 fruit 集合和 another-fruit 集合執(zhí)行 SMEMBERS 命令, 那么兩個(gè)腳本將得出相同的結(jié)果 —— 因?yàn)槟_本已經(jīng)對 SMEMBERS 命令的輸出進(jìn)行過排序了:

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ù)的錯(cuò)誤報(bào)告輔助函數(shù)

在這一步, 服務(wù)器將為 Lua 環(huán)境創(chuàng)建一個(gè)名為 __redis__err__handler 的錯(cuò)誤處理函數(shù), 當(dāng)腳本調(diào)用 redis.pcall 函數(shù)執(zhí)行 Redis 命令, 并且被執(zhí)行的命令出現(xiàn)錯(cuò)誤時(shí), __redis__err__handler 就會(huì)打印出錯(cuò)代碼的來源和發(fā)生錯(cuò)誤的行數(shù), 為程序的調(diào)試提供方便。

舉個(gè)例子, 如果客戶端要求服務(wù)器執(zhí)行以下 Lua 腳本:

那么服務(wù)器將向客戶端返回一個(gè)錯(cuò)誤:

$ redis-cli --eval wrong-command.lua
(error) @user_script: 4: Unknown Redis command called from Lua script

其中 @user_script 說明這是一個(gè)用戶定義的函數(shù), 而之后的 4 則說明出錯(cuò)的代碼位于 Lua 腳本的第四行。

保護(hù) Lua 的全局環(huán)境

在這一步, 服務(wù)器將對 Lua 環(huán)境中的全局環(huán)境進(jìn)行保護(hù), 確保傳入服務(wù)器的腳本不會(huì)因?yàn)橥浭褂?nbsp;local 關(guān)鍵字而將額外的全局變量添加到了 Lua 環(huán)境里面。

因?yàn)槿肿兞勘Wo(hù)的原因, 當(dāng)一個(gè)腳本試圖創(chuàng)建一個(gè)全局變量時(shí), 服務(wù)器將報(bào)告一個(gè)錯(cuò)誤:

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'

除此之外, 試圖獲取一個(gè)不存在的全局變量也會(huì)引發(fā)一個(gè)錯(cuò)誤:

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 腳本的時(shí)候, 必須非常小心, 以免錯(cuò)誤地修改了已存在的全局變量:

redis> EVAL "redis = 10086; return redis" 0
(integer) 10086

將 Lua 環(huán)境保存到服務(wù)器狀態(tài)的 lua 屬性里面

經(jīng)過以上的一系列修改, Redis 服務(wù)器對 Lua 環(huán)境的修改工作到此就結(jié)束了, 在最后的這一步, 服務(wù)器會(huì)將 Lua 環(huán)境和服務(wù)器狀態(tài)的 lua屬性關(guān)聯(lián)起來, 如圖 IMAGE_REDIS_SERVER_LUA 所示。

因?yàn)?Redis 使用串行化的方式來執(zhí)行 Redis 命令, 所以在任何特定時(shí)間里, 最多都只會(huì)有一個(gè)腳本能夠被放進(jìn) Lua 環(huán)境里面運(yùn)行, 因此, 整個(gè) Redis 服務(wù)器只需要?jiǎng)?chuàng)建一個(gè) Lua 環(huán)境即可。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號