Redis基本使用四(Lua脚本)

Lua脚本出现背景:

Lua脚本出现于Redis2.6以上的版本,基于Lua语言,其作用在于很大程度上弥补Redis命令计算能力的不足,通过前面的程序可以看出Redis命令根据不同的数据结构有自增、自减、求并集、求交集等基本运算,这些计算不足以应付开发中各式各样的需求,而Lua脚本就在此情况下应运而生。

 

如何使用Lua脚本:

Redis支持两种不同的方式运行Lua脚本,

  1. 直接输入Lua语言的程序代码,适用于简单的脚本;
  2. 编写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脚本表示为一个字符串,如果业务逻辑较复杂就不太实用了,此时就需要编写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),如果读者电脑配置较低则可以加大线程睡眠等待的时间。

你可能感兴趣的:(JavaEE)