---
# System prepended metadata

title: Ch11. Scripting Redis with Lua
tags: [Redis]

---

---
tags: Redis
---

# Ch11. Scripting Redis with Lua
── Redis 的 LUA 腳本編程

- Overview
    - 不寫 C 的情形下添加新功能
    - 使用 Lua 重寫鎖和信號量 (ch6)
    - 移除 WATCH/ MULTI/ EXEC 事務 (ch4, ch6)
    - 使用 Lua 對列表進行分片 (ch9, ch10)

## Lua
- 輕量腳本語言、用 C 語言編寫。
- 可嵌入應用程式中，為應用程式提供擴展性 & 訂製功能。
- 應用
    - 遊戲開發：將 Lua 腳本嵌入其他程序中運行，遊戲升級時可直接升級腳本，不用重新安裝遊戲。
    - 資料庫插件：Redis 本身有很多命令，但在某些特定領域需要擴充若干指令時，用戶可向 server 發送 Lua 腳本執行自定義動作。
        - 減少網路延遲：多個請求透過一個腳本一次性執行。
        - 原子操作：整個腳本一次性執行，過程中無競態條件 (無需使用事務)。
        - 重複使用：客戶端發送的腳本永久存在 Redis，其他客戶端可使用。

## 11.1 不寫 C 的情形下添加新功能
- 避免在商業環境中修改 Redis 源代碼，可使用 Lua 對 Redis 進行腳本編程。
### 11.1.1 將 Lua 腳本載入 Redis
- Redis 指令
    - `SCRIPT LOAD`
        - 輸入 = 字符串格式的 Lua 腳本
        - 輸出 = 被儲存腳本的 SHA1 hash
        - 目標：腳本會被先儲存起來
    - `EVALSHA`
        - 輸入 = 腳本的 SHA1 hash & 腳本所需所有參數
        - 目標：調用之前儲存的腳本
    - `EVAL`
        - 輸入 = 腳本字串符 & 腳本所需所有參數
        - 目標：直接執行指定的腳本，並將執行的腳本緩存到 Redis Server
    - KEYS AND ARGUMENTS TO LUA SCRIPTS:
        - Redis 連線
        - a list of keys：當使用 ch10 提到的 multiserver sharding 技術時，會需要驗證傳入的 key 值都在同一個 shard 上。
        - a list of arguments to the function：保存要在 Redis 上調用的資料。
- 由於 Lua 的資料輸入輸出限制，Lua 和 Redis 需要進行互相轉換。
![](https://i.imgur.com/dLIiygG.png)
※ 由於腳本回傳多種不同資料型態的結果可能 ambiguous，所以盡可能回傳 strings。

### 11.1.2 建立新的狀態消息 (status message)
- 案例：status message 的發送功能 (ch8)
    - Lua 腳本和單一 Redis 命令以及 `MULTI`/`EXEC` 一樣是「原子操作 (atomic)」
→ 執行過程不會被其他命令干擾
    - 如果已經對結構 (structure) 修改的 Lua 腳本就無法再被中斷，但如果撰寫的 Lua 腳本 never return，則其他客戶端就無法正常執行命令，這時候可使用 Redis 方法停止運行的腳本：
        - 只有「read」的腳本：運行超過 `lua-time-limit`，就執行`SCRIPT KILL` 終止腳本
        - 包含「write」的腳本：為避免終止導致 Redis 不一致，可使用 `SHUTDOWN NOSAVE`，但會遺失自上一次快照後的所有改動
    


## 11.2 使用 Lua 重寫鎖和信號量
- 為何使用 Lua 重寫？
    1. 執行 Lua 腳本指令的參數有包含需讀/ 寫的 key 值 (11.1 提到的 a list of keys)，因此可以在 Redis cluster 啟動時，去驗證是否可在指定分片上對鍵值讀寫。*
    2. 有多次讀取要被快取的資料可能會帶來額外消耗，甚至導致新資料被舊資料覆蓋。
- Lua 腳本限制
    - 如果事先不知道哪個 key 要被讀寫，就不要用 Lua 腳本，以避免腳本對未被記錄在 `KEYS` 中的參數做讀寫，導致在程序在遷移至 Redis clusters 時不相容。*
- 改寫案例 (ch6)
    - 取得鎖：直接使用 `SET`
    - 釋放鎖：可使用 Lua 腳本實現釋放自己取得的鎖。(核心程式從 Python 遷移到 Lua，可參考 ch6)

## 11.3 移除 WATCH/ MULTI/ EXEC 事務
- 為何使用 Lua 重寫？
    1. 當多個客戶端要對被 `WATCH` 監視的資料進行修改，因為要進行數次通信往返、衝突發生機率較高、網路延遲較大，客戶端可能要多次重士才能完成操作。
    → Lua 腳本保證客戶端嘗試的執行都成功、無通信往返，所以執行速度更快。
- Lua 腳本限制
    - 運行在 Redis 內部的 Lua 腳本只能訪問 Lua 腳本內，或是 Redis 之內的資料。
- 改寫案例 (ch4, ch6)

## 11.4 使用 Lua 對列表進行分片
- 分片列表的構成
    - `<listname>:first` 和 `<listname>:last` 儲存首尾分片 ID，當兩個 ID 相同代表分片列表為空。
    - 分片列表中每個分片的命名為 `<listname>:<shardid>`，並按照順序進行分配。
    - 頭尾分片可能是空的，但中間一定是被填滿的，
- 將元素推入分片列表
    - Lua 腳本依據 `LPUSH`/`RPUSH` 找出首/尾分片。
    - 將元素 push 進分片對應的列表中。假設該分片列表已經塞滿，就會自動產新一個新的分片繼續將元素推入 & 更新首/尾分片 ID。
    - 最終 return 被推入的元素數量。 
- 從分片列表中彈出元素
    - Lua 腳本依據 `LPOP`/`RPOP` 找出首/尾分片。
    - 當分片非空就 pop 出元素。假設該分片列表已經為空，但整個列表不是都空，就要更新首/尾分片 ID。
- 對分片列表執行阻塞彈出操作
    - 避免產生不正確的資料。




## 參考資源

- Redis中使用Lua脚本（一）
https://zhuanlan.zhihu.com/p/77484377