redis中的lua

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 校验和,返回一个包含 01 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

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();
        }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容