背景介紹
Redis在2.6推出了腳本功能,允許開發者使用Lua語言編寫腳本傳到Redis中執行。使用腳本的好處如下:
1.減少網絡開銷:本來5次網絡請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis服務器上完成。使用腳本,減少了網絡往返時延。
2.原子操作:Redis會將整個腳本作為一個整體執行,中間不會被其他命令插入(java等客戶端則會執行多次命令完成一個業務,違反了原子性操作)。
3.復用:客戶端發送的腳本會永久存儲在Redis中,意味着其他客戶端可以復用這一腳本而不需要使用代碼完成同樣的邏輯。
redis在服務器端內置lua解釋器(版本2.6以上)
redis-cli提供了EVAL與EVALSHA命令執行Lua腳本:
redis內置lua執行命令
EVAL命令語法
EVAL script numkeys key [key …] arg [arg …]
EVAL —lua程序的運行環境上下文
script —lua腳本
numkeys —參數的個數(key的個數)
key —redis鍵 訪問下標從1開始,例如:KEYS[1]
arg —redis鍵的附加參數
EVALSHA 命令語法
EVALSHA SHA1 numkeys key [key …] arg [arg …]
EVALSHA命令允許通過腳本的SHA1來執行(節省帶寬)
Redis在執行EVAL/SCRIPT LOAD后會計算腳本SHA1緩存, EVALSHA根據SHA1取出緩存腳本執行.
Redis中管理Lua腳本
script load script 將Lua腳本加載到Redis內存中(如果redis重啟則會丟失)
script exists sh1 [sha1 …] 判斷sha1腳本是否在內存中
script flush 清空Redis內存中所有的Lua腳本
script kill 殺死正在執行的Lua腳本。(如果此時Lua腳本正在執行寫操作,那么script kill將不會生效)
Redis提供了一個lua-time-limit參數,默認5秒,它是Lua腳本的超時時間,如果Lua腳本超時,其他執行正常命令的客戶端會收到“Busy Redis is busy running a script”錯誤,但是不會停止腳本運行,此時可以使用script kill 殺死正在執行的Lua腳本。
lua函數
主要有兩個函數來執行redis命令
redis.call() – 出錯時返回具體錯誤信息,並且終止腳本執行
redis.pcall() –出錯時返回lua table的包裝錯誤,但不引發錯誤
使用流程如下:
1.編寫腳本
2.腳本提交到REDIS並獲取SHA
3.使用SHA調用redis腳本
環境准備
win10中的bash做的實驗, 可以下載
luaforwindows
github/rjpcomputing/luaforwindows/releases
redis-windows
github/ServiceStack/redis-windows
redis運行lua腳本
EVAL 直接運行腳本
127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
EVALSHA使用
需要SCRIPT LOAD和EVALSHA配合使用
1.SCRIPT LOAD加載到內存,返回SHA簽名
2.EVALSHA使用已經存在的簽名
這樣只用加載一次,便可重復使用已經加載的簽名腳本,可以多次使用,避免長腳本輸入
127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"a42059b356c875f0717db19a51f6aaca9ae659ea"
127.0.0.1:6379> EVALSHA "a42059b356c875f0717db19a51f6aaca9ae659ea" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
在redis下使用腳本文件執行
在路徑下創建腳本文件,這里直接在redis/bin下創建,方便使用,其他目錄可以使用全路徑
set.lua
--[[ set.lua, redis的set命令使用
redis: set key val
--]]
local key=KEYS[1]
local val=ARGV[1]
return redis.call('set', key, val)
get.lua
--[[ get.lua, redis的get命令使用
redis: get key
--]]
local key=KEYS[1]
local val=redis.call("GET", key);
return val;
保存兩個文件到redis/bin目錄下,執行如下命令
設置k-v值
redis-cli --eval set.lua foo , bar
通過redis-cli查看值
127.0.0.1:6379> get foo
"bar"
獲取k值
redis-cli --eval get.lua foo
"bar"
可以看到, 可以用lua腳本操作redis數據。
注意: redis-cli --eval set.lua foo , bar, foo和bar之間的逗號左右都有空格分隔,否則會當做一個字符串
通常做法是
1.腳本文件保存到一個路徑下或者數據庫中,/mnt/redis/lua/set.lua
2.SCRIPT LOAD 加載腳本文件內容,返回SHA簽名保存到應對的值K-V值,(set,SHA)
3.獲取對應腳本名稱的SHA簽名,如果存在則執行,否則進行第二部操作
訪問次數限制
ratelimiting.lua
local times=redis.call('incr',KEYS[1])
if times==1 then
redis.call('expire',KEYS[1], ARGV[1])
end
if times > tonumber(ARGV[2]) then
return 0
end
return 1
運行腳本
redis-cli --eval ratelimiting.lua rate.limitingl:127.0.0.1 , 10 3
rate.limitingl:127.0.0.1是前綴+ip組成的KEY,用KEYS[1]獲取,
”,”后面的10和3是參數,在腳本中能夠使用ARGV[1]和ARGV[2]獲得
命令的作用是將訪問頻率限制為每10秒最多3次,所以在終端中不斷的運行此命令會發現當訪問頻率在10秒內小於或等於3次時返回1,否則返回0。
lua腳本實現redis分布式鎖
setnx 如果key不存在則添加值並返回1,如果已經存在key則返回0
加鎖
使用業務setnx(key,業務流水號)當加鎖成功返回1時設置過期時間,避免業務異常沒有解鎖時防止死鎖
當同一業務再次申請鎖時,如果隨機值相同 則認為是重試,則直接設置過期時長;如果隨機值不同則直接返回0,獲取鎖失敗
解鎖
業務完成直接del(key)完成
以上方案是很多客戶端實現的方式,建立和釋放鎖,並保證絕對的安全,是這個鎖的設計比較棘手的地方。有兩個潛在的陷阱:
1.應用程序通過網絡和redis交互,這意味着從應用程序發出命令到redis結果返回之間會有延遲。這段時間內,redis可能正在運行其他的命令,而redis內數據的狀態可能不是你的程序所期待的。如果保證程序中獲取鎖的線程和其他線程不發生沖突?
2.如果程序在獲取鎖后突然crash,而無法釋放它?這個鎖會一直存在而導致程序進入“死鎖”
對於第一個問題,除了pile批量一次執行,目前只有lua腳本是在同一個線程中一次執行完的。
第二個問題,如果在獲取鎖之后,設置expire之前發生了異常,那么這個key-v永遠都不會過期,即便是lua腳本也是一樣會發生這樣的情況(通常是設置過期時間這個參數設置的不是數字類型,雖然這種情況不太可能發生),但仍然比客戶端多條命令執行來的更加簡短
lock.lua
-- Set a lock
-- 如果獲取鎖成功,則返回 1
local key=KEYS[1]
local content=ARGV[1]
local ttl=tonumber(ARGV[2])
local lockSet=redis.call('setnx', key, content)
if lockSet==1 then
redis.call('PEXPIRE', key, ttl)
else
-- 如果value相同,則認為是同一個線程的請求,則認為重入鎖
local value=redis.call('get', key)
if(value==content) then
lockSet=1;
redis.call('PEXPIRE', key, ttl)
end
end
return lockSet
unlock.lua
-- unlock key
local key=KEYS[1]
local content=ARGV[1]
local value=redis.call('get', key)
if value==content then
return redis.call('del', key)
else
return 0
end
測試加鎖和解鎖
redis-cli --eval lock.lua lo3 , 2 60000
redis-cli --eval unlock.lua lo3 , 2
在java代碼中我們可以使用AOP獲取當前業務的key,業務主鍵實現加鎖,如果一旦業務異常 那么在超時后自動解鎖
java調用腳本操作redis
直接使用腳本執行
private boolean accessLimit(String ip, int limit, int timeout, Jedis connection) throws IOException {
List keys=Collections.singletonList(ip);
List argv=Arrays.asList(String.valueOf(limit), String.valueOf(timeout));
return 1==(long) connection.eval(loadScriptString("script.lua"), keys, argv);
}
// 加載Lua代碼
private String loadScriptString(String fileName) throws IOException {
Reader reader=new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName));
return CharStreams.toString(reader);
}
使用SHA執行
內容轉載自:
作者:菜鳥-翡青
原文:
blog.csdn/zjf280441589/article/details/52716720
腳本工具
/**
* @author jifang
* @since 16/8/25 下午3:35.
*/
public class ScriptCaller {
private static final ConcurrentMap<String, String> SHA_CACHE=new ConcurrentHashMap<>();
private String script;
private ScriptCaller(String script) {
this.script=script;
}
public static ScriptCaller getInstance(String script) {
return new ScriptCaller(script);
}
public Object call(Jedis connection, List keys, List argv, boolean forceEval) {
if (!forceEval) {
String sha=SHA_CACHE.get(this.script);
if (Strings.isNullOrEmpty(sha)) {
// load 腳本得到 sha1 緩存
sha=connection.scriptLoad(this.script);
SHA_CACHE.put(this.script, sha);
}
return connection.evalsha(sha, keys, argv);
}
return connection.eval(script, keys, argv);
}
}
調用端:
1.提交腳本到redis獲取SHA的值
2.利用SHA的值執行腳本
public class Client {
private ScriptCaller acquireCaller=ScriptCaller.getInstance(
"local key=KEYS[1]
" +
"local identifier=ARGV[1]
" +
"local lockTimeOut=ARGV[2]
" +
"
" +
"if redis.call("SETNX", key, identifier)==1 then
" +
" redis.call("EXPIRE", key, lockTimeOut)
" +
" return 1
" +
"elseif redis.call("TTL", key)==-1 then
" +
" redis.call("EXPIRE", key, lockTimeOut)
" +
"end
" +
"return 0"
);
private ScriptCaller releaseCaller=ScriptCaller.getInstance(
"local key=KEYS[1]
" +
"local identifier=ARGV[1]
" +
"
" +
"if redis.call("GET", key)==identifier then
" +
" redis.call("DEL", key)
" +
" return 1
" +
"end
" +
"return 0"
);
@Test
public void client() {
Jedis jedis=new Jedis("127.0.0.1", 9736);
String identifier=acquireLockWithTimeOut(jedis, "ret1", 200 * 1000, 300);
System.out.println(releaseLock(jedis, "ret1", identifier));
}
String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) {
String identifier=UUID.randomUUID().toString();
List keys=Collections.singletonList("lock:" + lockName);
List argv=Arrays.asList(identifier,
String.valueOf(lockTimeOut));
long acquireTimeEnd=System.currentTimeMillis() + acquireTimeOut;
boolean acquired=false;
while (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) {
if (1==(long) acquireCaller.call(connection, keys, argv, false)) {
acquired=true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
}
}
return acquired ? identifier : null;
}
boolean releaseLock(Jedis connection, String lockName, String identifier) {
List keys=Collections.singletonList("lock:" + lockName);
List argv=Collections.singletonList(identifier);
return 1==(long) releaseCaller.call(connection, keys, argv, true);
}
}
Lua+Redis 斷點調試環境搭建
redis3.2之后內置了debug引擎,可以通過–ldb選項進入debug模式
windows環境,使用Redis,寫lua腳本頭疼的問題之一不能對腳本斷點調試,google加上自己的摸索,終於搞定。
1、下載ZeroBraneStudio,我下載的是破解版(我自己為自己感到可恥,其實並不貴,百十來塊錢的樣子)
解壓后在bin下有lua解釋器的路徑,把該路徑添加到環境變量中:假設解釋器路徑是:
D:/ZeroBraneStudio/bin/lua.exe,那么就把D:/ZeroBraneStudio/bin添加到Path環境變量下。
2、下載luaRocks,它是一個lua相關類型的維護工具包,下載地址:
github/keplerproject/luarocks/wiki/Installation-instructions-for-Windows。
下載后從cmd命令行中運行Install.bat安裝。
3、安裝redis及調試相關類庫:
打開cmd依次運行三個命令進行安裝:
luarocks install remdebug
luarocks install prtr-dump
luarocks install redis-lua
4、打開ZeroBraneStudio,建lua腳本,名字隨意,比如my.lua,添加如下內容:
local redis=require 'redis'
local host="127.0.0.1"
local port=6379
client=redis.connect(host, port)
redis.call=function(cmd, ...)
return assert(loadstring('return client:'.. string.lower(cmd) ..'(...)'))(...)
end
碼字不易看到最后了,那就點個關注唄,只收藏不點關注的都是在耍流氓!
關注並私信我“架構”,免費送一套Java架構資料,先到先得!
