1相关命令
1.1 EVAL
1.1.1 EVAL概念
EVAL script numkeys key [key …] arg [arg …]
script
参数是一段 Lua 5.1 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。
numkeys
参数用于指定键名参数的个数。
键名参数 key [key ...] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 ==1为基址==的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)
一个例子
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
结合最开始的定义,"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
对应的就是script
, 2对应的是numkeys
,key1 key2对应的是key [key …]
,例子中是两个key,实际不固定可以更多,但是数量要和前面的numkeys
对应,同理first second对应的就是arg [arg …]
1.1.1 脚本中执行redis命令
EVAL
后面跟的是script脚本,如果脚本中要执行redis的命令,可以使用如下两个命令
redis.call()
redis.pcall()
一个例子
> lpush foo a
1
-- redis.call()报错如下
> eval "return redis.call('get','foo')" 0
ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e38c0e791): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value
-- redis.pcall()报错如下
> EVAL "return redis.pcall('get', 'foo')" 0
WRONGTYPE Operation against a key holding the wrong kind of value
redis.call() 和 redis.pcall() 的唯一区别在于它们对错误处理的不同,使用上没啥差别
上述写法在单实例情况下在语法上是没问题的,只是命令和结构不对应,如果foo
是个字符串就不会报错,官方推荐的写法应该通过外部传递键值
eval "return redis.call('get',KEY[1])" 1 foo
这种写法兼容集群的情况
1.2 EVALSHA
EVALSHA sha1 numkeys key [key …] arg [arg …]
根据给定的 sha1 校验码,对缓存在服务器中的脚本进行求值。
将脚本缓存到服务器的操作可以通过 SCRIPT LOAD script
命令进行。
这个命令的其他地方,比如参数的传入方式,都和 eval命令一样。
--脚本加载到缓存,返回一个校验码,此时脚本并未执行
redis> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
--用指定的校验码执行对应脚本
redis> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0
"hello moto"
1.3 SCRIPT LOAD
eval
命令要求在每次执行脚本的时候都发送一次脚本主体,所以有了EVALSHA
,EVALSHA
后面跟的是 SCRIPT LOAD生成的校验码
- 如果服务器还缓存了给定的 SHA1 校验码所指定的脚本,那么执行这个脚本
- 如果服务器未缓存给定的 SHA1 校验码所指定的脚本,那么它返回一个特殊的错误,提醒用户使用
EVAL
代替EVALSHA
一个例子
--存一个字符串
> set key value
OK
--缓存一个脚本,得到一个校验码
> SCRIPT LOAD "return redis.call('get','key')"
ec185682c217800dc6301235a0f12960ad149fa7
--evalsha执行存在的脚本
> evalsha ec185682c217800dc6301235a0f12960ad149fa7 0
value
evalsha执行不存在的脚本
> evalsha 1111111111111111111111111111 0
NOSCRIPT No matching script. Please use EVAL.
1.3 SCRIPT LOAD
SCRIPT LOAD script
将脚本 script
添加到脚本缓存中,但并不立即执行这个脚本
--脚本加载到缓存,返回一个校验码
redis> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
1.4 SCRIPT EXISTS
SCRIPT EXISTS sha1 [sha1 …]
给定一个或多个脚本的 SHA1 校验和,返回一个包含 0
和 1
的列表,表示校验和所指定的脚本是否已经被保存在缓存当中
SCRIPT EXISTS 后面跟的是一个或者多个SCRIPT LOAD指令返回的sha1校验码
redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a
1) (integer) 1
1.5 SCRIPT FLUSH
清除所有 Lua 脚本缓存
-- 清空缓存
redis> SCRIPT FLUSH
OK
redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a
1) (integer) 0
1.6 SCRIPT KILL
杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。
这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。
SCRIPT KILL
执行之后,当前正在运行的脚本会被杀死,执行这个脚本的客户端会从 [EVAL script numkeys key [key …\] arg [arg …]
命令的阻塞当中退出,并收到一个错误作为返回值。
另一方面,假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL
,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用 SHUTDOWN NOSAVE
命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。
执行成功返回 OK
,否则返回一个错误
# 没有脚本在执行时
redis> SCRIPT KILL
(error) ERR No scripts in execution right now.
# 成功杀死脚本时
redis> SCRIPT KILL
OK
(1.30s)
# 尝试杀死一个已经执行过写操作的脚本,失败
redis> SCRIPT KILL
(error) ERR Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in an hard way using the SHUTDOWN NOSAVE command.
(1.69s)
lua脚本中一定不要出现死循环,因为redis是单线程的,后续所有指令将被阻塞,如下
> EVAL "while true do end" 0
--eval执行了一个死循环,后面的指令都被阻塞了,没有响应
> SCRIPT KILL
> set key value
> get key
此时可以另起一个客户端连接redis,执行SCRIPT KILL
> SCRIPT KILL
ok
>
此时原来的客户端为
> EVAL "while true do end" 0
> SCRIPT KILL
> set key value
> get key
ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL...
NOTBUSY No scripts in execution right now.
OK
value
2.实际应用
2.1一些例子
2.1.1删除dict*格式的所有key值
eval "local redisKeys = redis.call('keys',KEYS[1]..'*');for i,k in pairs(redisKeys) do redis.call('del',k);end;return redisKeys;" 1 dict
脚本展开如下
local redisKeys = redis.call('keys',KEYS[1]..'*');
for i,k in pairs(redisKeys) do
redis.call('del',k);
end;
return redisKeys;
2.1.2删除所有key值
eval "local sum = 0;for i,k in pairs(redis.call('keys','*')) do redis.call('del', k);sum=sum+1;end; return 'clear '..sum..' key'" 0
脚本展开如下
local sum = 0;
for i,k in pairs(redis.call('keys','*')) do
redis.call('del', k);
sum=sum+1;
end;
return 'clear '..sum..' key'
2.1.3删除所有值为a的key,键值参数数量0、非键值参数a
eval "local ks = {};for i,k in pairs(redis.call('keys','*')) do local v = redis.call('get',k);if v==ARGV[1] then redis.call('del',k);table.insert(ks,k); end;end;return ks;" 0 a
脚本展开
local ks = {};
for i,k in pairs(redis.call('keys','*')) do
local v = redis.call('get',k);
if v==ARGV[1] then
redis.call('del',k);
table.insert(ks,k);
end;
end;
return ks
2.1.4 redis分布式锁
如果不存在lock,则设置lock为uid,并设置过期时间为60,如果返回1表示加锁成功,返回0则加锁失败,该操作是原子操作
eval "local key=KEYS[1] local value = ARGV[1] if redis.call('SETNX',key,value) >0 then redis.call('SETEX', key,ARGV[2], value) return 0 end return 1" 1 lock uid 60
展开如下:
local key=KEYS[1]
local value = ARGV[1]
--setnx key存在时不做任何操作,key不存在则存入key
--有操作(key不存在,执行插入)则返回1,无操作(key存在)返回0
if redis.call('SETNX',key,value) >0 then
--SETEX 设置key value ,过期时间,属于原子操作,存在相同key会覆盖原key
redis.call('SETEX', key,ARGV[2], value)
return 1
end
return 0
解锁
eval "if redis.call('GET',KEYS[1]) ==ARGV[1] then redis.call('DEL', KEYS[1]) return 0 end return 1" 1 lock uid
展开如下
if redis.call('GET',KEYS[1]) ==ARGV[1] then
redis.call('DEL', KEYS[1])
return 0
end
return 1
其实就是很朴素的if条件判断,加锁就是加个key,解锁就是删除key,只是这种判断在java
里面并发下可能有问题,因为获取key是否存在
和加锁解锁
不是原子的,而调用lua
脚本则是原子的,避免了并发问题
2.2 RedissonLock中的应用
RedissonLock
中用了不少lua脚本,其中一个例子如下,可以看到和上面的规则是一样的,应该也能直接看懂了,该例子位于RedissonLock的源码中
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
展开后的脚本
--检查key是否被占用了,如果没有则设置超时时间和唯一标识,初始化value=1
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
--如果锁重入,需要判断锁的key field 都一致情况下 value 加一
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
--返回剩余的过期时间
return redis.call('pttl', KEYS[1]);
--传入脚本的参数
KEYS[1](getName()) :需要加锁的key
ARGV[1](internalLockLeaseTime) :锁的超时时间,防止死锁
ARGV[2](getLockName(threadId)) :锁的唯一标识, id + “:” + threadId
2.3 redisTemplate中使用lua
public void redisTest(){
Long result=null;
try {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//返回类型是Long
redisScript.setResultType(Long.class);
//调用lua脚本并执行,本例子是maven项目,脚本放到了resource/redisScript/luaScript.lua,
//脚本内容就是下下一行代码scriptText中内容,实际工作中,脚本位置肯定是不会放这里,而是某个指定位置,写法是差不多的
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redisScript/luaScript.lua")));
//上面的lua脚本比较简单,也可以不用 redisScript.setScriptSource, 直接用String的形式写,如下,效果是一样的
// String scriptText="if redis.call('SETNX',KEYS[1],ARGV[1]) >0 then redis.call('SETEX', KEYS[1],ARGV[2], ARGV[1]); return 1; end return 0;";
// redisScript.setScriptText(scriptText);
List<String> keyList=new ArrayList<>();
keyList.add("thiskey");
//对应EVAL script numkeys key [key …] arg [arg …]
// keyList就是key [key …],若干个key,下面的100,300对应的是若干上面arg [arg …]
result = (Long) redisTemplate.execute(redisScript, keyList, 100, 300);
System.out.println("result==" + result);
System.out.println("获取存入结果==" + redisTemplate.opsForValue().get("thiskey"));
} catch (Exception e) {
e.printStackTrace();
}
}