Lua在Redis的應用

首發於 樊浩柏科學院

Redis 從 2.6 版本起,也已開始支持 Lua 腳本,咱們能夠更加駕輕就熟地使用或擴展 Redis,特別是在高併發場景下 Lua 腳本提供了更高效、可靠的解決方案。html

爲何要使用Lua

咱們先看一個搶購場景下 商品庫存 的問題,用 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 將整個 Lua 腳本做爲一個原子執行,無需考慮併發,無需使用事務來保證數據一致性;
  • 高性能:嵌入 Lua 腳本後,能夠減小多個命令執行的網絡開銷,進而間接提升 Redis 性能;
  • 可複用:Lua 腳本會保存於 Redis 中,客戶端均可以使用這些腳本;

在Redis中嵌入Lua

使用Lua解析器

Redis 提供了 EVAL(直接執行腳本) 和 EVALSHA(執行 SHA1 值的腳本) 這兩個命令,可使用內置的 Lua 解析器執行 Lua 腳本。語法格式爲:網絡

  • EVAL script numkeys key [key ...] arg [arg ...]
  • EVALSHA sha1 numkeys key [key ...] arg [arg ...]

參數說明:併發

  • script / sha1:EVAL 命令的第一個參數爲須要執行的 Lua 腳本字符,EVALSHA 命令的一個參數爲 Lua 腳本的 SHA1 值
  • numkeys:表示 key 的個數
  • key [key ...]:從第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數能夠在 Lua 中經過全局數組 KYES[i] 訪問
  • arg [arg ...]:附加參數,在 Lua 中經過全局數組 ARGV[i] 訪問

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'

Lua腳本調用Redis命令

Redis 建立了用於與 Lua 環境協做的組件—— 僞客戶端,它負責執行 Lua 腳本中的 Redis 命令。

調用Redis命令

在 Redis 內置的 Lua 解析器中,調用 redis.call() 和 redis.pcall() 函數執行 Redis 的命令。它們除了處理錯誤的行爲不同外,其餘行爲都保持一致。調用 格式:

  • redis.call(command, [key ...], arg [arg ...] )
  • redis.pcall(command, [key ...], arg [arg ...] )
> EVAL "return redis.call('SET', 'name', 'fhb')" 0
> EVAL "return redis.pcall('GET', 'name')" 0
"fhb"

Redis日誌

在 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

案例

API 訪問速率控制

經過 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] 爲限制的單位時間。

批量HGETTALL

這個例子演示經過 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 腳本給咱們帶來了許多便利,可是須要注意幾個使用事項:

  • Lua 腳本在執行時是阻塞的,不該該在 Lua 腳本中有耗時的處理邏輯;
  • 在集羣模式時,Lua 腳本必須使用參數 key 傳遞需操做的 Redis 的 key,且要求所操做的 key 都在同一個 slot 節點上,可使用以{}標記的 hash tag 方式解決。

相關文章 »

相關文章
相關標籤/搜索