首發於 樊浩柏科學院
Redis 從 2.6 版本起,也已開始支持 Lua 腳本,咱們能夠更加駕輕就熟地使用或擴展 Redis,特別是在高併發場景下 Lua 腳本提供了更高效、可靠的解決方案。html
咱們先看一個搶購場景下 商品庫存 的問題,用 PHP 可簡單實現爲:nginx
$key = 'number:string'; $redis = new Redis(); $number = $redis->get($key); if ($number <= 0) { return 0; } $redis->decr($key); return $number--;
這段代碼其實存在問題,高併發時會出現庫存超賣的狀況,由於上述操做在 Redis 中不是原子操做,會致使庫存邏輯的判斷失效。儘管能夠經過優化代碼來解決問題,好比使用 Decr 原子操做命令、或者使用 鎖 的方式,但這裏使用 Lua 腳原本解決。redis
local key = 'number:string' local number = tonumber(redis.call("GET", key)) if number <= 0 then return 0 end redis.call("DECR", key) return number--
這段腳本代碼雖然是 Lua 語言編寫( 進入Lua的世界),可是其實就是 PHP 版本的翻譯版。那爲何這樣,Lua 腳本就能解決庫存問題了呢?數組
Redis 中嵌入 Lua 腳本,所具備的幾個特性爲:緩存
Redis 提供了 EVAL(直接執行腳本) 和 EVALSHA(執行 SHA1 值的腳本) 這兩個命令,可使用內置的 Lua 解析器執行 Lua 腳本。語法格式爲:網絡
參數說明:併發
EVAL 命令的使用示例:函數
> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
每次使用 EVAL 命令都會傳遞需執行的 Lua 腳本內容,這樣增長了寬帶的浪費。Redis 內部會永久保存被運行在腳本緩存中,因此使用 EVALSHA(建議使用) 命令就能夠根據腳本 SHA1 值執行對應的 Lua 腳本。高併發
> SCRIPT LOAD "return 'hello'" "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b" > EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0 "hello"
Redis 中執行 Lua 腳本都是以原子方式執行,因此是原子操做。另外,redis-cli 命令行客戶端支持直接使用
--eval lua_file
參數執行 Lua 腳本。
Redis 中有關腳本的命令除了 EVAL 和 EVALSHA 外,其餘經常使用命令 以下:性能
命令 | 描述 |
---|---|
SCRIPT EXISTS script [script ...] | 查看腳本是是否保存在緩存中 |
SCRIPT FLUSH | 從緩存中移除全部腳本 |
SCRIPT KILL | 殺死當前運行的腳本 |
SCRIPT LOAD script | 將腳本添加到緩存中,不當即執行 返回腳本SHA1值 |
因爲 Redis 和 Lua 都有各自定義的數據類型,因此在使用執行完 Lua 腳本後,會存在一個數據類型轉換的過程。
Lua 到 Redis 類型轉換與 Redis 到 Lua 類型轉換相同部分關係:
Lua 類型 | Redis 返回類型 | 說明 |
---|---|---|
number | integer | 浮點數會轉換爲整數 3.333-->3 |
string | bulk | |
table(array) | multi bulk | |
boolean false | nil |
> EVAL "return 3.333" 0 (integer) 3 > EVAL "return 'fhb'" 0 "fhb" > EVAL "return {'fhb', 'lw', 'lbf'}" 0 1) "fhb" 2) "lw" 3) "lbf" > EVAL "return false" 0 (nil)
須要注意的是,從 Lua 轉化爲 Redis 類型比 Redis 轉化爲 Lua 類型多了一條 額外 規則:
Lua 類型 | Redis 返回類型 | 說明 |
---|---|---|
boolean true | integer | 返回整型 1 |
> EVAL "return true" 0 (integer) 1
總而言之,類型轉換的原則 是將一個 Redis 值轉換成 Lua 值,以後再將轉換所得的 Lua 值轉換回 Redis 值,那麼這個轉換所得的 Redis 值應該和最初時的 Redis 值同樣。
爲了防止沒必要要的數據泄漏進 Lua 環境, Redis 腳本不容許建立全局變量。
-- 定義全局函數 function f(n) return n * 2 end return f(4);
執行redis-cli --eval function.lua
命令,會拋出嘗試定義全局變量的錯誤:
(error) ERR Error running script (call to f_0a602c93c4a2064f8dc648c402aa27d68b69514f): @enable_strict_lua:8: user_script:1: Script attempted to create global variable 'f'
Redis 建立了用於與 Lua 環境協做的組件—— 僞客戶端,它負責執行 Lua 腳本中的 Redis 命令。
在 Redis 內置的 Lua 解析器中,調用 redis.call() 和 redis.pcall() 函數執行 Redis 的命令。它們除了處理錯誤的行爲不同外,其餘行爲都保持一致。調用 格式:
> EVAL "return redis.call('SET', 'name', 'fhb')" 0 > EVAL "return redis.pcall('GET', 'name')" 0 "fhb"
在 Lua 腳本中,能夠經過調用 redis.log() 函數來寫 Redis 日誌。格式爲:
redis.log(loglevel, message)
loglevel 參數能夠是 redis.LOG_DEBUG、redis.LOG_VERBOSE、redis.LOG_NOTICE、redis.LOG_WARNING 的任意值。
查看redis.conf
日誌配置信息:
# logleval必須一致纔會記錄 loglevel notice logfile "/home/logs/redis.log"
Lua 寫 Redis 日誌示例:
> EVAL "redis.log(redis.LOG_NOTICE, 'I am fhb')" 0 113:M 04 Sep 13:12:36.229 * I am fhb
經過 Lua 實現一個針對用戶的 API 訪問速率控制,Lua 代碼以下:
local key = "rate.limit:string:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = tonumber(ARGV[2]) local times = redis.call("INCR", key) if times == 1 then redis.call("EXPIRE", key, expire_time) end if times > limit then return 0 end return 1
KEYS[1] 能夠用 API 的 URI + 用戶 uid 組成,ARGV[1] 爲單位時間限制訪問的次數,ARGV[2] 爲限制的單位時間。
這個例子演示經過 Lua 實現批量 HGETALL,固然也可使用 管道 實現。
-- KEYS爲uid數組 local users = {} for i,uid in ipairs(KEYS) do local user = redis.call('hgetall', uid) if user ~= nil then table.insert(users, i, user) end end return users
雖然使用 Lua 腳本給咱們帶來了許多便利,可是須要注意幾個使用事項:
{}
標記的 hash tag 方式解決。相關文章 »