一、背景與動機
傳統的 Redis 腳本機制依賴于客戶端加載 EVAL 腳本,存在以下局限:
- 網絡與編譯開銷
每次調用都要傳輸腳本源碼或重新加載 SHA1。 - 緩存失效風險
重啟、主從切換、SCRIPT FLUSH 后腳本緩存丟失,事務易失敗。 - 調試與運維困難
SHA1 不可讀、難以在 MONITOR 或日志中追蹤。 - 代碼復用受限
腳本之間無法互相調用,只能由客戶端拼接多份代碼。
Redis Functions 應運而生,將腳本納入 Redis 數據模型,以「聲明—持久化—調用」的方式,提供了更高效、更可靠、更易運維的服務器端腳本化能力。
二、Redis Functions 概覽
-
第一類工件
函數(Function)屬于數據庫自身的組成部分,隨數據持久化(AOF/RDB)與復制而同步。 -
庫(Library)管理
多個函數組織在同一庫中,整體加載或替換,不支持單個函數增量更新。 -
Shebang 元數據
通過#!<engine> name=<library>
指定執行引擎與庫名稱。 -
調用接口
FUNCTION LOAD
/FUNCTION DELETE
/FUNCTION LIST
管理庫FCALL
(可寫)或FCALL_RO
(只讀)執行函數
三、加載與注冊函數庫
-
編寫庫源碼
以 Lua 為例,庫文件開頭必須包含 Shebang 行:#!lua name=mylib
-
注冊函數
通過redis.register_function(name, callback, [flags])
將每個函數注冊到庫中:redis.register_function('knockknock',function() return "Who's there?" end )
-
加載到 Redis
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
返回值為庫名(如
mylib
),表示加載成功。
四、調用函數:FCALL 與 FCALL_RO
- FCALL [keys…] [args…]
在任何節點執行(可寫)。 - FCALL_RO [keys…] [args…]
只讀副本上執行,只允許標記為no-writes
的函數。
示例調用:
redis> FCALL knockknock 0
"Who's there?"
五、鍵名與參數
- KEYS:指定所有將要訪問的 Redis 鍵名,必須在調用時列出,保證集群模式下數據訪問的正確路由。
- ARGV:所有非鍵名輸入,供函數內部業務邏輯使用。
在函數回調中,Lua 參數形式為:
function(keys, args)-- keys 是一個 Lua 數組,包含前 numkeys 個參數-- args 是包含后續所有普通輸入的數組
end
六、示例:可復用的 Hash 操作庫
下面以一個「自動記錄最后修改時間」的 Hash 操作庫為例,演示如何在同一庫中組織多個函數并實現代碼復用。
#!lua name=mylib-- 輔助:校驗 keys 數量
local function check_keys(keys)if #keys ~= 1 thenreturn redis.error_reply("只允許傳入一個鍵名")endreturn nil
end-- my_hset:設置字段并記錄時間戳
local function my_hset(keys, args)local err = check_keys(keys)if err then return err endlocal hash = keys[1]local ts = redis.call("TIME")[1]return redis.call("HSET", hash, "_last_modified_", ts, unpack(args))
end-- my_hgetall:讀取所有字段(排除 _last_modified_)
local function my_hgetall(keys, args)local err = check_keys(keys)if err then return err endredis.setresp(3) -- 切換到 RESP3,返回字典格式local hash = keys[1]local res = redis.call("HGETALL", hash)res.map["_last_modified_"] = nilreturn res
end-- my_hlastmodified:讀取修改時間戳
local function my_hlastmodified(keys, args)local err = check_keys(keys)if err then return err endreturn redis.call("HGET", keys[1], "_last_modified_")
end-- 注冊函數,并為只讀函數添加 no-writes 標志
redis.register_function("my_hset", my_hset)
redis.register_function{function_name = "my_hgetall",callback = my_hgetall,flags = {"no-writes"}
}
redis.register_function{function_name = "my_hlastmodified",callback = my_hlastmodified,flags = {"no-writes"}
}
加載并調用:
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
redis> FCALL my_hset 1 myhash field1 "value1"
(integer) 2redis> FCALL_RO my_hgetall 1 myhash
1) "field1"
2) "value1"redis> FCALL_RO my_hlastmodified 1 myhash
"1640772721"
七、集群與持久化
- 復制與持久化
函數與數據一同寫入 AOF/RDB,并復制到從節點,保證函數持久可用。 - 集群加載
需要在所有主節點執行FUNCTION LOAD
,可借助redis-cli --cluster-only-masters
批量操作。 - RDB 鉤子
使用redis-cli --functions-rdb
可導出函數的 RDB 文件,在啟動時預加載到新的實例。
八、函數標志與權限控制
- 默認行為
Redis 假定所有函數可能讀寫數據,故禁止在只讀場景(FCALL_RO 或只讀副本)下執行。 - no-writes 標志
在redis.register_function
時添加flags={'no-writes'}
,表明函數僅執行讀操作,允許在只讀場景下調用。 - 更多 Flags
請參見官方文檔 Script flags,設置合適權限確保安全與隔離。
九、最佳實踐與注意事項
- 避免長時間阻塞
函數執行時會阻塞主線程,應保持邏輯簡短,避免慢查詢或大批量數據掃描。 - 鍵名列舉全
集群模式下訪問所有鍵必須列出于numkeys
參數,防止跨槽錯誤。 - 整體替換
函數庫更新時會替換全量代碼,確保所有函數均在同一版本下協同工作。 - 日志與監控
可在函數內部調用redis.log()
輸出警告或錯誤,便于生產環境診斷。 - 版本管理
建議將函數庫源碼納入版本控制,與應用一并發布,保持部署可追溯。
十、總結
Redis Functions 通過將服務器端腳本化提升為第一類工件,徹底解決了 EVAL 腳本在分發、緩存、調試和復用上的痛點。它不僅讓腳本管理更可靠,也讓 Redis 能像模塊一樣對外提供統一 API,助力構建更復雜、更高效的業務邏輯。結合本文示例和最佳實踐,相信你能快速上手 Redis 7+ 的函數特性,并在生產環境中獲得顯著的運維與性能收益。