Redis腳本的基本認識
EVAL和EVALSHA
Redis從2.6.0版本開始,内置了lua腳本的解析執行器EVAL和EVALSHA,為避免終端相同腳本的頻繁傳輸,Redis提供了腳本緩存的機制,緩存腳本的解析對應于EVALSHA,使用SCRIPT LOAD将腳本緩存到Redis并擷取SHA1值備用。
EVAL script numkeys key [key ...] arg [arg ...]
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
EVAL參數說明:
- script: 參數是一段 Lua 5.1 腳本程式。腳本不必(也不應該)定義為一個 Lua 函數。
- numkeys: 用于指定鍵名參數的個數。
- key [key ...]: 從 EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數可以在 Lua 中通過全局變量 KEYS 數組,用 1 為基址的形式通路( KEYS[1] , KEYS[2] ,以此類推)。
- arg [arg ...]: 附加參數,在 Lua 中通過全局變量 ARGV 數組通路,通路的形式和 KEYS 變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。
EVALSHA參數說明:
- sha1: 通過 SCRIPT LOAD 生成的 sha1 校驗碼。
120.78.72.23:0>eval "if redis.call('setex',KEYS[1],100,ARGV[1]) then return 1 else return 0 end" 1 liming-key liming-arg
120.78.72.23:0>script load "if redis.call('setex',KEYS[1],100,ARGV[1]) then return 1 else return 0 end"
"7a5b676ca41522553a5d587787d56e810465380b"
120.78.72.23:0>evalsha 7a5b676ca41522553a5d587787d56e810465380b 1 liming 1
"1"
CALL和PCALL
Redis再内置的lua中提供了redis.call與redis.pcall兩個lua函數來通路redis的指令,兩者唯一的不同的地方是面對異常的處理方式,當redis指令執行出現異常時,redis.call将會抛出lua錯誤并強制eval指令傳回一個錯誤給調用者,而redis.pcall将會把錯誤資訊封裝作為結果傳回。現用redis中不存在的指令x做異常測試,表現如下:
120.78.72.23:0>eval "return redis.call('x','liming')" 0
"ERR Error running script (call to f_3e6a9c0be3d9d53955c4478529aac907cb317793): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script"
120.78.72.23:0>eval "return redis.pcall('x','liming')" 0
"@user_script: 1: Unknown Redis command called from Lua script"
腳本的原子性
Redis使用同一個Lua解釋器來執行一個腳本中的全部指令,且當一個腳本在執行時,不會有其他腳本或Redis指令同時執行,是以Redis能確定以原子方式執行腳本。從另一個角度來看,執行一個非常耗時的腳本不是一個很好的實踐,因為它會阻塞其他腳本或指令執行。當Redis中的記憶體使用超過maxmemory限制,且腳本執行過程中遇到第一個需要使用額外記憶體空間的寫指令時,會導緻腳本終止(除非使用redis.pcall),若不滿足上面條件,那麼即使後面的寫指令需要額外的記憶體空間,redis也能通過允許記憶體使用超過maxmemory限制,保證腳本的原子性。
Java實作
擷取分布式鎖
if redis.call('exists', KEYS[1]) == 0 or redis.call('get', KEYS[1]) == ARGV[1] then
if redis.call('setex', KEYS[1], tonumber(ARGV[2]), ARGV[1]) then
return 1
end
end
return 0
腳本分析
- 1:不存在指定鍵的緩存,或者指定鍵的緩存值與傳入值是一緻的(支援相同所有者的重入);
- 2:設定鍵值,并且設定過期時間,避免異常導緻鎖長期得不到釋放;
釋放分布式鎖
if redis.pcall('exists', KEYS[1]) == 1 and redis.pcall('get', KEYS[1]) == ARGV[1] then
return redis.pcall('del', KEYS[1])
end
return 0
- 1:鎖存在性校驗與擁有者校驗,遵循誰申請誰釋放原則;
- 2:删除鎖;
完整代碼
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.Collections;
/**
* 基于redis腳本實作的分布式鎖
*/
@Slf4j
public class DistributionLock {
/**
* 分布式鎖鍵的字首
*/
public static final String PREFIX_LOCK = "distribution:lock:{0}";
/**
* 申請鎖的腳本
*/
private static final String SCRIPT_LOCK = "if redis.call('exists', KEYS[1]) == 0 or redis.call('get', KEYS[1]) == ARGV[1] then if redis.call('setex', KEYS[1], tonumber(ARGV[2]), ARGV[1]) then return 1 end end return 0";
/**
* 釋放鎖的腳本
*/
private static final String RELEASE_LOCK = "if redis.call('exists', KEYS[1]) == 1 and redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) end return 0";
/**
* Redis用戶端
*/
private RedisTemplate<String, String> redisTemplate;
public DistributionLock(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 申請鎖
*
* @param key 鍵
* @param owner 所有者
* @param timeout 實作時間(秒)
* @return 成功傳回1,其他傳回0
*/
public boolean lock(String key, String owner, Integer timeout) {
log.info("distribution lock get : key:{} , owner:{} , timeout:{}", key, owner, timeout);
String redisKey = MessageFormat.format(PREFIX_LOCK, key);
return ((long) redisTemplate.execute(RedisScript.of(SCRIPT_LOCK, Long.class), Collections.singletonList(redisKey), owner, timeout)) == 1L;
}
/**
* 釋放鎖
*
* @param key 鍵
* @param owner 所有者
* @return 成功傳回1,其他傳回0
*/
public boolean release(Serializable key, Serializable owner) {
log.info("distribution lock release : key:{} , owner:{}", key, owner);
String redisKey = MessageFormat.format(PREFIX_LOCK, key);
return ((long) redisTemplate.execute(RedisScript.of(RELEASE_LOCK, Long.class), Collections.singletonList(redisKey), owner)) == 1L;
}
}
- lock(Serializable key, Serializable owner, Long timeout)中,key應該是業務相關,owner應該與鎖的所有者相關,在單執行個體的實體環境下,owner可以是線程名稱(不建議);
- lock(Serializable key, Serializable owner, Long timeout)中,timeout定義了持有鎖的失效時長,避免程式異常導緻持有鎖不能釋放;
- release(Serializable key, Serializable owner)中,輸入key與owner確定誰申請誰釋放;
示例代碼
try {
if (distributionLock.lock(key, Thread.currentThread().getName(), 60L)) {
//todo
}
} catch (Exception e) {
log.error("process error : {}", e.getMessage(), e);
} finally {
distributionLock.release(key, Thread.currentThread().getName());
}
若有收獲,就點個贊吧!
若有錯誤,歡迎留言指正~