一、背景
在使用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的情況下可能有問題.
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
注意:
-
該命令只可以殺死正在運行的
只讀腳本
。 -
對于修改了數據的腳本,無法使用此命令殺死,只能使用
shutdown nosave
命令。 -
腳本執行的
默認超時時間
為5分鐘
,可以通過redis.conf
配置文件的lua-time-limit
配置項修改。 - 腳本即使到達了超時時間,也不會停止執行,因為這違反了lua腳本的原子性。
三、lua和redis數據類型轉換
lua
的數據類型和redis
的數據類型存在一對一的轉換關系,如果將redis類型轉換成lua類型,然后在轉換成redis類型,那么結果和初試值是一致的。
1、類型轉換
redis to luaconversion 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 redisconversion 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、額外的轉換規則
- 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
舉例:
五、一個簡單限流的案例
1、需求
在 1s 之內,方法最大的并發只能是 5。
1s 和 5 當作參數傳遞。
2、實現步驟
1、編寫lua腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
-- 輸出用戶傳遞進來的參數 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腳本
完整代碼: https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/springboot-redis-lua
六、lua腳本的debug
當我們編寫好了lua腳本后,如果在執行的過程中發生了錯誤,那么我們如何該如何解決呢?此處我們來了解下如何debug lua 腳本。
1、lua腳本中的幾個小命令
在 腳本中打一個斷點
redis.breakpoint()
2、斷點調試
1、執行命令
1
2
3
4
5
6
7
|
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指令 -
s
或n
: 運行到當前行并停止 (此時當前行還未執行) -
c
:運行到下個斷點,即運行到lua腳本中存在redis.breakpoint()
方法的地方 -
list
:列出當前行周圍的一些源碼 -
p
:打印出所有的 local 變量的值 -
p <var>
:打印具體的某個 local 變量的值 -
r
:執行 redis 命令
-- eg:
r set key value
r get key
3、debug運行結果
七、參考文檔
https://redis.io/topics/ldb
https://redis.io/commands/eval
到此這篇關于redis中lua腳本的簡單使用的文章就介紹到這了,更多相關redis中lua腳本使用內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/huan1993/p/15472920.html