lua脚本在redis中的使用

  1. 先开启redis的日志输出

    修改redis.conf文件,设置logfile /root/tools/redis-6.0.9/logs/redis.log
    image-20230304130756593

  2. 重启redis
    systemctl restart redisd

  3. 创建一个简单的lua脚本test.lua

    --在redis日志文件中输入日志,并且日志级别是redis.LOG_NOTICE
    redis.log(redis.LOG_NOTICE,"测试打印控制台")
    
    return "123123123"
    
  4. 执行脚本

     ../redis-cli --eval test.lua
    

    返回值
    image-20230304131740897

​ 日志文件的控制台输入
image-20230304131808881

  1. 修改test.lua

    --相当于 set key1 value1
    redis.call("set","key1","value1")
    
    --相当于 get key1,然后把结果赋值给 var1
    local var1 = redis.call("get","key1")
    
    --返回var1
    return var1;
    

    执行结果
    image-20230304132425102
    查看redis中的 键多了一个名为key1的键
    image-20230304132533402

  2. redis.call 和 redis.pcall的区别

    • redis.call 如果遇到单挑命令错误,会中断整个脚本的执行,已经执行的不会回退

      --这句正常执行
      redis.call("set","key1","value2")
      --call  执行set1 这种非法的命令出错以后,直接抛出异常
      redis.call("set1","key1","value1")
      
      --相当于 get key1,然后把结果赋值给 var1
      local var1 = redis.call("get","key1")
      
      --返回var1
      return var1;
      

      正常返回:

      值已经被修改:

      image-20230304134804579

    • redis.pcall 如果遇到单条语句失败,会继续执行完整个脚本,已经执行的不会回退

      --这句正常执行
      redis.call("set","key1","value2")
      
      --pcall  执行set1 这种非法的命令出错以后,后面的命令依旧会执行(set1 是非法的命令)
      redis.pcall("set1","key1","value1")
      
      --相当于 get key1,然后把结果赋值给 var1
      local var1 = redis.call("get","key1")
      
      --返回var1
      return var1;
      

      返回结果:

      值已经被修改:
      image-20230304135901943

  3. lua 脚本的注释

    • 单行注释

      --这里是注释
      
    • 多行注释

      --[[
      
      		这里是多行注释
      
      		这里是注释的第二行
      
       ]]
      
  4. 参数的传入

    • 传入参数:redis-cli --eval 脚本名字 键1 键2 , 值1 值2 注意键和值中间有个逗号,键和值任意多个,并且可以不对称
      ../redis-cli --eval test.lua k1 k2 , v1 v2

    • 上面命令的键和值没有对应关系,可以把他们看出两种类型的参数,或者是会放到两个数组里面的参数,如果我们需要传入类似map这种,可以用参数KEYS 和 ARGV下标的对应关系老实现。

    • 使用参数,KEYS获取key的数组,ARGV获取值的数据,可以通过下标获取具体那个参数

      redis.log(redis.LOG_NOTICE,cjson.encode(KEYS))
      redis.log(redis.LOG_NOTICE,cjson.encode(ARGV))
      
      
      redis.log(redis.LOG_NOTICE,cjson.encode(KEYS[1]))
      redis.log(redis.LOG_NOTICE,cjson.encode(ARGV[1]))
      

      参数打印
      image-20230304143139817

  5. redis内置的lua库函数

    • redis.call 以会中断脚本执行的方式执行redis命令
    • redis.pcall 以不中断脚本执行的方式执行redis命名
    • cjson.encode 把lua的table装换成json字符串。
    • cjson.decode 把json字符串转成 lua脚本的 table 对象。
    • cmspack.pack 把table序列化成字符串
    • cmspack.unpack 把字符串还原成表
  6. 参数类型对应

    • redis的数字-->lua的数字

    • redis 的字符串-->lua的字符串

    • redis的多行字符串-->lua的表

    • redis执行状态返回值-->lua的表(ok,或者err)
      返回状态table是这个样子

      ​ {"ok":"OK"}
      ​ {"err":"@user_script: 4: Unknown Redis command called from Lua script"}
      如果直接 return table,返回的是value部分。
      image-20230304141748016

  7. 脚本加入缓存,然后执行

    • 需要先进入redis-cli才能执行,不能在linux的命令行执行,只能导入文本的脚本,不能是文件

    • 导入脚本
      script load "脚本内容"
      image-20230304145952165

    • 检查脚本是否存在
      script exists "30dc9ef2a8d563dced9a243ecd0cc449c2ea0144"
      image-20230304150243237

    • 执行脚本,需要指定 0 个参数
      evalsha "30dc9ef2a8d563dced9a243ecd0cc449c2ea0144" 0
      image-20230304150449548

    • 清空缓存的脚本
      script flush

  8. lua脚本长时间执行或者死循环问题

    • 当执行时间大于lua-time-limit以后,redis 会接受io请求,这时候在命令行执行脚本会有redis繁忙的提示。
      image-20230304224651729

    • lua-time-limit建议一定要设置,0或者负数表示没有限制。

      默认5秒
      image-20230304224752104

      执行多5秒的脚本日志会有提示
      image-20230304224618363

    • 如果这时候正在运行的lua脚本没有执行过修改操作,那么可以通过 script kill 杀掉正常执行的脚本。
      image-20230304225055689

    • 如果已经有修改操作,redis认为有任务在执行,是不让直接杀掉的。只能强杀redis 服务,并且这样似乎并不会打破原子性。但是可能会丢失之前的数据,所以我们可以考虑在执行复杂脚本前先存盘,避免出死循环强杀丢数据的问题。

      kill -9 pid
      或者客户端执行 SHUTDOWN NOSAVE

    • 强杀的结果是相当于这条指令没没做过,但是强杀可能导致前面没存盘的数据流失,redis可能返回成功但是还没持久化,但是不会没有返回成功就把脚本的一般份中间结果持久化了。

  9. redis 的lua 脚本的原子性建立在单线程按序执行的基础上,或者说redis lua脚本只是保证了按序执行,不被插队,并没有保证原子性,配合单线程执行命令才能保证原子性。

  10. redis 的lua脚本中的操作没有隔离性。但是因为它是单线程执行命令的,别的线程进不来,没法读取到中间数据,所以表现的有隔离性。如果这时候即便强杀了,即便持久化方式是aof,并且开启了每次命令都持久,命令没执行完也不会持久化,前面执行的一半在内存中丢失了,所以效果和没执行一样。redis可能返回成功但是还没持久化,但是不会没有返回成功就把脚本的一般份中间结果持久化了。

  11. redis 里面的 lua 是在沙箱中运行的,不能使用全局变量,不能调用操作系统和文件等资源,只能对redis内部数据做修改。redis也对随机做了处理,2次脚本执行,脚本里面的第N次获取的随机数总是一样的。需要要不一样,需要通过随机数种子来实现。

    下面的代码每次执行的结果都是一样的,只有设置不同 种子才会有不一样的随机数。

    --设置随机数种子
    math.randomseed(1)
    
    local arr = {}
    arr[1] = math.random(1,10)
    arr[2] = math.random(1,10)
    arr[3] = math.random(1,10)
    arr[4] = math.random(1,10)
    
    return arr
    
    

    红色箭头是不设置种子,绿色箭头是设置种子以后执行
    image-20230305001147536

  12. 在java程序中使用lua脚本
    配置按照string类型序列化redisTemplate

        /**
         *
         * redisTemplateString 用于处理字符串格式,或者不带类型的json字符串
         *
         * @param redisConnectionFactory
         * @return
         */
        @Bean
        public RedisTemplate<String, String> redisTemplateString(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
    
            
            //相当于key,value都使用string类型序列化
            redisTemplate.setDefaultSerializer( new StringRedisSerializer() );
            return redisTemplate;
        }
    
    

    lua脚本

    --相当于 set key1 value1
    redis.call("set", KEYS[1] , ARGV[1] );
    redis.call("set", KEYS[2] , ARGV[2] );
    
    --相当于 get key1,然后把结果赋值给 var1
    local value1 = redis.call("get",KEYS[1]);
    local value2 = redis.call("get",KEYS[2]);
    
    local data = {};
    data.data1 = value1;
    data.data2 = value2;
    
    --返回json字符串
    return cjson.encode( data );
    
    --如果java那边用的带有类型的json需要在返回值两边加上字符串标记。
    --return "\"" .. "asdasdasd" .. "\"";
    

    使用java程序调用lua脚本

    	@Resource
    	RedisTemplate<String, String> redisTemplateString;
    
    	@ApiOperation(value = "调用lua脚本")
    	@RequestMapping(value="lua/t1", method= {RequestMethod.GET})
    	public Object t2() throws Exception{
    		DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
    		redisScript.setResultType(String.class);
    
    		redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/test.lua")));
    		//RedisScript<Map> redisScript = RedisScript.of(new ClassPathResource("lua\\test.lua"));
    
    		List<String> keys = new ArrayList<>();
    
    
    		keys.add("k1");
    		keys.add("k2");
    
    
    		Object rt = redisTemplateString.execute(redisScript, keys, "v1","v2");
    		System.out.println( rt.getClass() );
    
    		return rt;
    	}
    

    需要注意的是如果是带有java类型的json序列化方式和 不带类型的json序列化方式不兼容的。

    如果是返回string类型并且加了"内容",是可以兼容的。

    lua脚本返回值使用的是value类型的序列化方式。

posted on 2023-03-04 20:20  zhangyukun  阅读(579)  评论(0编辑  收藏  举报

导航