redis 执行lua脚本

参考: https://redis.io/commands/eval

参考:https://redis.io/topics/ldb

1. redis-cli 命令行测试

命令如下:key 可以理解用于传键名称,而arg 用于传递其他参数

EVAL script numkeys key [key ...] arg [arg ...]

1. 例如

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"

  简单理解: 2 是key的个数,接下来key1、key2是key;剩下的first、second是arg参数列表

2. 放入值然后获取值

存值:

127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
OK

取值:

127.0.0.1:6379> eval "return redis.call('get','foo')" 0
"bar"

2. redis-cli 加脚本测试lua 脚本

官网解释:Starting with version 3.2 Redis includes a complete Lua debugger, that can be used in order to make the task of writing complex Redis scripts much simpler.

其命令如下:(多个key和arg用逗号隔开即可)

./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2

例如:

1. 创建脚本:Test.lua, 内容如下:

--- 获取key
local key = KEYS[1]
--- 获取value
local val = KEYS[2]
--- 获取一个参数
local expire = ARGV[1]
--- 如果redis找不到这个key就去插入
if redis.call("get", key) == false then
    --- 如果插入成功,就去设置过期值
    if redis.call("set", key, val) then
        --- 由于lua脚本接收到参数都会转为String,所以要转成数字类型才能比较
        if tonumber(expire) > 0 then
            --- 设置过期时间
            redis.call("expire", key, expire)
        end
        return true
    end
    return false
else
    return false
end

2. 命令行测试脚本:

(1) 测试部加--ldb执行命令,相当于用redis-cli 跑lua脚本

liqiang@root MINGW64 ~/Desktop/新建文件夹
$ redis-cli.exe --eval Test.lua testKey testValue , 100
1

(2) 可以调试lua脚本:

redis-cli.exe --ldb --eval Test.lua testKey testValue , 100

3. Spring boot 项目跑lua脚本

Springboot 项目跑lua脚本一般基于redisTemplate。

1. pom引入如下依赖:

        <!-- 引入 redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2. 将上面Test.lua 脚本放到resource/script目录下:

 3. 编写测试方法:

package com.xm.ggn.controller;

import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.http.ResponseEntity;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;

@Controller
public class TestController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/lua")
    public ResponseEntity lua() {
        List<String> keys = Arrays.asList("testLua", "hello lua");
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/Test.lua")));
        redisScript.setResultType(Boolean.class);
        Boolean execute = stringRedisTemplate.execute(redisScript, keys, "100");
        assert execute != null;
        return ResponseEntity.ok(execute);
    }
}

redisTemlate 最终会调用org.springframework.data.redis.core.script.DefaultScriptExecutor#execute(org.springframework.data.redis.core.script.RedisScript<T>, java.util.List<K>, java.lang.Object...) 方法

4. 访问测试:

curl http://localhost:8088/lua

补充: 也可直接用字符串构造对象

例如:

    @GetMapping("/lua")
    public ResponseEntity lua() {
        List<String> keys = Arrays.asList("testLua", "hello lua");
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>("local key = KEYS[1]; local val = KEYS[2]; local expire = ARGV[1]; redis.call(\"set\", key, val); redis.call(\"expire\", key, expire);");
        redisScript.setResultType(Boolean.class);
        Boolean execute = stringRedisTemplate.execute(redisScript, keys, "100");
        assert execute != null;
        return ResponseEntity.ok(execute);
    }

补充: 更详细的保存对象或者集合 

import com.bo.ExecutedTaskWrapperBO;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@SpringBootTest
@RunWith(SpringRunner.class)
public class DistributedLockUtilTest {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 带参数的自己的测试。 可以作为初版学习语法
     */
    @Test
    public void test0() {
        List<String> keys = Arrays.asList("testLua", "hello lua");
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>("local key = KEYS[1]; local val = KEYS[2]; local expire = ARGV[1]; redis.call(\"set\", key, val); redis.call(\"expire\", key, expire);");
        redisScript.setResultType(Boolean.class);
        Boolean execute = redisTemplate.execute(redisScript, keys, "100");
        System.out.println(execute);
    }

    /**
     * redis.call('zrem', 'zkey1', 'key1','key2'); redis.call('rpush', 'lkey1', 'key1','key2');
     */
    @Test
    public void test1() {
        String zsetKey = "zkey1";
        String llistKey = "lkey1";
        String format = String.format("redis.call('zrem', '%s', ${ZREM_KEY}); redis.call('rpush', '%s', ${RPUSH_KEY});", zsetKey, llistKey);

        List<String> keys = Lists.newArrayList("key1", "key2");
        List<String> params = new ArrayList<>();
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            params.add(String.format("'%s'", key));
        }
        String luaScript = format.replace("${ZREM_KEY}", StringUtils.join(params, ",")).replace("${RPUSH_KEY}", StringUtils.join(params, ","));
        System.out.println(luaScript);
        // redis.call('zrem', 'zkey1', 'key1','key2'); redis.call('rpush', 'lkey1', 'key1','key2');

        RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
        String execute = redisTemplate.execute(redisScript, null);

        System.out.println(execute);
    }

    /**
     * 存对象, 对象实验的序列号方式是redisTemplate 默认的序列号方式,避免反序列化失败
     */
    @Test
    public void test3() {
        String llistKey = "lkey1";
        String luaScript = String.format("local val = ARGV[1]; redis.call('rpush', '%s', val);", llistKey);

        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        List<String> args = Lists.newArrayList();
        args.add(new String(genericJackson2JsonRedisSerializer.serialize(new ExecutedTaskWrapperBO("key1", 1L))));

        RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
        String execute = redisTemplate.execute(redisScript, null, args.toArray());
        System.out.println(execute);
    }

    /**
     * 直接传集合, 解析集合参数
     */
    @Test
    public void test4() {
        /**
         * -- keys[1] 是Redis的key
         * -- argv 是传递给脚本的参数,argv[1]开始是集合的元素
         */
        String luaScript = "local key = KEYS[1]  \n" +
                "local set = {}  \n" +
                "for i, value in ipairs(ARGV) do  \n" +
                "    table.insert(set, value)  \n" +
                "end  \n" +
                "redis.call('RPUSH', key, unpack(set))  ";

        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        List<String> args = Lists.newArrayList();
        args.add(new String(genericJackson2JsonRedisSerializer.serialize(new ExecutedTaskWrapperBO("key1", 1L))));
        args.add(new String(genericJackson2JsonRedisSerializer.serialize(new ExecutedTaskWrapperBO("key2", 2L))));

        RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
        String execute = redisTemplate.execute(redisScript, Lists.newArrayList("lkey1"), args.toArray());
        System.out.println(execute);
    }

    /**
     * 先rpush,后zrem; 同时操作。 一个list、一个zset
     * rpush 到 lkey1; zrem zkey1
     *
     * 原语句:
     local rpushValue = {}
     for i, value in ipairs(ARGV) do
     table.insert(rpushValue, value)
     end
     redis.call('RPUSH', ${rpushKey}, unpack(rpushValue))

     local zremValue = {}
     for i, value in ipairs(KEYS) do
     table.insert(zremValue, value)
     end
     redis.call('zrem', ${zremKey}, unpack(zremValue))
     *
     */
    @Test
    public void test5() {
        String luaScript = "local rpushValue = {}  \n" +
                "for i, value in ipairs(ARGV) do  \n" +
                "    table.insert(rpushValue, value)  \n" +
                "end  \n" +
                "redis.call('RPUSH', '${rpushKey}', unpack(rpushValue))  \n" +
                "\n" +
                "local zremValue = {}  \n" +
                "for i, value in ipairs(KEYS) do  \n" +
                "    table.insert(zremValue, value)  \n" +
                "end  \n" +
                "redis.call('zrem', '${zremKey}', unpack(zremValue)) ";

        // args
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        List<String> args1 = Lists.newArrayList();
        args1.add(new String(genericJackson2JsonRedisSerializer.serialize(new ExecutedTaskWrapperBO("key1", 1L))));
        args1.add(new String(genericJackson2JsonRedisSerializer.serialize(new ExecutedTaskWrapperBO("key2", 2L))));

        List<String> keys = Lists.newArrayList();
        keys.add("1");
        keys.add("2");

        String rpushKey = "lkey1";
        String zremKey = "zkey1";
        RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript.replace("${rpushKey}", rpushKey).replace("${zremKey}", zremKey), String.class);
        String execute = redisTemplate.execute(redisScript, keys, args1.toArray());
        System.out.println(execute);
    }

}

 补充:redis 分片集群执行要求执行的key在同一个slot

  cluster keyslot keyName,可以计算得到hash的slot 值。cluster slots    // 查看节点插槽最大最小值, 16384 个,0-16383 slot计算方式:slot = crc16(key) mod 16384。

java 代码: hutool 工具包

        CRC16XModem crc16XModem = new CRC16XModem();
        crc16XModem.update("task:dataQueue:summaryInfo:DEFAULT".getBytes());
        System.out.println(crc16XModem.getValue() % 16384);

  redis lua 脚本操作的情况下,如果多个key 不是同一个slot,可能会收到:command keys must in same slot 的异常。

  原因:redis为了保持事务,同一个lua脚本访问应该访问同一个slot(hash槽),但是redis集群会根据 key 进行 hash 并对 16384 取模得到slot。

  可以指定redis key用于计算hash的部分字符串,也就是hash tag。比如我有两个key,分别是Lock:GlobalKey和Lock:PriKey,默认会将整个key拿去计算hash值,很显然,hash不可能一样。如果要让这两个key处于同一个hash槽,我们可以将key的公共部分提取出来用于hash计算,redis支持的格式是用{}包裹这部分字符串,官网把被{}包裹的这部分叫做 hash tag。 注意是第一个{} 中间内容(取key中第一个遇到的{}中的字符串来计算),
比如 {Lock}:GlobalKey 和 {Lock}:PriKey。把 Lock 字符串作为key的hash tag,
task:{{handleed}:{xxx}} 把{handleed 作为hash tag。

redisTemplate lua 脚本在分片集群操作多个key 的问题,分片集群要求:
1. 集群分片key slot必须一致, 可以用 hash tag 解决。 {tag}Key, 比如: {lock}:myKey
2. 要求execute第二个参数不能为空,必须有值
3, 一般按照正规传参数即可, 第二个参数是key, key 满足{hashtag} 一样, 第三个参数传argv数组,用lua 解析argv 取自己想要的数据
需求: 从zset 删除一个数据,rpush 到一个list

(1). lua 脚本示例

local zremValue = KEYS[1]
local zremValue = {}  
for i, value in ipairs(ARGV) do 
    if i <= ${ID_VALUE_NUM} then   
        table.insert(zremValue, value)   
    end   
end  
redis.call('zrem', '{a}zremKey', unpack(zremValue)); 

local rpushValue = {} 
local zremValue = KEYS[2]
for i, value in ipairs(ARGV) do 
    if i > ${ID_VALUE_NUM} then   
        table.insert(rpushValue, value)   
    end   
end 
redis.call('RPUSH', '{a}rpushKey', unpack(rpushValue)); 

(2). 代码

        String rpushKey = "task:dataQueue:{summaryInfo:DEFAULT}";
        String zremKey = "task:handled:{summaryInfo:DEFAULT}";
        ArrayList<String> keys = Lists.newArrayList(zremKey, rpushKey);

        // json 序列化,需要注意redisTemplate 不能再用这个序列号,自己定义第二个字符串专用 tenmplate
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        // args\keys
        List<Entity> objs = Lists.newArrayList();
        // todo 构造objs
        // 转换为string,注意需要和原始的序列化以及反序列化的序列化器保持一致
        List<String> convertedDataStrs = objs.stream().map(tmp -> new String(genericJackson2JsonRedisSerializer.serialize(tmp))).collect(Collectors.toList());
        int dataSize = convertedDataStrs.size();
        // todo 构造第二个集合数据,两个集合合并后传到 ARGV. lua 脚本根据dataSize 拆分集合,
        convertedDataStrs.addAll(otherDatas);

        RedisScript<String> redisScript = new DefaultRedisScript<>(LUA_SCRIPT1.replace("${ID_VALUE_NUM}", String.valueOf(dataSize)), String.class);
        redisTemplate.execute(redisScript, keys, convertedDataStrs.toArray());

补充:lua 基本语法

变量:
a = 10       -- 数字  
b = "Hello"  -- 字符串  
c = true     -- 布尔值
数据类型:
nil:空值
boolean:布尔值,truefalse
number:双精度浮点数
string:字符串
table:表(相当于其他语言中的数组或字典)
function:函数
thread:线程
userdata:用户自定义数据
条件语句:
if a > b then  
    print("a is greater than b")  
elseif a < b then  
    print("a is less than b")  
else  
    print("a is equal to b")  
end
for 循环:
for i = 1, 10 do  
    print(i)  
end
函数:
function greet(name)  
    print("Hello, " .. name .. "!")  
end  
  
greet("World")
table-数组:
-- 数组  
t1 = {"apple", "banana", "cherry"}  
print(t1[1])  -- 输出 "apple"  
  
-- 字典  
t2 = {key1 = "value1", key2 = "value2"}  
print(t2.key1)  -- 输出 "value1"

 

posted @ 2021-03-14 15:04  QiaoZhi  阅读(5070)  评论(0编辑  收藏  举报