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());
}
若有收获,就点个赞吧!
若有错误,欢迎留言指正~