Redis整合Lua腳本的實(shí)現(xiàn)操作
一、Lua介紹
- Lua是一種用C語(yǔ)言編寫而成的輕量級(jí)的腳本語(yǔ)言。
1.1 Lua特點(diǎn)
輕量性:lua語(yǔ)言的官方版本只具有核心和最基本的庫(kù),啟動(dòng)速度很快,非常適合嵌入其他語(yǔ)言編寫的程序里,比如將lua嵌入到Redis里。
擴(kuò)展性:在lua語(yǔ)言里,包含了非常便于開(kāi)發(fā)使用的擴(kuò)展接口,能非常方便地?cái)U(kuò)展實(shí)現(xiàn)其他語(yǔ)言的功能。
結(jié)合Redis和lua腳本語(yǔ)言的特性,如果在Redis里遇到如下需求,就可以引入lua腳本。
- 重復(fù)執(zhí)行相同類型的命令,比如要緩存1到1000的數(shù)字到內(nèi)存里。
- 在高并發(fā)場(chǎng)景下減少網(wǎng)絡(luò)調(diào)用的開(kāi)銷,一次性執(zhí)行多條命令。
- Redis會(huì)將lua腳本作為一個(gè)整體來(lái)執(zhí)行,天然具有原子性。
由于Redis是以單線程的形式運(yùn)行的,如果運(yùn)行的lua腳本沒(méi)有響應(yīng)或者不返回值,就會(huì)阻塞整個(gè)Redis服務(wù),并且在運(yùn)行時(shí)lua腳本一般很難調(diào)試,所以在Redis整合lua腳本時(shí)應(yīng)該確保腳本里的代碼盡量少且盡可能結(jié)構(gòu)清晰,以免造成阻塞整個(gè)Redis服務(wù)的情況。
Redis對(duì)lua腳本的支持是從Redis 2.6.0版本開(kāi)始引入的,它可以讓用戶在Redis服務(wù)器內(nèi)置的Lua解釋器中執(zhí)行指定的lua腳本。
被執(zhí)行的lua腳本可以直接調(diào)用Redis命令,并使用lua語(yǔ)言以及內(nèi)置的函數(shù)庫(kù)處理命令結(jié)果。
二、在Redis里調(diào)用lua腳本
2.1 redis-cli 命令執(zhí)行腳本
lua腳本是一種解釋語(yǔ)言,所以可以安裝解釋器以后再運(yùn)行l(wèi)ua腳本,但這里是在redis里引入lua腳本,所以就將給出redis-cli 命令運(yùn)行l(wèi)ua腳本的相關(guān)步驟。
創(chuàng)建/opt/lua
目錄,在其中創(chuàng)建redis-demo.lua文件。注意,lua腳本的文件擴(kuò)展名一般都是.lua
。
redis.call('set','name','zhangsan')
在lua腳本里,可以通過(guò)redis.call
方法調(diào)用Redis的命令。
- 該方法的第一個(gè)參數(shù)是Redis命令。
- 第二個(gè)以及后繼參數(shù)是該Redis命令的參數(shù)。
通過(guò)如下的docker命令創(chuàng)建一個(gè)名為redis-lua的Redis容器,在其中通過(guò)-v
參數(shù)把包含lua腳本的/opt/lua
目錄映射為容器里的/lua-script
目錄。這樣啟動(dòng)后該容器的/lua-script
目錄里就能看到在外部操作系統(tǒng)里創(chuàng)建的lua腳本。
docker run -itd --name redis-lua -v /opt/lua:/lua-script -p 6379:6379 redis:latest
啟動(dòng)該容器后,可以通過(guò)如下的命令進(jìn)入該容器的命令行窗口里
docker exec -it redis-lua /bin/bash
可以通過(guò)如下的redis-cli
命令執(zhí)行剛才創(chuàng)建的lua腳本,其中--eval
是redis里執(zhí)行l(wèi)ua腳本的命令,/lua-script/redis-demo.lua
則表示該腳本的路徑和文件名。
redis-cli --eval /lua-script/redis-demo.lua
運(yùn)行上述命令后,得到的返回值是空(nil),這是因?yàn)樵搇ua腳本沒(méi)有通過(guò)return
返回值。
如果用redis-cli
命令進(jìn)入該Redis服務(wù)器,在通過(guò)get name
命令就能看到通過(guò)上述lua腳本設(shè)置到緩存的那么值。
root@c1beeb888673:/data# redis-cli --eval /lua-script/redis-demo.lua (nil) root@c1beeb888673:/data# redis-cli 127.0.0.1:6379> get name "zhangsan" 127.0.0.1:6379>
2.2 eval命令執(zhí)行腳本
在實(shí)際項(xiàng)目里,如果lua腳本里包含的語(yǔ)句較多,那么一般會(huì)以lua腳本文件的方式來(lái)維護(hù)。
如果lua腳本里的語(yǔ)句很少,那么可以直接通過(guò)eval
命令來(lái)執(zhí)行腳本。
通過(guò)redis-cli命令進(jìn)入Redis服務(wù)器的客戶端里,隨后運(yùn)行如下的eval命令
EVAL 腳本內(nèi)容 key參數(shù)的數(shù)量 [key...] [args...]
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 age 22
通過(guò)key
和arg
這兩類參數(shù)向腳本傳遞數(shù)據(jù),他們的值可以在腳本中分別使用KEYS
和ARGV
兩個(gè)表類型的全局變量訪問(wèn)。
key參數(shù)的數(shù)量是必須要指定的,沒(méi)有key參數(shù)時(shí)必須設(shè)為0,EVAL會(huì)依據(jù)這個(gè)數(shù)值將傳入的參數(shù)分別傳入KEYS
和ARGV
兩個(gè)表類型的全局變量。
2.3 return返回腳本運(yùn)行結(jié)果
在剛才redis整合lua腳本的場(chǎng)景里,都是通過(guò)redis.call
方法執(zhí)行redis命令,并沒(méi)有返回結(jié)果,在一些場(chǎng)景里,需要返回結(jié)果,此時(shí)就需要在腳本里引入return
語(yǔ)句。
到/opt/lua
目錄,在其中創(chuàng)建return-lua.lua
文件。在其中加入如下的一句return
代碼。返回1這個(gè)結(jié)果。
return redis.call('set','name','tom')
進(jìn)入該容器的命令窗口,在其中在運(yùn)行命令,就能看到返回結(jié)果。
redis-cli --eval /lua-script/return-lua.lua
2.4 Redis和lua相關(guān)的命令
可以通過(guò)SCRIOPT LOAD
命令事先裝置腳本,隨后可以用EVALSHA
命令多次運(yùn)行該腳本。
SCRIOPT LOAD '腳本內(nèi)容' EVALSHA 'id' 0
127.0.0.1:6379> SCRIPT LOAD "return 1" "e0e1f9fabfc9d4800c877a703b823ac0578ff8db" 127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0 (integer) 1 127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0 (integer) 1 127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0 (integer) 1 127.0.0.1:6379>
通過(guò)EVALSHA 命令執(zhí)行已經(jīng)緩存到內(nèi)存中的lua腳本時(shí),第一個(gè)參數(shù)是該腳本的ID號(hào),第二個(gè)參數(shù)0表示該腳本的參數(shù)個(gè)數(shù)是0。
可以通過(guò)SCRIPT FLUSH
命令清空緩存里的所有l(wèi)ua腳本。
SCRIPT FLUSH
可以通過(guò)SCRIPT KILL
命令終止正在運(yùn)行的腳本,如果當(dāng)前沒(méi)有腳本在運(yùn)行,該命令會(huì)返回錯(cuò)誤提示。
127.0.0.1:6379> SCRIPT KILL (error) NOTBUSY No scripts in execution right now.
2.5 觀察lua腳本阻塞Redis
Redis服務(wù)是單線程的,所以如果在lua腳本里代碼編寫不當(dāng),比如引入了死循環(huán),就會(huì)阻塞住當(dāng)前Redis線程,也就是說(shuō)該Redis服務(wù)器就無(wú)法在對(duì)外提供服務(wù)了。
比如運(yùn)行如下所示的eval命令,由于在腳本里引入了while死循環(huán),之后就無(wú)法繼續(xù)輸入其他Redis命令了,也就是說(shuō)當(dāng)前Redis服務(wù)被阻塞了。
eval "while true do end" 0
可以使用script kill命令結(jié)束lua腳本
127.0.0.1:6379> eval "while true do end" 0 root@c1beeb888673:/data# redis-cli 127.0.0.1:6379> get name (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. 127.0.0.1:6379> script kill OK 127.0.0.1:6379> get name "tom"
所以,在Redis里整合lua腳本時(shí)需要非常小心
- 需要確保該腳本盡量短小。
- 如果邏輯相對(duì)復(fù)雜,一定要反復(fù)測(cè)試,以確保不會(huì)因?yàn)殚L(zhǎng)時(shí)間運(yùn)行而阻塞Redis緩存服務(wù)。
三、進(jìn)階
3.1 參數(shù)傳遞
KEYS和ARGV參數(shù)
Redis在調(diào)用lua腳本時(shí),可以傳入KEYS和ARGV這兩種類型的參數(shù),它們的區(qū)別是前者表示要操作的鍵名,后者表示非鍵名參數(shù),但是這一要求并不是強(qiáng)制的,比如設(shè)置鍵值的腳本。
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 age 22
也可以寫成
EVAL "return redis.call('SET',ARGV[1],ARGV[2])" 0 age 22
雖然規(guī)則不是強(qiáng)制的,但是不遵守這樣的規(guī)則,可能會(huì)為后續(xù)帶來(lái)不必要的麻煩,比如Redis 3.0 之后支持集群功能,開(kāi)啟集群后會(huì)將鍵發(fā)布到不同的節(jié)點(diǎn)上,所以在腳本執(zhí)行前就需要知道腳本會(huì)操作那些鍵以便找到對(duì)應(yīng)的節(jié)點(diǎn),而如果腳本中的鍵名沒(méi)有使用KEYS參數(shù)傳遞則無(wú)法兼容集群。
eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1 key1 one two eval "return {KEYS[1],ARGV[1],ARGV[2]}" 2 key1 one two
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1 key1 one two 1) "key1" 2) "one" 3) "two" 127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 2 key1 one two 1) "key1" 2) "two"
在第1行運(yùn)行的腳本里,KEYS[1]
表示KEYS類型的第一個(gè)參數(shù)。ARGV[1]
和ARGV[2]
分別表示ARGV類型的第一個(gè)和第二個(gè)參數(shù),注意
,相關(guān)下標(biāo)是從1開(kāi)始的,不是從0開(kāi)始。
第1行腳本雙引號(hào)之后的1表示該腳本KEYS類型的參數(shù)是1個(gè),這里在統(tǒng)計(jì)參數(shù)個(gè)數(shù)時(shí),并不把ARGV自定義類型的參數(shù)統(tǒng)計(jì)在內(nèi),隨后的key1 ,one 和two分別按次序指向KEYS[1],ARGV[1]和ARGV[2]。
執(zhí)行該return語(yǔ)句后,輸出了KEYS[1],ARGV[1]和ARGV[2]這三個(gè)參數(shù)具體的值。
第二個(gè)腳本與第一個(gè)腳本的差異在于:表示參數(shù)的個(gè)數(shù)的值從1變成2,所以這里表示KEYS類型的參數(shù)個(gè)數(shù)有兩個(gè)。
redis-cli --eval 和eval命令
官網(wǎng)說(shuō)明:
redis-cli --eval 腳本文件 0
root@c1beeb888673:/data# cat /lua-script/script.lua return redis.call('SET',KEYS[1],ARGV[1]) root@c1beeb888673:/data# redis-cli --eval /lua-script/script.lua location:hastings:temp , 23 OK root@c1beeb888673:/data#
redis-cli --eval
不需要指定keys的數(shù)量,并且keys和argv之間使用,
分隔,同時(shí),兩側(cè)必須使用空格。
3.2 流程控制
分支語(yǔ)句
在lua腳本里,可以用if...else
語(yǔ)句來(lái)控制分支流程,具體語(yǔ)法如下
if (condition) then ... else ... end
注意,其中if 、then、else和end等關(guān)鍵字的寫法,在如下的ifDemo.lua腳本里將演示在lua腳本里使用分支語(yǔ)句的做法。
if redis.call('exists','name')==1 then return 'existed' else redis.call('set','name','tom') return 'not existed' end
通過(guò)if語(yǔ)句判斷redis.call
命令執(zhí)行的exists name
語(yǔ)句是否返回1,如果返回1,就表示name鍵存在,執(zhí)行第二行的return 'existed'
語(yǔ)句。否則執(zhí)行第4行和第5行的else語(yǔ)句,給name鍵設(shè)值并返回not existed
。
root@c1beeb888673:/data# redis-cli --eval /lua-script/ifDemo.lua "existed"
while循環(huán)調(diào)用
在lua腳本里,可以使用while關(guān)鍵字實(shí)現(xiàn)循環(huán)調(diào)用的效果,具體語(yǔ)法如下所示:
while (condition) do ... end
當(dāng)condition
條件為true
時(shí),會(huì)執(zhí)行do
部門的語(yǔ)句塊,否則退出該while循環(huán)語(yǔ)句。
local i=0 while(i<10) do redis.call('set',i,i) i=i+1 end
在第1行里定義了i變量,在第2行的while循環(huán)條件里會(huì)判斷i變量是否小于10,如果小于就進(jìn)入第4行執(zhí)行set操作,隨后通過(guò)第5行的代碼給i進(jìn)行加1操作并退出本次while循環(huán)。
redis-cli --eval /lua-script/while-demo.lua
for循環(huán)調(diào)用
在lua腳本里,也可以使用for關(guān)鍵字來(lái)實(shí)現(xiàn)循環(huán)的調(diào)用,具體語(yǔ)法如下
for var=start ,end,step do ... end
在執(zhí)行for循環(huán)前,首先會(huì)給var賦予start所演示的值,在執(zhí)行每次循環(huán)語(yǔ)句時(shí),會(huì)以step為步長(zhǎng)遞增start,當(dāng)遞增到end所示的值后會(huì)退出for循環(huán),實(shí)例如下
for i=0,10,1 do redis.call('del',i) end
四、springboot結(jié)合redis實(shí)現(xiàn)lua腳本的操作
4.1 springboot集成redis
- 添加Redis依賴項(xiàng)到你的pom.xml文件:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- 在application.properties或application.yml文件中配置Redis連接參數(shù):
spring.redis.host=localhost spring.redis.port=6379
使用StringRedisTemplate或RedisTemplate來(lái)執(zhí)行Lua腳本
首先我們要初始化成員變量:
//lua腳本 private DefaultRedisScript<Boolean> casScript; @Resource private RedisTemplate redisTemplate; @PostConstruct public void init(){ casScript=new DefaultRedisScript<>(); //lua腳本類型 casScript.setResultType(Boolean.class); //lua腳本在哪加載 casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua"))); }
- 使用:
public Boolean compareAndSet(String key,Long oldValue,Long newValue){ List<String> keys=new ArrayList<>(); keys.add(key); //參數(shù)一為lua腳本 //參數(shù)二為keys集合 對(duì)應(yīng)KEYS[1]、KEYS[2].... //參數(shù)三為可變長(zhǎng)參數(shù) 對(duì)應(yīng) ARGV[1]、ARGV[2]... return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue); }
如果對(duì)springboot集成redis有問(wèn)題,可以看我之前的文章SpringBoot集成Redis
4.2 使用lua腳本實(shí)現(xiàn)cas操作
初始化:
@Resource private RedisTemplate redisTemplate; //lua腳本 private DefaultRedisScript<Boolean> casScript; @PostConstruct public void init(){ casScript=new DefaultRedisScript<>(); //lua腳本類型 casScript.setResultType(Boolean.class); //lua腳本在哪加載 casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua"))); } public Boolean compareAndSet(String key,Long oldValue,Long newValue){ List<String> keys=new ArrayList<>(); keys.add(key); return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue); }
lua腳本:
local key=KEYS[1] local oldValue=ARGV[1] local newValue=ARGV[2] local redisValue=redis.call('get',key) if(redisValue==false or tonumber(redisValue)==tonumber(oldValue)) then redis.call('set',key,newValue) return true else return false end
使用:
public Boolean compareAndSet(String key,Long oldValue,Long newValue){ List<String> keys=new ArrayList<>(); keys.add(key); return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue); }
4.3 Redis整合lua腳本實(shí)例
基于Redis的lua腳本能確保Redis命令執(zhí)行時(shí)的順序性和原子性,所以在高并發(fā)的場(chǎng)景里會(huì)用兩者整合的方法實(shí)現(xiàn)限流和防超賣等效果。
限流
限流是指某應(yīng)用模塊需要限制指定IP(或指定模塊,指定應(yīng)用)在單位時(shí)間內(nèi)的訪問(wèn)次數(shù)。
這里將給出用lua腳本實(shí)現(xiàn)的基于計(jì)數(shù)模式的限流效果,示例如下
編寫lua腳本
local obj=KEYS[1] local limitNum=tonumber(ARGV[1]) local curVisitNum=tonumber(redis.call('get',obj) or '0') if(limitNum == curVisitNum) then return 0 else redis.call('incrby',obj,'1') redis.call('expire',obj,ARGV[2]) return curVisitNum+1 end
該腳本有三個(gè)參數(shù):
- KEYS[1]用來(lái)接收待限流的對(duì)象
- ARGV[1]表示限流的次數(shù)
- ARGV[2]表示限流的時(shí)間單位
該腳本的功能是限制KEYS[1]對(duì)象在ARGV[2]時(shí)間范圍內(nèi)只能訪問(wèn)ARGV[1]次。
- 首先用KEYS[1]接收待限流的對(duì)象,比如模塊或應(yīng)用等,并把它賦給obj變量。
- 用ARGV[1]參數(shù)接收到的表示限流次數(shù)的對(duì)象賦給limitNum ,注意這里需要用tonumber方法把包含限流次數(shù)的ARGV[1]參數(shù)轉(zhuǎn)換成數(shù)值類型。
- 通過(guò)if語(yǔ)句判斷待限流對(duì)象的訪問(wèn)次數(shù)是否達(dá)到限流標(biāo)準(zhǔn),如果達(dá)到,則返回0。
- 如果沒(méi)有達(dá)到限流標(biāo)準(zhǔn),首先通過(guò)incrby命令對(duì)訪問(wèn)次數(shù)加1,然后通過(guò)expire命令設(shè)置表示訪問(wèn)次數(shù)的鍵值對(duì)的生存時(shí)間,即限流的時(shí)間范圍,最后通過(guò)return語(yǔ)句返回當(dāng)前對(duì)象的訪問(wèn)次數(shù)。
- 在調(diào)用該lua腳本時(shí),如果返回值是0,就說(shuō)明當(dāng)前訪問(wèn)量已經(jīng)達(dá)到限流標(biāo)準(zhǔn),否則還可以繼續(xù)訪問(wèn)。
private DefaultRedisScript<Long> limitScript; @PostConstruct public void init(){ limitScript=new DefaultRedisScript<>(); //lua腳本類型 limitScript.setResultType(Long.class); //lua腳本在哪加載 limitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua"))); } @GetMapping("testLimit") public String testLimit(){ Boolean aBoolean = canVisit("limit", 3, 10); if (aBoolean){ return "可以訪問(wèn)"; }else { return "不可以訪問(wèn)"; } } public Boolean canVisit(String key,int oldValue,int newValue){ List<String> keys=new ArrayList<>(); keys.add(key); Long execute = (Long)redisTemplate.execute(limitScript, keys, oldValue , newValue); System.out.println(execute); return !(0==execute); }
到此這篇關(guān)于Redis整合Lua腳本的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis整合Lua腳本內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis中使用java腳本實(shí)現(xiàn)分布式鎖
這篇文章主要介紹了redis中使用java腳本實(shí)現(xiàn)分布式鎖,本文同時(shí)講解了java腳本和lua腳本實(shí)現(xiàn)分布式鎖,需要的朋友可以參考下2015-01-01使用Redis實(shí)現(xiàn)請(qǐng)求限制與速率限制
API速率限制(Rate Limiting)是控制用戶訪問(wèn)API的請(qǐng)求速率的一種機(jī)制,防止系統(tǒng)被過(guò)多請(qǐng)求淹沒(méi),下面我們來(lái)看看如何使用Redis和FastAPI實(shí)現(xiàn)請(qǐng)求限制與速率控制吧2025-04-04Redis下載部署并加入idea應(yīng)用的小結(jié)
這篇文章主要介紹了Redis下載部署并加入idea應(yīng)用,需要的朋友可以參考下2022-10-10完美解決linux上啟動(dòng)redis后配置文件未生效的問(wèn)題
今天小編就為大家分享一篇完美解決linux上啟動(dòng)redis后配置文件未生效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05通過(guò)redis的腳本lua如何實(shí)現(xiàn)搶紅包功能
這篇文章主要給大家介紹了關(guān)于通過(guò)redis的腳本lua如何實(shí)現(xiàn)搶紅包功能的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05詳解Redis用鏈表實(shí)現(xiàn)消息隊(duì)列
Redis有兩種方式實(shí)現(xiàn)消息隊(duì)列,一種是用Redis自帶的鏈表數(shù)據(jù)結(jié)構(gòu),另一種是用Redis發(fā)布/訂閱模式實(shí)現(xiàn),這篇文章先介紹鏈表實(shí)現(xiàn)消息隊(duì)列,有需要的朋友們可以參考借鑒。2016-09-09