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:布尔值,true 或 false 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"