為了在 Redis 服務器中執行 Lua 腳本, Redis 在服務器內嵌了一個 Lua 環境(environment), 并對這個 Lua 環境進行了一系列修改, 從而確保這個 Lua 環境可以滿足 Redis 服務器的需要。
Redis 服務器創建并修改 Lua 環境的整個過程由以下步驟組成:
1. 創建一個基礎的 Lua 環境, 之后的所有修改都是針對這個環境進行的。
2. 載入多個函數庫到 Lua 環境里面, 讓 Lua 腳本可以使用這些函數庫來進行數據操作。
3. 創建全局表格?`redis`?, 這個表格包含了對 Redis 進行操作的函數, 比如用于在 Lua 腳本中執行 Redis 命令的?`redis.call`?函數。
4. 使用 Redis 自制的隨機函數來替換 Lua 原有的帶有副作用的隨機函數, 從而避免在腳本中引入副作用。
5. 創建排序輔助函數, Lua 環境使用這個輔佐函數來對一部分 Redis 命令的結果進行排序, 從而消除這些命令的不確定性。
6. 創建?`redis.pcall`?函數的錯誤報告輔助函數, 這個函數可以提供更詳細的出錯信息。
7. 對 Lua 環境里面的全局環境進行保護, 防止用戶在執行 Lua 腳本的過程中, 將額外的全局變量添加到了 Lua 環境里面。
8. 將完成修改的 Lua 環境保存到服務器狀態的?`lua`?屬性里面, 等待執行服務器傳來的 Lua 腳本。
接下來的各個小節將分別介紹這些步驟。
## 創建 Lua 環境
在最開始的這一步, 服務器首先調用 Lua 的 C API 函數?`lua_open`?, 創建一個新的 Lua 環境。
因為 lua_open 函數創建的只是一個基本的 Lua 環境, 為了讓這個 Lua 環境可以滿足 Redis 的操作要求, 接下來服務器將對這個 Lua 環境進行一系列修改。
## 載入函數庫
Redis 修改 Lua 環境的第一步, 就是將以下函數庫載入到 Lua 環境里面:
* 基礎庫(base library): 這個庫包含 Lua 的核心(core)函數, 比如?`assert`?、?`error`?、?`pairs`?、?`tostring`?、?`pcall`?, 等等。 另外, 為了防止用戶從外部文件中引入不安全的代碼, 庫中的?`loadfile`?函數會被刪除。
* 表格庫(table library): 這個庫包含用于處理表格的通用函數, 比如?`table.concat`?、?`table.insert`?、?`table.remove`?、?`table.sort`, 等等。
* 字符串庫(string library): 這個庫包含用于處理字符串的通用函數, 比如用于對字符串進行查找的?`string.find`?函數, 對字符串進行格式化的?`string.format`?函數, 查看字符串長度的?`string.len`?函數, 對字符串進行翻轉的?`string.reverse`?函數, 等等。
* 數學庫(math library): 這個庫是標準 C 語言數學庫的接口, 它包括計算絕對值的?`math.abs`?函數, 返回多個數中的最大值和最小值的?`math.max`?函數和?`math.min`?函數, 計算二次方根的?`math.sqrt`?函數, 計算對數的?`math.log`?函數, 等等。
* 調試庫(debug library): 這個庫提供了對程序進行調試所需的函數, 比如對程序設置鉤子和取得鉤子的?`debug.sethook`?函數和`debug.gethook`?函數, 返回給定函數相關信息的?`debug.getinfo`?函數, 為對象設置元數據的?`debug.setmetatable`?函數, 獲取對象元數據的`debug.getmetatable`?函數, 等等。
* Lua CJSON 庫([http://www.kyne.com.au/~mark/software/lua-cjson.php](http://www.kyne.com.au/~mark/software/lua-cjson.php)): 這個庫用于處理 UTF-8 編碼的 JSON 格式, 其中`cjson.decode`?函數將一個 JSON 格式的字符串轉換為一個 Lua 值, 而?`cjson.encode`?函數將一個 Lua 值序列化為 JSON 格式的字符串。
* Struct 庫([http://www.inf.puc-rio.br/~roberto/struct/](http://www.inf.puc-rio.br/~roberto/struct/)): 這個庫用于在 Lua 值和 C 結構(struct)之間進行轉換, 函數`struct.pack`?將多個 Lua 值打包成一個類結構(struct-like)字符串, 而函數?`struct.unpack`?則從一個類結構字符串中解包出多個 Lua 值。
* Lua cmsgpack 庫([https://github.com/antirez/lua-cmsgpack](https://github.com/antirez/lua-cmsgpack)): 這個庫用于處理 MessagePack 格式的數據, 其中?`cmsgpack.pack`?函數將 Lua 值轉換為 MessagePack 數據, 而?`cmsgpack.unpack`?函數則將 MessagePack 數據轉換為 Lua 值。
通過使用這些功能強大的函數庫, Lua 腳本可以直接對執行 Redis 命令獲得的數據進行復雜的操作。
## 創建?`redis`?全局表格
在這一步, 服務器將在 Lua 環境中創建一個?`redis`?表格(table), 并將它設為全局變量。
這個?`redis`?表格包含以下函數:
* 用于執行 Redis 命令的?`redis.call`?和?`redis.pcall`?函數。
* 用于記錄 Redis 日志(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`?函數。
在這些函數里面, 最常用也最重要的要數?`redis.call`?函數和?`redis.pcall`?函數 —— 通過這兩個函數, 用戶可以直接在 Lua 腳本中執行 Redis 命令:
~~~
redis> EVAL "return redis.call('PING')" 0
PONG
~~~
## 使用 Redis 自制的隨機函數來替換 Lua 原有的隨機函數
為了保證相同的腳本可以在不同的機器上產生相同的結果, Redis 要求所有傳入服務器的 Lua 腳本, 以及 Lua 環境中的所有函數, 都必須是無副作用([side effect](http://en.wikipedia.org/wiki/Side_effect_(computer_science)))的純函數([pure function](http://en.wikipedia.org/wiki/Pure_function))。
但是, 在之前載入到 Lua 環境的?`math`?函數庫中, 用于生成隨機數的?`math.random`?函數和?`math.randomseed`?函數都是帶有副作用的, 它們不符合 Redis 對 Lua 環境的無副作用要求。
因為這個原因, Redis 使用自制的函數替換了?`math`?庫中原有的?`math.random`?函數和?`math.randomseed`?函數, 替換之后的兩個函數有以下特征:
* 對于相同的 seed 來說,?`math.random`?總產生相同的隨機數序列, 這個函數是一個純函數。
* 除非在腳本中使用?`math.randomseed`?顯式地修改 seed , 否則每次運行腳本時, Lua 環境都使用固定的?`math.randomseed(0)`?語句來初始化 seed 。
比如說, 使用以下腳本, 我們可以打印 seed 值為?`0`?時,?`math.random`?對于輸入?`10`?至?`1`?所產生的隨機序列:
無論執行這個腳本多少次, 產生的值都是相同的:
~~~
$ 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`?:
那么這個腳本生成的隨機數序列將和使用默認 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
~~~
## 創建排序輔助函數
上一個小節說到, 為了防止帶有副作用的函數令腳本產生不一致的數據, Redis 對?`math`?庫的?`math.random`?函數和?`math.randomseed`?函數進行了替換。
對于 Lua 腳本來說, 另一個可能產生不一致數據的地方是那些帶有不確定性質的命令。
比如對于一個集合鍵來說, 因為集合元素的排列是無序的, 所以即使兩個集合的元素完全相同, 它們的輸出結果也可能并不相同。
考慮下面這個集合例子:
~~~
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?這種在相同數據集上可能會產生不同輸出的命令稱為“帶有不確定性的命令”, 這些命令包括:
* SINTER
* SUNION
* SDIFF
* SMEMBERS
* HKEYS
* HVALS
* KEYS
為了消除這些命令帶來的不確定性, 服務器會為 Lua 環境創建一個排序輔助函數?`__redis__compare_helper`?, 當 Lua 腳本執行完一個帶有不確定性的命令之后, 程序會使用?`__redis__compare_helper`?作為對比函數, 自動調用?`table.sort`?函數對命令的返回值做一次排序, 以此來保證相同的數據集總是產生相同的輸出。
舉個例子, 如果我們在 Lua 腳本中對?`fruit`?集合和?`another-fruit`?集合執行?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"
~~~
## 創建?`redis.pcall`?函數的錯誤報告輔助函數
在這一步, 服務器將為 Lua 環境創建一個名為?`__redis__err__handler`?的錯誤處理函數, 當腳本調用?`redis.pcall`?函數執行 Redis 命令, 并且被執行的命令出現錯誤時,?`__redis__err__handler`?就會打印出錯代碼的來源和發生錯誤的行數, 為程序的調試提供方便。
舉個例子, 如果客戶端要求服務器執行以下 Lua 腳本:
那么服務器將向客戶端返回一個錯誤:
~~~
$ redis-cli --eval wrong-command.lua
(error) @user_script: 4: Unknown Redis command called from Lua script
~~~
其中?`@user_script`?說明這是一個用戶定義的函數, 而之后的?`4`?則說明出錯的代碼位于 Lua 腳本的第四行。
## 保護 Lua 的全局環境
在這一步, 服務器將對 Lua 環境中的全局環境進行保護, 確保傳入服務器的腳本不會因為忘記使用?`local`?關鍵字而將額外的全局變量添加到了 Lua 環境里面。
因為全局變量保護的原因, 當一個腳本試圖創建一個全局變量時, 服務器將報告一個錯誤:
~~~
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'
~~~
除此之外, 試圖獲取一個不存在的全局變量也會引發一個錯誤:
~~~
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 并未禁止用戶修改已存在的全局變量, 所以在執行 Lua 腳本的時候, 必須非常小心, 以免錯誤地修改了已存在的全局變量:
~~~
redis> EVAL "redis = 10086; return redis" 0
(integer) 10086
~~~
## 將 Lua 環境保存到服務器狀態的?`lua`?屬性里面
經過以上的一系列修改, Redis 服務器對 Lua 環境的修改工作到此就結束了, 在最后的這一步, 服務器會將 Lua 環境和服務器狀態的?`lua`屬性關聯起來, 如圖 IMAGE_REDIS_SERVER_LUA 所示。

因為 Redis 使用串行化的方式來執行 Redis 命令, 所以在任何特定時間里, 最多都只會有一個腳本能夠被放進 Lua 環境里面運行, 因此, 整個 Redis 服務器只需要創建一個 Lua 環境即可。
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤