Redis之Lua的應用(四)
一、什麼是Lua腳本
Lua是一個高效的輕量級腳本語言(和JavaScript類似),用標準C語言編寫並以源程式碼形式開放, 其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴展和訂製功能。Lua在葡萄牙語中是「月亮」的意思,它的logo形式衛星,寓意是Lua是一個「衛星語言」,能夠方便地嵌入到其他語言中使用;其實在很多常見的框架中,都有嵌入Lua腳本的功能,比如OpenResty、Redis等。
使用Lua腳本的好處:
-
減少網路開銷,在Lua腳本中可以把多個命令放在同一個腳本中運行
-
原子操作,redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。換句話說,編寫腳本的過程中無需擔心會出現競態條件
-
復用性,客戶端發送的腳本會永遠存儲在redis中,這意味著其他客戶端可以復用這一腳本來完成同樣的邏輯
二、Lua的下載和安裝
Lua是一個獨立的腳本語言,所以它有專門的編譯執行工具,下面簡單帶大家安裝一下。
安裝步驟
tar -zxvf lua-5.4.3.tar.gz cd lua-5.4.3 make linux make install
最後,直接輸入lua
命令即可進入lua的控制台。Lua腳本有自己的語法、變數、邏輯運算符、函數等,這塊我就不在這裡做過多的說明,可以自己進入下面鏈接進去看
//www.runoob.com/lua/lua-tutorial.html
三、Redis與Lua
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中執行Lua腳本相關的命令
編寫完腳本後最重要的就是在程式中執行腳本。Redis提供了EVAL命令可以使開發者像調用其他Redis內置命令一樣調用腳本。
EVAL命令-執行腳本
[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中。
EVALSHA命令
考慮到我們通過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
五、自己通過Redisson執行Lua腳本
通過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> {}
六、Lua腳本的原子性
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不會進行持久化操作,意味著發生在上一次快照後的資料庫修改都會丟失。