Lua脚本出现于Redis2.6以上的版本,基于Lua语言,其作用在于很大程度上弥补Redis命令计算能力的不足,通过前面的程序可以看出Redis命令根据不同的数据结构有自增、自减、求并集、求交集等基本运算,这些计算不足以应付开发中各式各样的需求,而Lua脚本就在此情况下应运而生。
Redis支持两种不同的方式运行Lua脚本,
Lua脚本命令格式:
eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]
eval:代表执行Lua语言的命令,不变;
lua-script:代表Lua语言脚本,例如return,redis.call等(类似于存储过程,redis.call代表调用redis指令);
key-num:代表参数中key的数量,0代表没有任何参数;
[key1 key2 key3 ...]:代表key作为参数传递给Lua语言,个数需要与key-num对应;
[value1 value2 value3 ...] :代表传递给Lua语言的参数;
使用示例(注意的是KEYS和ARGV必须大写):
Jedis jedis=new Jedis("127.0.0.1",6379);
Object o1=jedis.eval("return 'hello Lua!'");
// o1 返回 hello Lua!
System.out.println(o1);
String key="lua_key";
Object o2=jedis.eval("redis.call('set',KEYS[1],ARGV[1])",1,key,"新恒结衣");
// o2 返回 null
System.out.println(o2);
// 访问key对应的value
System.out.println(jedis.get(key));
通过如上简单的例子可以发现Lua脚本的基本使用还是比较友善的,接下来的例子可以进一步使用Lua脚本:
package xyz.lsm1998;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import redis.clients.jedis.Jedis;
import xyz.lsm1998.config.RedisConfig;
import xyz.lsm1998.domain.User;
import java.util.ArrayList;
import java.util.List;
/**
* 作者:刘时明
* 日期:2018/11/29
* 时间:23:51
* 说明:Lua脚本测试
*/
public class LuaTest
{
public static void main(String[] args)
{
Jedis jedis = new Jedis("127.0.0.1", 6379);
//test2(jedis);
var context = new AnnotationConfigApplicationContext(RedisConfig.class);
RedisTemplate template = context.getBean(RedisTemplate.class);
test3(template);
}
private static void test1(Jedis jedis)
{
Object o1 = jedis.eval("return 'hello Lua!'");
// o1 返回 hello Lua!
System.out.println(o1);
String key = "lua_key";
Object o2 = jedis.eval("redis.call('set',KEYS[1],ARGV[1])", 1, key, "新恒结衣");
// o2 返回 null
System.out.println(o2);
// 访问key对应的value
System.out.println(jedis.get(key));
}
private static void test2(Jedis jedis)
{
// 缓存脚本,返回SHA-1签名算法的标识
String sha = jedis.scriptLoad("redis.call('set',KEYS[1],ARGV[1])");
// 通过标识执行脚本
jedis.evalsha(sha, 1, new String[]{"name", "新恒结衣"});
String name = jedis.get("name");
System.out.println("name=" + name);
jedis.close();
}
private static void test3(RedisTemplate template)
{
// 设置序列化器,避免出现类型转换异常
template.setValueSerializer(new JdkSerializationRedisSerializer());
// 保存一个自定义对象,需要实现Serializable接口
User user = new User();
user.setId(1);
user.setAge(20);
// Spring提供的RedisScript接口,DefaultRedisScript是它的实现类
DefaultRedisScript redisScript = new DefaultRedisScript<>();
// 设置脚本
redisScript.setScriptText("redis.call('set',KEYS[1],ARGV[1]) return redis.call('get',KEYS[1])");
// key参数列表
List keyList = new ArrayList<>();
keyList.add("user_1");
// 设置脚本返回值,如果不设置则返回null
redisScript.setResultType(User.class);
// 执行脚本并获取返回值
User result = template.execute(redisScript,keyList, user);
System.out.println("result=" + result);
}
}
之前的案例都是把Lua脚本表示为一个字符串,如果业务逻辑较复杂就不太实用了,此时就需要编写lua文件来保存脚本,创建一个game文件,后缀名为lua,实现一个模拟石头剪刀布的游戏,如下:
redis.call('set',KEYS[1],ARGV[1])
redis.call('set',KEYS[2],ARGV[2])
local n1 = tostring(redis.call('get',KEYS[1]))
local n2 = tostring(redis.call('get',KEYS[2]))
if n1=='剪刀' and n2=='布' then
return 1
end
if n1=='布' and n2=='石头' then
return 1
end
if n1=='石头' and n2=='剪刀' then
return 1
end
return 0
程序如下(给出三个方法,只需要main方法调用即可):
private static void test5(Jedis jedis)
{
// 方便起见填绝对路径
File file = new File("C:\\JavaCode\\SpringBoot01_Redis\\src\\main\\java\\xyz\\lsm1998\\game.lua");
byte[] bytes = getFileBytes(file);
System.out.println("执行脚本=\n" + new String(bytes));
byte[] sha = jedis.scriptLoad(bytes);
System.out.println("请选择:1.剪刀,2.石头,3.布");
Scanner input = new Scanner(System.in);
String n1 = getSelect(input.nextInt());
if (n1 == null)
{
System.out.println("非法输入");
} else
{
String n2 = getSelect(new Random().nextInt(3) + 1);
System.out.println("n1="+n1+",n2="+n2);
Object result = jedis.evalsha(sha, 2, "key1".getBytes(), "key2".getBytes(), n1.getBytes(), n2.getBytes());
System.out.println(result.toString().equals("0")?"你没有赢":"你赢了");
}
}
private static String getSelect(int n)
{
String result;
switch (n)
{
case 1:
result = "剪刀";
break;
case 2:
result = "石头";
break;
case 3:
result = "布";
break;
default:
System.out.println("非法输入");
result = null;
break;
}
return result;
}
private static byte[] getFileBytes(File file)
{
byte[] bytes = new byte[(int) file.length()];
try
{
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] temp = new byte[10 * 1024];
int len;
while ((len = fis.read(temp)) > 0)
{
baos.write(temp, 0, len);
}
bytes = baos.toByteArray();
} catch (Exception e)
{
e.printStackTrace();
}
return bytes;
}
在面临高并发业务时,事务与锁机制可以保证数据一致性,而Lua脚本的出现使得情况大有改观,通常情况下都是采用Lua脚本来处理高并发业务,因为Lua脚本的执行具有原子性,而灵活程度和执行效率更是前者无可比拟的,接下来程序实现一个抢购任务,共10000件商品,分10个线程,每个线程抢购1200次,也就是说注定会有2000次抢购失败的请求,Lua脚本文件如下:
local shopPacket='shop_packet_'..KEYS[1]
local stock=tonumber(redis.call('hget',shopPacket,'stock'))
if stock<=0 then
return 0
end
stock=stock-1
redis.call('hset',shopPacket,'stock',tostring(stock))
if stock==0 then
return 2
end
return 1
抢购程序如下:
package xyz.lsm1998;
import redis.clients.jedis.Jedis;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
/**
* 作者:刘时明
* 日期:2018/11/30
* 时间:16:00
* 说明:
*/
public class ShopTest
{
private static byte[] sha = null;
private static long start, end;
private static boolean flag = true;
public static void main(String[] args)
{
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 初始化项目,设置商品key为shop_packet_1,库存stock为10000
jedis.hset("shop_packet_1", "stock", "10000");
// 循环开启10个线程,每个线程抢1200次
start = System.currentTimeMillis();
for (int i = 0; i < 10; i++)
{
new Thread(() ->
{
for (int j = 0; j < 1200; j++)
{
long result = once(1);
switch ((int) result)
{
case 1:
System.out.println("抢到一个商品");
break;
case 2:
System.out.println("抢到最后一个商品");
break;
case 0:
if (flag)
{
end = System.currentTimeMillis();
flag = false;
}
System.out.println("商品已经被抢完了");
break;
default:
System.out.println(result);
System.out.println("返回值异常");
break;
}
}
}).start();
}
try
{
Thread.sleep(5 * 1000);
} catch (Exception e)
{
e.printStackTrace();
}
String shopNum = jedis.hget("shop_packet_1", "stock");
System.out.println("当前商品剩余:" + shopNum);
System.out.println("抢购时长:" + (end - start));
}
private static byte[] getFileBytes(File file)
{
byte[] bytes = new byte[(int) file.length()];
try
{
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] temp = new byte[10 * 1024];
int len;
while ((len = fis.read(temp)) > 0)
{
baos.write(temp, 0, len);
}
bytes = baos.toByteArray();
} catch (Exception e)
{
e.printStackTrace();
}
return bytes;
}
private static Long once(int shopId)
{
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 如果脚本为加载则加载
if (sha == null)
{
File file = new File("C:\\JavaCode\\SpringBoot01_Redis\\src\\main\\java\\xyz\\lsm1998\\shop.lua");
byte[] bytes = getFileBytes(file);
//System.out.println("执行脚本=\n" + new String(bytes));
sha = jedis.scriptLoad(bytes);
}
Object result = jedis.evalsha(sha, 1, (shopId + "").getBytes(), (shopId + "-" + System.currentTimeMillis()).getBytes());
return (Long) result;
}
}
最后笔者的程序在3秒内执行完毕(CPU i5 7400),如果读者电脑配置较低则可以加大线程睡眠等待的时间。