在 Redis 中編寫和執行 Lua 腳本
Lua 腳本是在 Redis 中執行自定義邏輯的強大功能,可以直接在 Redis 服務器上執行。這減少了延遲,提高了性能,并能夠實現客戶端腳本難以或不可能實現的原子操作。通過在 Redis 中嵌入 Lua 腳本,您可以執行復雜的數據操作和業務邏輯,而無需為每個單獨的命令承擔網絡通信的開銷。本課將涵蓋在 Redis 中編寫和執行 Lua 腳本的基礎知識,為您提供有效利用這一功能的知識。
Redis 中的 Lua 腳本簡介
Redis 使用 Lua 作為其腳本語言。Lua 是一種輕量級、可嵌入的腳本語言,以其速度和簡潔性而聞名。Redis 在服務器端執行 Lua 腳本,這意味著腳本直接在 Redis 實例內運行。這有幾個優勢:
- 減少延遲:?避免了客戶端和服務器之間為每個命令進行的網絡往返。
- 原子性:?確保整個腳本作為一個單一的原子操作執行,防止競態條件和數據不一致。這類似于事務的工作方式,但增加了自定義邏輯的靈活性。
- 代碼復用性:?腳本可以存儲在 Redis 中,并在多個客戶端和會話之間復用。
- 簡化邏輯:?復雜操作可以封裝在單個腳本中,使客戶端代碼更清晰且易于維護。
為什么是 Lua?
Lua 被選為 Redis 腳本語言的原因是:
- 簡潔性: Lua 的語法相對容易學習,特別是對于那些熟悉其他腳本語言的人。
- 速度: Lua 以其性能而聞名,適合在 Redis 中進行服務器端執行。
- 可嵌入性:?Lua 被設計為可以輕松嵌入到其他應用程序中,使其成為 Redis 的理想選擇。
- 安全性:?Redis 為 Lua 腳本提供了一個受保護的環境,阻止它們訪問文件系統或進行網絡調用(某些需要顯式配置的例外情況除外)。
為 Redis 編寫 Lua 腳本
Redis 中的 Lua 腳本通過一個特殊的?redis
?對象與 Redis 數據存儲交互。該對象提供了與標準 Redis 命令相對應的函數。
基礎 Lua 語法
在深入 Redis 特定腳本之前,讓我們復習一些基本的 Lua 語法:
-
變量:?變量無需指定類型聲明。
local my_variable = "Hello, Redis!" local my_number = 123
-
數據類型: Lua 支持多種數據類型,包括:
string
: 文本數據。number
: 數值數據(整數和浮點數)。boolean
:?true
?或?false
.table
: 一種通用的數據結構,可以用作數組或字典。nil
: 表示值不存在。
-
注釋:?使用?
--
?進行單行注釋。-- This is a comment
-
控制流: Lua 提供了標準的控制流語句:
-
if-then-else
:local x = 10 if x > 5 then-- Code to execute if x is greater than 5 else-- Code to execute otherwise end
-
for
?循環:for i = 1, 10 do-- Code to execute 10 times end
-
while
?循環:local i = 1 while i <= 10 do-- Code to execute while i is less than or equal to 10i = i + 1 end
-
-
功能:?功能使用?
function
?關鍵字定義。function add(a, b)return a + b endlocal result = add(5, 3) -- result will be 8
redis
?對象
redis
?對象是從 Lua 腳本中與 Redis 交互的主要接口。它提供了一個?call()
?函數,允許你執行 Redis 命令。
local value = redis.call('GET', 'mykey')
在這個例子中,redis.call('GET', 'mykey')
?對鍵?mykey
?執行?GET
?命令并返回值。
示例:遞增計數器
這是一個簡單的 Lua 腳本,用于增加存儲在 Redis 中的計數器:
-- Get the current value of the counter
local current_value = redis.call('GET', KEYS[1])-- If the counter doesn't exist, initialize it to 0
if not current_value thencurrent_value = 0
end-- Increment the counter
local new_value = tonumber(current_value) + 1-- Set the new value in Redis
redis.call('SET', KEYS[1], new_value)-- Return the new value
return new_value
在這個腳本中:
KEYS[1]
?指的是傳遞給腳本的第一個鍵。 Redis 中的 Lua 腳本以數組形式接收鍵和參數。?KEYS
?是一個包含鍵名稱的數組,而?ARGV
?是一個包含參數值的數組。redis.call('GET', KEYS[1])
?獲取計數器的當前值。tonumber(current_value)
?將從 Redis 獲取的值(始終是字符串)轉換為數字。redis.call('SET', KEYS[1], new_value)
?設置計數器的新值。- 腳本返回計數器的新值。
示例:賬戶間原子轉賬
考慮一個需要原子方式在兩個賬戶間轉賬的場景。這可以使用 Lua 腳本實現:
-- KEYS[1]: Source account key
-- KEYS[2]: Destination account key
-- ARGV[1]: Amount to transferlocal source_balance = tonumber(redis.call('GET', KEYS[1]))
local destination_balance = tonumber(redis.call('GET', KEYS[2]))
local amount = tonumber(ARGV[1])if source_balance and source_balance >= amount thenredis.call('DECRBY', KEYS[1], amount)redis.call('INCRBY', KEYS[2], amount)return {1, "OK"} -- Success
elsereturn {0, "Insufficient funds"} -- Failure
end
在這個腳本中:
KEYS[1]
?是源賬戶余額的密鑰。KEYS[2]
?是目標賬戶余額的密鑰。ARGV[1]
?是轉賬金額。- 腳本檢查源賬戶是否有足夠的資金。
- 如果資金充足,它會減少源賬戶余額并增加目標賬戶余額。
- 腳本返回成功或失敗消息。
Lua 腳本的錯誤處理
Lua 腳本在執行過程中可能會遇到錯誤。優雅地處理這些錯誤非常重要。如果 Lua 腳本遇到錯誤,Redis 將自動撤銷腳本所做的任何更改,確保原子性。
您可以使用?pcall
(受保護調用)來捕獲腳本中的錯誤:
local status, result = pcall(function()-- Your code herereturn redis.call('GET', 'nonexistent_key')
end)if status then-- Code to handle successful executionif result then-- Process the resultend
else-- Code to handle errorsredis.log(redis.LOG_WARNING, "Error: " .. result)
end
在這個例子中:
pcall
?在受保護的環境中執行匿名函數。- 如果函數執行成功,
status
?將為?true
,result
?將包含函數的返回值。 - 如果函數遇到錯誤,
status
?將是?false
,而?result
?將包含錯誤信息。 redis.log
?用于將錯誤信息記錄到 Redis 日志中。
在 Redis 中執行 Lua 腳本
在 Redis 中執行 Lua 腳本主要有兩種方式:
- EVAL:?通過將腳本代碼直接傳遞給 Redis 服務器來執行腳本。
- EVALSHA:?通過將腳本的 SHA1 哈希值傳遞給 Redis 服務器來執行腳本。這需要首先使用?
SCRIPT LOAD
?命令將腳本加載到 Redis 腳本緩存中。
使用 EVAL
EVAL
?命令接受以下參數:
EVAL script numkeys key [key ...] arg [arg ...]
script
?: 要執行的 Lua 腳本。numkeys
: 腳本將訪問的鍵的數量。這些鍵必須立即作為參數傳遞給?numkeys
。key [key ...]
: 腳本將訪問的鍵。arg [arg ...]
: 腳本可使用的額外參數。
示例:
redis-cli EVAL "return redis.call('GET', KEYS[1])" 1 mykey
該命令執行一個 Lua 腳本,獲取鍵?mykey
?的值。1
?表示該腳本訪問一個鍵,即?mykey
。
使用 EVALSHA
EVALSHA
?命令接受以下參數:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
sha1
: Lua 腳本的 SHA1 哈希值。numkeys
: 腳本將訪問的鍵的數量。key [key ...]
: 腳本將訪問的鍵。arg [arg ...]
: 腳本可使用的額外參數。
在使用?EVALSHA
?之前,你必須使用?SCRIPT LOAD
?命令將腳本加載到 Redis 腳本緩存中:
redis-cli SCRIPT LOAD "return redis.call('GET', KEYS[1])"
此命令返回腳本的 SHA1 哈希值。然后您可以使用?EVALSHA
:
redis-cli EVALSHA <sha1_hash> 1 mykey
EVALSHA 的優點
- 降低帶寬:
EVALSHA
?僅發送腳本的 SHA1 哈希值,這比腳本本身小得多。這減少了網絡帶寬,特別是對于大型腳本。 - 性能提升: Redis 可以緩存編譯后的腳本,這可以提升性能,特別是如果腳本執行頻繁的話。
示例:使用 EVAL 和 EVALSHA
讓我們重新審視計數器自增的例子,并使用?EVAL
?和?EVALSHA
?來執行它。
使用 EVAL:
redis-cli EVAL "local current_value = redis.call('GET', KEYS[1]) if not current_value then current_value = 0 end local new_value = tonumber(current_value) + 1 redis.call('SET', KEYS[1], new_value) return new_value" 1 mycounter
使用 EVALSHA:
首先,加載腳本:
redis-cli SCRIPT LOAD "local current_value = redis.call('GET', KEYS[1]) if not current_value then current_value = 0 end local new_value = tonumber(current_value) + 1 redis.call('SET', KEYS[1], new_value) return new_value"
這將返回腳本的 SHA1 哈希值(例如,?a7e5b98b9d4a8a2a3b1c2c3d4e5f6a7b8c9d0e1f
?)。
然后,使用?EVALSHA
?執行腳本:
redis-cli EVALSHA a7e5b98b9d4a8a2a3b1c2c3d4e5f6a7b8c9d0e1f 1 mycounter
管理腳本
Redis 提供了幾個用于管理 Lua 腳本的命令:
- SCRIPT LOAD:?將腳本加載到腳本緩存中,并返回其 SHA1 哈希。
- SCRIPT EXISTS:?檢查是否存在具有給定 SHA1 哈希值的腳本在腳本緩存中。
- SCRIPT FLUSH:?從腳本緩存中移除所有腳本。
- SCRIPT KILL:?終止當前正在執行的腳本。這只有在腳本被標記為可中斷時才可能實現(這需要 Redis 版本 7 或更高版本,并在腳本中使用?
redis.yield()
?函數)。
實際練習
- 實現一個速率限制器:?編寫一個 Lua 腳本,實現一個簡單的速率限制器。該腳本應接受一個鍵(例如,用戶 ID)和一個限制值作為參數。它應在指定的時間窗口內允許一定數量的請求。如果超出限制,該腳本應返回錯誤。使用 Redis 列表來存儲請求的時間戳。
- 原子增量帶過期功能:?編寫一個 Lua 腳本,原子性地遞增一個計數器,并在該鍵不存在時設置過期時間。這可以用于跟蹤臨時事件。
- 實現一個簡單的布隆過濾器:?編寫一個 Lua 腳本,實現一個簡化的布隆過濾器。該腳本應接受一個鍵(布隆過濾器的名稱)和一個要插入的值。它應多次對值進行哈希處理,并在布隆過濾器中設置相應的位。