Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis整合Lua脚本

Redis整合Lua脚本的实现操作

作者:qq_39093474

Redis对lua脚本的支持是从Redis2.6.0版本开始引入的,它可以让用户在Redis服务器内置的Lua解释器中执行指定的lua脚本,本文就来介绍一下Redis整合Lua脚本的实现,感兴趣的可以了解一下

一、Lua介绍

1.1 Lua特点

二、在Redis里调用lua脚本

2.1 redis-cli 命令执行脚本

lua脚本是一种解释语言,所以可以安装解释器以后再运行lua脚本,但这里是在redis里引入lua脚本,所以就将给出redis-cli 命令运行lua脚本的相关步骤。

创建/opt/lua目录,在其中创建redis-demo.lua文件。注意,lua脚本的文件扩展名一般都是.lua

redis.call('set','name','zhangsan')

在lua脚本里,可以通过redis.call方法调用Redis的命令。

通过如下的docker命令创建一个名为redis-lua的Redis容器,在其中通过-v参数把包含lua脚本的/opt/lua目录映射为容器里的/lua-script目录。这样启动后该容器的/lua-script目录里就能看到在外部操作系统里创建的lua脚本。

docker run -itd --name redis-lua -v /opt/lua:/lua-script -p 6379:6379 redis:latest

启动该容器后,可以通过如下的命令进入该容器的命令行窗口里

docker exec -it redis-lua /bin/bash

可以通过如下的redis-cli命令执行刚才创建的lua脚本,其中--eval是redis里执行lua脚本的命令,/lua-script/redis-demo.lua则表示该脚本的路径和文件名。

redis-cli --eval /lua-script/redis-demo.lua

运行上述命令后,得到的返回值是空(nil),这是因为该lua脚本没有通过return返回值。

如果用redis-cli 命令进入该Redis服务器,在通过get name命令就能看到通过上述lua脚本设置到缓存的那么值。

root@c1beeb888673:/data# redis-cli --eval /lua-script/redis-demo.lua
(nil)
root@c1beeb888673:/data# redis-cli 
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> 

2.2 eval命令执行脚本

在实际项目里,如果lua脚本里包含的语句较多,那么一般会以lua脚本文件的方式来维护。

如果lua脚本里的语句很少,那么可以直接通过eval命令来执行脚本。

通过redis-cli命令进入Redis服务器的客户端里,随后运行如下的eval命令

EVAL 脚本内容 key参数的数量 [key...] [args...]
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1  age 22

通过keyarg这两类参数向脚本传递数据,他们的值可以在脚本中分别使用KEYSARGV两个表类型的全局变量访问。

key参数的数量是必须要指定的,没有key参数时必须设为0,EVAL会依据这个数值将传入的参数分别传入KEYSARGV两个表类型的全局变量。

2.3 return返回脚本运行结果

在刚才redis整合lua脚本的场景里,都是通过redis.call方法执行redis命令,并没有返回结果,在一些场景里,需要返回结果,此时就需要在脚本里引入return语句。

/opt/lua目录,在其中创建return-lua.lua文件。在其中加入如下的一句return代码。返回1这个结果。

return redis.call('set','name','tom')

进入该容器的命令窗口,在其中在运行命令,就能看到返回结果。

redis-cli --eval /lua-script/return-lua.lua

2.4 Redis和lua相关的命令

可以通过SCRIOPT LOAD命令事先装置脚本,随后可以用EVALSHA命令多次运行该脚本。

SCRIOPT LOAD '脚本内容'
EVALSHA 'id' 0
127.0.0.1:6379> SCRIPT LOAD "return 1"
"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0
(integer) 1
127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0
(integer) 1
127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0
(integer) 1
127.0.0.1:6379> 

通过EVALSHA 命令执行已经缓存到内存中的lua脚本时,第一个参数是该脚本的ID号,第二个参数0表示该脚本的参数个数是0。

可以通过SCRIPT FLUSH命令清空缓存里的所有lua脚本。

SCRIPT  FLUSH

可以通过SCRIPT KILL命令终止正在运行的脚本,如果当前没有脚本在运行,该命令会返回错误提示。

127.0.0.1:6379> SCRIPT KILL
(error) NOTBUSY No scripts in execution right now.

2.5 观察lua脚本阻塞Redis

Redis服务是单线程的,所以如果在lua脚本里代码编写不当,比如引入了死循环,就会阻塞住当前Redis线程,也就是说该Redis服务器就无法在对外提供服务了。

比如运行如下所示的eval命令,由于在脚本里引入了while死循环,之后就无法继续输入其他Redis命令了,也就是说当前Redis服务被阻塞了。

eval "while true do end" 0

可以使用script kill命令结束lua脚本

127.0.0.1:6379> eval "while true do end" 0
root@c1beeb888673:/data# redis-cli 
127.0.0.1:6379> get name
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
127.0.0.1:6379> script kill
OK
127.0.0.1:6379> get name
"tom"

所以,在Redis里整合lua脚本时需要非常小心

三、进阶

3.1 参数传递

KEYS和ARGV参数

Redis在调用lua脚本时,可以传入KEYS和ARGV这两种类型的参数,它们的区别是前者表示要操作的键名,后者表示非键名参数,但是这一要求并不是强制的,比如设置键值的脚本。

EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1  age 22

也可以写成

EVAL "return redis.call('SET',ARGV[1],ARGV[2])" 0  age 22

虽然规则不是强制的,但是不遵守这样的规则,可能会为后续带来不必要的麻烦,比如Redis 3.0 之后支持集群功能,开启集群后会将键发布到不同的节点上,所以在脚本执行前就需要知道脚本会操作那些键以便找到对应的节点,而如果脚本中的键名没有使用KEYS参数传递则无法兼容集群。

eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1  key1 one two
eval "return {KEYS[1],ARGV[1],ARGV[2]}" 2  key1 one two
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1  key1 one two
1) "key1"
2) "one"
3) "two"
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 2  key1 one two
1) "key1"
2) "two"

在第1行运行的脚本里,KEYS[1]表示KEYS类型的第一个参数。ARGV[1]ARGV[2]分别表示ARGV类型的第一个和第二个参数,注意,相关下标是从1开始的,不是从0开始。

第1行脚本双引号之后的1表示该脚本KEYS类型的参数是1个,这里在统计参数个数时,并不把ARGV自定义类型的参数统计在内,随后的key1 ,one 和two分别按次序指向KEYS[1],ARGV[1]和ARGV[2]。

执行该return语句后,输出了KEYS[1],ARGV[1]和ARGV[2]这三个参数具体的值。

第二个脚本与第一个脚本的差异在于:表示参数的个数的值从1变成2,所以这里表示KEYS类型的参数个数有两个。

redis-cli --eval 和eval命令

官网说明:

redis-cli  --eval 脚本文件 0
root@c1beeb888673:/data# cat /lua-script/script.lua 
return redis.call('SET',KEYS[1],ARGV[1])
root@c1beeb888673:/data# redis-cli --eval /lua-script/script.lua location:hastings:temp , 23
OK
root@c1beeb888673:/data# 

redis-cli --eval 不需要指定keys的数量,并且keys和argv之间使用,分隔,同时,两侧必须使用空格

3.2 流程控制

分支语句

在lua脚本里,可以用if...else语句来控制分支流程,具体语法如下

if (condition) then
	...
else
	...
end

注意,其中if 、then、else和end等关键字的写法,在如下的ifDemo.lua脚本里将演示在lua脚本里使用分支语句的做法。

if redis.call('exists','name')==1 then
	return 'existed'
else
	redis.call('set','name','tom')
	return 'not existed'
end

通过if语句判断redis.call命令执行的exists name语句是否返回1,如果返回1,就表示name键存在,执行第二行的return 'existed' 语句。否则执行第4行和第5行的else语句,给name键设值并返回not existed

root@c1beeb888673:/data# redis-cli --eval /lua-script/ifDemo.lua 
"existed"

while循环调用

在lua脚本里,可以使用while关键字实现循环调用的效果,具体语法如下所示:

while (condition)
do
	...
end

condition条件为true时,会执行do部门的语句块,否则退出该while循环语句。

local i=0
while(i<10)
do
 	redis.call('set',i,i)
 	i=i+1
end

在第1行里定义了i变量,在第2行的while循环条件里会判断i变量是否小于10,如果小于就进入第4行执行set操作,随后通过第5行的代码给i进行加1操作并退出本次while循环。

redis-cli --eval /lua-script/while-demo.lua

for循环调用

在lua脚本里,也可以使用for关键字来实现循环的调用,具体语法如下

for var=start ,end,step do
	...
end

在执行for循环前,首先会给var赋予start所演示的值,在执行每次循环语句时,会以step为步长递增start,当递增到end所示的值后会退出for循环,实例如下

for i=0,10,1 do
	redis.call('del',i)
end

四、springboot结合redis实现lua脚本的操作

4.1 springboot集成redis

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>
spring.redis.host=localhost  
spring.redis.port=6379
  //lua脚本
    private DefaultRedisScript<Boolean> casScript;
 
    @Resource
    private RedisTemplate redisTemplate;
    @PostConstruct
    public void init(){
        casScript=new DefaultRedisScript<>();
        //lua脚本类型
        casScript.setResultType(Boolean.class);
        //lua脚本在哪加载
        casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));
    }
    public Boolean compareAndSet(String key,Long oldValue,Long newValue){
        List<String> keys=new ArrayList<>();
        keys.add(key);
        //参数一为lua脚本   
        //参数二为keys集合    对应KEYS[1]、KEYS[2]....
        //参数三为可变长参数  对应 ARGV[1]、ARGV[2]...
        return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue);
    }

如果对springboot集成redis有问题,可以看我之前的文章SpringBoot集成Redis

4.2 使用lua脚本实现cas操作

初始化:

 @Resource
    private RedisTemplate redisTemplate;
    //lua脚本
    private DefaultRedisScript<Boolean> casScript;
    @PostConstruct
    public void init(){
        casScript=new DefaultRedisScript<>();
        //lua脚本类型
        casScript.setResultType(Boolean.class);
        //lua脚本在哪加载
        casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));
    }
    public Boolean compareAndSet(String key,Long oldValue,Long newValue){
        List<String> keys=new ArrayList<>();
        keys.add(key);
        return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue);
    }

lua脚本:

local key=KEYS[1]
local oldValue=ARGV[1]
local newValue=ARGV[2]
local redisValue=redis.call('get',key)
if(redisValue==false or tonumber(redisValue)==tonumber(oldValue))
then
    redis.call('set',key,newValue)
    return true
else
    return false
end

使用:

  public Boolean compareAndSet(String key,Long oldValue,Long newValue){
        List<String> keys=new ArrayList<>();
        keys.add(key);
        return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue);
    }

4.3 Redis整合lua脚本实例

基于Redis的lua脚本能确保Redis命令执行时的顺序性和原子性,所以在高并发的场景里会用两者整合的方法实现限流和防超卖等效果。

限流

限流是指某应用模块需要限制指定IP(或指定模块,指定应用)在单位时间内的访问次数。

这里将给出用lua脚本实现的基于计数模式的限流效果,示例如下

编写lua脚本

local obj=KEYS[1]
local limitNum=tonumber(ARGV[1])
local curVisitNum=tonumber(redis.call('get',obj) or '0')
if(limitNum == curVisitNum) then
	return 0
else
	redis.call('incrby',obj,'1')
	redis.call('expire',obj,ARGV[2])
	return curVisitNum+1
end

该脚本有三个参数:

该脚本的功能是限制KEYS[1]对象在ARGV[2]时间范围内只能访问ARGV[1]次。

  private DefaultRedisScript<Long> limitScript;

    @PostConstruct
    public void init(){
        limitScript=new DefaultRedisScript<>();
        //lua脚本类型
        limitScript.setResultType(Long.class);
        //lua脚本在哪加载
        limitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
    }


@GetMapping("testLimit")
    public String testLimit(){
        Boolean aBoolean = canVisit("limit", 3, 10);

        if (aBoolean){
            return "可以访问";
        }else {
            return "不可以访问";
        }

    }


    public Boolean canVisit(String key,int oldValue,int newValue){
        List<String> keys=new ArrayList<>();
        keys.add(key);
        Long execute = (Long)redisTemplate.execute(limitScript, keys, oldValue , newValue);
        System.out.println(execute);
        return !(0==execute);
    }

到此这篇关于Redis整合Lua脚本的实现的文章就介绍到这了,更多相关Redis整合Lua脚本内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文