redis中lua指令碼的簡單使用

huan1993發表於2021-10-27

一、背景

在使用redis的過程中,發現有些時候需要原子性去操作redis命令,而redis的lua指令碼正好可以實現這一功能。比如: 扣減庫存操作、限流操作等等。
redis的pipelining雖然也可以一次執行一組命令,但是如果在這一組命令的執行過程中,需要根據上一步執行的結果做一些判斷,則無法實現。

二、使用lua指令碼

Redis中使用的是 Lua 5.1 的指令碼規範,同時我們編寫的指令碼的時候,不需要定義 Lua 函式。同時也不能使用全域性變數等等。

1、lua指令碼的格式和注意事項

1、格式

EVAL script numkeys key [key ...] arg [arg ...]

127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1 key1 arg1 arg2
1) "key1"
2) "arg1"
3) "arg2"
127.0.0.1:6379>

解釋

2、注意事項

Lua指令碼中的redis操作的key最好都是通過 KEYS來傳遞,而不要寫死。否則在Redis Cluster的情況下可能有問題.

Lua中KEYS的規範

1、好的寫法

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'zhangsan')" 1 username
OK
127.0.0.1:6379> get username
"zhangsan"

redis命令操作的key是通過KEYS獲取的。

2、差的寫法

127.0.0.1:6379> eval "return redis.call('set','username','zhangsan')" 0
OK
127.0.0.1:6379> get username
"zhangsan"

redis命令操作的key是直接寫死的。

2、將指令碼載入到redis中

需求: 此處定義一個lua指令碼,將輸入的引數的值+1返回。

注意:

當我們把 lua指令碼載入到redis中,這個指令碼並不會馬上執行,而是會快取起來,並且返回sha1校驗和,後期我們可以通過 EVALSHA 來執行這個指令碼。

此處我們記住這個指令碼載入後返回的hash值,在下一步執行的時候需要用到。

127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379>

3、執行lua指令碼

1、通過eval執行

127.0.0.1:6379> eval "return tonumber(KEYS[1]) + 1" 1 100
(integer) 101
127.0.0.1:6379>

2、通過evalsha執行

ef424d378d47e7a8b725259cb717d90a4b12a0de的值為上一步通過 script load載入指令碼後獲取的。

127.0.0.1:6379> evalsha ef424d378d47e7a8b725259cb717d90a4b12a0de 1 100
(integer) 101
127.0.0.1:6379>

通過 evalsha 執行的好處是可以節省頻寬。如果我們的lua指令碼比較長,程式在執行的時候將lua指令碼傳送到redis伺服器則可能耗費的頻寬多,如果傳送的是hash值的話,則耗費的頻寬少。

4、判斷指令碼是否在redis伺服器快取中

127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 1
127.0.0.1:6379> script exists not-exists-sha1
1) (integer) 0
127.0.0.1:6379>

5、清空伺服器上的指令碼快取

注意:
我們無法清除某一個指令碼的快取,只可以清楚所有的快取,一般情況下沒有必要清楚,因為即使有大量的指令碼也不會太佔用伺服器記憶體。


127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 0

清空伺服器上的指令碼快取

6、殺死正在執行的指令碼

127.0.0.1:6379> script kill

注意:

  1. 該命令只可以殺死正在執行的 只讀指令碼
  2. 對於修改了資料的指令碼,無法使用此命令殺死,只能使用 shutdown nosave命令。
  3. 指令碼執行的預設超時時間5分鐘,可以通過redis.conf配置檔案的lua-time-limit配置項修改。
  4. 指令碼即使到達了超時時間,也不會停止執行,因為這違反了lua指令碼的原子性。

三、lua和redis資料型別轉換

Lua的資料型別和Redis的資料型別存在一對一的轉換關係,如果將Redis型別轉換成Lua型別,然後在轉換成Redis型別,那麼結果和初試值是一致的。

1、型別轉換

Redis to Lua conversion table.

  • Redis integer reply -> Lua number
  • Redis bulk reply -> Lua string
  • Redis multi bulk reply -> Lua table (may have other Redis data types nested)
  • Redis status reply -> Lua table with a single ok field containing the status
  • Redis error reply -> Lua table with a single err field containing the error
  • Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type

Lua to Redis conversion table.

  • Lua number -> Redis integer reply (the number is converted into an integer)
  • Lua string -> Redis bulk reply
  • Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
  • Lua table with a single ok field -> Redis status reply
  • Lua table with a single err field -> Redis error reply
  • Lua boolean false -> Redis Nil bulk reply.

2、額外的轉換規則

  1. Lua的布林型別,Lua的True會轉換成Redis的1

3、3個重要規則

1. 數字型別

在Lua中,只有一個number型別,整數和浮點數之間沒有區別,如果我們在Lua中返回一個浮點數,實際返回的是一個整數,如果要返回浮點數,需要以字串的方式返回。

127.0.0.1:6379> eval "return 3.98" 0
(integer) 3
127.0.0.1:6379> eval "return '3.98'" 0
"3.98"

2. lua陣列存在nil

當 Redis 將 Lua 陣列轉換為 Redis 協議時,如果遇到 nil,則轉換會停止。即 nil 後的值都不會返回。

127.0.0.1:6379> eval "return {1,2,'data',nil,'can not return value','vv'}" 0
1) (integer) 1
2) (integer) 2
3) "data"
127.0.0.1:6379>

3. Lua的Table型別包含建和值

出現這種情況返回的redis的是一個空陣列

127.0.0.1:6379> eval "return {key1 ='value1',key2='value2'}" 0
(empty array)
127.0.0.1:6379>

四、lua指令碼中輸出日誌

這個一般除錯我們的指令碼的時候比較有用。

redis.log(loglevel,message)

loglevel的取值範圍:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

舉例:

列印lua指令碼日誌.png

五、一個簡單限流的案例

1、需求

在 1s 之內,方法最大的併發只能是 5。

1s 和 5 當作引數傳遞。

2、實現步驟

1、編寫lua指令碼


-- 輸出使用者傳遞進來的引數
for i, v in pairs(KEYS) do
    redis.log(redis.LOG_NOTICE, "limit: key" .. i .. " = " .. v)
end
for i, v in pairs(ARGV) do
    redis.log(redis.LOG_NOTICE, "limit: argv" .. i .. " = " .. v)
end

-- 限流的key
local limitKey = tostring(KEYS[1])
-- 限流的次數
local limit = tonumber(ARGV[1])
-- 多長時間過期
local expireMs = tonumber(ARGV[2])

-- 當前已經執行的次數
local current = tonumber(redis.call('get', limitKey) or '0')

-- 設定一個斷點
redis.breakpoint()

redis.log(redis.LOG_NOTICE, "limit key: " .. tostring(limitKey) .. " 在[" .. tostring(expireMs) .. "]ms內已經訪問了 " .. tostring(current) .. " 次,最多可以訪問: " .. limit .. " 次")

-- 限流了
if (current + 1 > limit) then
    return { true }
end

-- 未達到訪問限制
-- 訪問次數+1
redis.call("incrby", limitKey, "1")
if (current == 0) then
    -- 設定過期時間
    redis.call("pexpire", limitKey, expireMs)
end

return { false }

2、程式中執行lua指令碼

程式中執行lua指令碼

完整程式碼: https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/springboot-redis-lua

六、lua指令碼的debug

當我們編寫好了lua指令碼後,如果在執行的過程中發生了錯誤,那麼我們如何該如何解決呢?此處我們來了解下如何debug lua 指令碼。

1、lua指令碼中的幾個小命令

在 指令碼中打一個斷點

redis.breakpoint()

2、斷點除錯

1、執行命令


redis-cli --ldb --eval limit.lua invoked , 1 1000

limit.lua 需要debug的lua檔案
invoked 為傳遞到 lua 指令碼中 KEYS 的值
1 和 1000 為傳遞到 lua 指令碼中 ARGV 的值

, 分割 出 KEYS 和 ARGV 的值

2、一些debug指令

  • help: 列出可用的debug指令
  • sn: 執行到當前行並停止 (此時當前行還未執行)
  • c:執行到下個斷點,即執行到lua指令碼中存在 redis.breakpoint()方法的地方
  • list:列出當前行周圍的一些原始碼
  • p:列印出所有的 local 變數的值
  • p <var>:列印具體的某個 local 變數的值
  • r:執行 redis 命令
-- eg:
r set key value 
r get key

3、debug執行結果

lua debug.gif

七、參考文件

  1. https://redis.io/topics/ldb
  2. https://redis.io/commands/eval

相關文章