為了在 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è)過程由以下步驟組成:
redis
, 這個(gè)表格包含了對 Redis 進(jìn)行操作的函數(shù), 比如用于在 Lua 腳本中執(zhí)行 Redis 命令的 redis.call
函數(shù)。redis.pcall
函數(shù)的錯(cuò)誤報(bào)告輔助函數(shù), 這個(gè)函數(shù)可以提供更詳細(xì)的出錯(cuò)信息。lua
屬性里面, 等待執(zhí)行服務(wù)器傳來的 Lua 腳本。接下來的各個(gè)小節(jié)將分別介紹這些步驟。
在最開始的這一步, 服務(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)行一系列修改。
Redis 修改 Lua 環(huán)境的第一步, 就是將以下函數(shù)庫載入到 Lua 環(huán)境里面:
assert
、 error
、 pairs
、 tostring
、 pcall
, 等等。 另外, 為了防止用戶從外部文件中引入不安全的代碼, 庫中的 loadfile
函數(shù)會(huì)被刪除。table.concat
、 table.insert
、 table.remove
、 table.sort
, 等等。string.find
函數(shù), 對字符串進(jìn)行格式化的 string.format
函數(shù), 查看字符串長度的 string.len
函數(shù), 對字符串進(jìn)行翻轉(zhuǎn)的 string.reverse
函數(shù), 等等。math.abs
函數(shù), 返回多個(gè)數(shù)中的最大值和最小值的 math.max
函數(shù)和 math.min
函數(shù), 計(jì)算二次方根的 math.sqrt
函數(shù), 計(jì)算對數(shù)的 math.log
函數(shù), 等等。debug.sethook
函數(shù)和debug.gethook
函數(shù), 返回給定函數(shù)相關(guān)信息的 debug.getinfo
函數(shù), 為對象設(shè)置元數(shù)據(jù)的 debug.setmetatable
函數(shù), 獲取對象元數(shù)據(jù)的debug.getmetatable
函數(shù), 等等。cjson.decode
函數(shù)將一個(gè) JSON 格式的字符串轉(zhuǎn)換為一個(gè) Lua 值, 而 cjson.encode
函數(shù)將一個(gè) Lua 值序列化為 JSON 格式的字符串。struct.pack
將多個(gè) Lua 值打包成一個(gè)類結(jié)構(gòu)(struct-like)字符串, 而函數(shù) struct.unpack
則從一個(gè)類結(jié)構(gòu)字符串中解包出多個(gè) Lua 值。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ù)雜的操作。
redis
全局表格在這一步, 服務(wù)器將在 Lua 環(huán)境中創(chuàng)建一個(gè) redis
表格(table), 并將它設(shè)為全局變量。
這個(gè) redis
表格包含以下函數(shù):
redis.call
和 redis.pcall
函數(shù)。redis.log
函數(shù), 以及相應(yīng)的日志級別(level)常量: redis.LOG_DEBUG
, redis.LOG_VERBOSE
,redis.LOG_NOTICE
, 以及 redis.LOG_WARNING
。redis.sha1hex
函數(shù)。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
為了保證相同的腳本可以在不同的機(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ù)有以下特征:
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
上一個(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)生不同輸出的命令稱為“帶有不確定性的命令”, 這些命令包括:
為了消除這些命令帶來的不確定性, 服務(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"
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 腳本的第四行。
在這一步, 服務(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
屬性里面經(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)境即可。
更多建議: