Lua脚本解决超卖问题

原文章来自我的语雀知识库

什么是Lua脚本?

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
在Redis2.6之前,如果用户想添加一些Redis不具备的功能,则需要通过编写客户端代码或者修改Redis的C源代码来实现自己想要的功能,前后者的解决方案都存在一些弊端,于是Redis2.6版本引入了eval命令,使得可以在Redis服务器内部执行Lua代码。
Lua脚本跟单个Redis命令以及MULTI/EXEC事务一样,都是原子操作。在某个Lua脚本的执行过程中不会受到其它命令的干扰,Redis将EVAL和EVALSHA看成是单个命令进行处理。

Lua脚本可以保证原子性吗?

Lua脚本只是作为原子操作执行,但它并不能保证ACID中的原子性,当redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因。和redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误。
如果在SpringBoot环境下使用StringRedisTemplate执行lua脚本,如果某个redis.call()执行出错,那么脚本会停止执行并返回错误信息,脚本中剩余的语句不会执行,而且已经执行的操作不会回滚。redis.pcall()在遇到错误时不会停止脚本的执行,而是返回一个包含错误信息的表。这意味着你可以在脚本中处理错误,而不是立即停止脚本的执行。就算不处理这个错误,脚本中剩余的语句也会执行完。

    // 演示lua脚本中redis.pcall()出错的情况
	// 在SpringBoot环境下运行下面的代码后,控制台无任何报错
	// 而且redis.call('SET','123451','543211'),redis.call('SET','TEST','TEST')都执行成功
	public static void testRedis(StringRedisTemplate stringRedisTemplate){
        String script = "redis.call('SET','123451','543211')" +
                "redis.pcall('INCRBY','123451','3.14159')" +
                "redis.call('SET','TEST','TEST')";
        DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptText(script);
        List<String> keys = new ArrayList<>();
        String result = stringRedisTemplate.execute(defaultRedisScript, keys);
    }
	// 演示lua脚本中redis.call()出错的情况
    // 在SpringBoot环境下运行下面代码
	// 控制台报错RedisCommandExecutionException: ERR Error running script
	// redis在执行到redis.call('INCRBY','123451','3.14159')时报错,脚本立即结束并返回
	// 但是redis.call('SET','123451','543211')依然成功执行,且没有回滚
	public static void testRedis(StringRedisTemplate stringRedisTemplate){
        String script = "redis.call('SET','123451','543211')" +
                "redis.call('INCRBY','123451','3.14159')" +
                "redis.call('SET','TEST','TEST')";
        DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptText(script);
        List<String> keys = new ArrayList<>();
        String result = stringRedisTemplate.execute(defaultRedisScript, keys);
    }

使用Lua脚本解决超卖问题

超卖问题来源于查询操作和更新操作不是原子性的,会导致在并发的场景下,出现库存超卖的情况。
例如:当商品A一共有库存15件,用户甲先下单10件,用户乙下单8件,这时候库存只能满足一个人下单成功,如果两个人同时提交,就出现了超卖的问题。
由于Lua脚本具有原子操作的特性,可以利用这一点来解决超卖问题。

    private static String script = null;

    public static void loadScript(){
        StringBuilder scriptBuilder = new StringBuilder();
        // 如果给定的商品不存在,则返回-1表示失败
        scriptBuilder.append("if redis.call('EXISTS', KEYS[1]) == 0 then ");
        scriptBuilder.append("return -1;");
        scriptBuilder.append("end;");
        scriptBuilder.append("local stock = tonumber(redis.call('GET', KEYS[1]));");
        // 如果库存量为-1表示无限量供应,返回0表示成功
        scriptBuilder.append("if stock == -1 then ");
        scriptBuilder.append("return 0;");
        scriptBuilder.append("end;");
        // 如果商品库存量不足,则返回-1表示失败
        scriptBuilder.append("if stock < tonumber(ARGV[1]) then ");
        scriptBuilder.append("return -1;");
        scriptBuilder.append("end;");
        // 扣减库存,返回原库存量表示成功
        scriptBuilder.append("redis.call('DECRBY', KEYS[1], tonumber(ARGV[1]));");
        scriptBuilder.append("return stock;");
        script = scriptBuilder.toString();
    }

    /**
     * 使用lua脚本扣减库存
     * @param stuff 商品名称
     * @param amount 想要扣减多少库存
     * @return 是否扣减成功
     */
    public static boolean decrementStockByLua(String stuff,Integer amount){
        if (script == null){
            loadScript();
        }
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
        // 注意,这里必须显式地设置返回值类型,不然lettuce会报错。
        defaultRedisScript.setResultType(Long.class);
        defaultRedisScript.setScriptText(script);
        List<String> keys = new ArrayList<>();
        keys.add(stuff);
        Long result = stringRedisTemplate.execute(defaultRedisScript, keys, String.valueOf(amount));
        return result > 0;
    }
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值