Lua是一個高效的輕量級指令碼語言(和JavaScript類似),用標準C語言編寫並以原始碼形式開放, 其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴充套件和客製化功能。Lua在葡萄牙語中是「月亮」的意思,它的logo形式衛星,寓意是Lua是一個「衛星語言」,能夠方便地嵌入到其他語言中使用;其實在很多常見的框架中,都有嵌入Lua指令碼的功能,比如OpenResty、Redis等。
使用Lua指令碼的好處:
減少網路開銷,在Lua指令碼中可以把多個命令放在同一個指令碼中執行
原子操作,redis會將整個指令碼作為一個整體執行,中間不會被其他命令插入。換句話說,編寫指令碼的過程中無需擔心會出現競態條件
複用性,使用者端傳送的指令碼會永遠儲存在redis中,這意味著其他使用者端可以複用這一指令碼來完成同樣的邏輯
Lua是一個獨立的指令碼語言,所以它有專門的編譯執行工具,下面簡單帶大家安裝一下。
安裝步驟
tar -zxvf lua-5.4.3.tar.gz cd lua-5.4.3 make linux make install
最後,直接輸入lua
命令即可進入lua的控制檯。Lua指令碼有自己的語法、變數、邏輯運運算元、函數等,這塊我就不在這裡做過多的說明,可以自己進入下面連結進去看
https://www.runoob.com/lua/lua-tutorial.html
Redis中整合了Lua的編譯和執行器,所以我們可以在Redis中定義Lua指令碼去執行。同時,在Lua指令碼中,可以直接呼叫Redis的命令,來操作Redis中的資料。
redis.call(‘set’,'hello','world') local value=redis.call(‘get’,’hello’)
redis.call 函數的返回值就是redis命令的執行結果,前面我們介紹過redis的5中型別的資料返回的值的型別也都不一樣,redis.call函數會將這5種型別的返回值轉化對應的Lua的資料型別
在很多情況下我們都需要指令碼可以有返回值,畢竟這個指令碼也是一個我們所編寫的命令集,我們可以像呼叫其他redis內建命令一樣呼叫我們自己寫的指令碼,所以同樣redis會自動將指令碼返回值的Lua資料型別轉化為Redis的返回值型別。 在指令碼中可以使用return 語句將值返回給redis使用者端,通過return語句來執行,如果沒有執行return,預設返回為nil。
編寫完指令碼後最重要的就是在程式中執行指令碼。Redis提供了EVAL命令可以使開發者像呼叫其他Redis內建命令一樣呼叫指令碼。
[EVAL] [指令碼內容] [key引數的數量] [key …] [arg …]
可以通過key和arg這兩個引數向指令碼中傳遞資料,他們的值可以在指令碼中分別使用KEYS和ARGV 這兩個型別的全域性變數存取。
比如我們通過指令碼實現一個set命令,通過在redis使用者端中呼叫,那麼執行的語句是:
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lua hello
上述指令碼相當於使用Lua指令碼呼叫了Redis的set
命令,儲存了一個key=lua,value=hello到Redis中。
考慮到我們通過eval執行lua指令碼,指令碼比較長的情況下,每次呼叫指令碼都需要把整個指令碼傳給redis,比較佔用頻寬。為了解決這個問題,redis提供了EVALSHA命令允許開發者通過指令碼內容的SHA1摘要來執行指令碼。該命令的用法和EVAL一樣,只不過是將指令碼內容替換成指令碼內容的SHA1摘要
Redis在執行EVAL命令時會計算指令碼的SHA1摘要並記錄在指令碼快取中
執行EVALSHA命令時Redis會根據提供的摘要從指令碼快取中查詢對應的指令碼內容,如果找到了就執行指令碼,否則返回「NOSCRIPT No matching script,Please use EVAL」
# 將指令碼加入快取並生成sha1命令 script load "return redis.call('get','lua')" # ["13bd040587b891aedc00a72458cbf8588a27df90"] # 傳遞sha1的值來執行該命令 evalsha "13bd040587b891aedc00a72458cbf8588a27df90" 0
通過lua指令碼來實現一個存取頻率限制功能。
思路,定義一個key,key中包含ip地址。 value為指定時間內的存取次數,比如說是10秒內只能存取3次。
定義Lua指令碼
local times=redis.call('incr',KEYS[1]) -- 如果是第一次進來,設定一個過期時間 if times == 1 then redis.call('expire',KEYS[1],ARGV[1]) end -- 如果在指定時間記憶體取次數大於指定次數,則返回0,表示存取被限制 if times > tonumber(ARGV[2]) then return 0 end -- 返回1,允許被存取 return 1
定義controller,提供存取測試方法
@RestController public class RedissonLuaController { @Autowired RedissonClient redissonClient; private final String LIMIT_LUA="local times=redis.call('incr',KEYS[1])\n" + "if times==1 then\n" + " redis.call('expire',KEYS[1],ARGV[1])\n" + "end\n" + "if times > tonumber(ARGV[2]) then\n" + " return 0\n" + "end \n" + "return 1"; @GetMapping("/lua/{id}") public String lua(@PathVariable("id") Integer id) throws ExecutionException, InterruptedException { RScript rScript=redissonClient.getScript(); List<Object> keys= Arrays.asList("LIMIT:"+id); RFuture<Object> future=rScript.evalAsync(RScript.Mode.READ_WRITE,LIMIT_LUA, RScript.ReturnType.INTEGER,keys,10,3); return future.get().toString(); } }
要注意,上述指令碼執行的時候會有問題,因為redis預設的序列化方式導致value的值在傳遞到指令碼中時,轉成了物件型別,需要修改redisson.yml
檔案,增加codec的序列化方式。
application.yml
spring:
redis:
redisson:
file: classpath:redisson.yml
redisson.yml
singleServerConfig: address: redis://192.168.221.128:6379 codec: !<org.redisson.codec.JsonJacksonCodec> {}
redis的指令碼執行是原子的,即指令碼執行期間Redis不會執行其他命令。所有的命令必須等待指令碼執行完以後才能執行。為了防止某個指令碼執行時間過程導致Redis無法提供服務。Redis提供了lua-time-limit引數限制指令碼的最長執行時間。預設是5秒鐘。
非事務性操作
當指令碼執行時間超過這個限制後,Redis將開始接受其他命令但不會執行(以確保指令碼的原子性),而是返回BUSY的錯誤,下面演示一下這種情況。
開啟兩個使用者端視窗,在第一個視窗中執行lua指令碼的死迴圈
eval "while true do end" 0
在第二個視窗中執行get lua
,會得到如下的異常。
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
我們會發現執行結果是Busy, 接著我們通過script kill 的命令終止當前執行的指令碼,第二個視窗的顯示又恢復正常了。
存在事務性操作
如果當前執行的Lua指令碼對Redis的資料進行了修改(SET、DEL等),那麼通過SCRIPT KILL 命令是不能終止指令碼執行的,因為要保證指令碼執行的原子性,如果指令碼執行了一部分終止,那就違背了指令碼原子性的要求。最終要保證指令碼要麼都執行,要麼都不執行
同樣開啟兩個視窗,第一個視窗執行如下命令
eval "redis.call('set','name','ljx') while true do end" 0
在第二個視窗執行
get lua
結果一樣,仍然是busy,但是這個時候通過script kill命令,會發現報錯,沒辦法kill。遇到這種情況,只能通過shutdown nosave命令來強行終止redis。shutdown nosave和shutdown的區別在於 shutdown nosave不會進行持久化操作,意味著發生在上一次快照後的資料庫修改都會丟失。