SpringBoot使用LUA解决Redis库存遗留问题

前面,我的博客提到了怎么用Redis的乐观锁解决超卖问题。但是,使用乐观锁其实,有一个缺点,就是我们假设现在有2000次请求,并发数为200,此时的库存如果比较大的话,比如是500,那么,我们最后会发现,这2000次请求最后会有很多次因为乐观锁机制的影响导致的抢购失败。

这个问题要解决,我们可以使用我们的LUA。
简单介绍一下,LUA是一个小巧的脚本语言,他不适合作为开发独立应用程序的语言,但是却可以作为我们嵌入式的脚本语言。很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

那么,使用过LUA在Redis中有什么优势呢?将复杂或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。

LUA脚本类似于redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。可以很好地解决我们redis的超卖问题和库存遗留问题

注意:Redis版本必须要在2.6以上才可以使用LUA脚本。通过LUA脚本解决争抢问题,实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题。

废话不多说,我们直接给源码:

@RestController
public class RedisController {
    static String secKillScript =
            "local userid=KEYS[1];\r\n"+
            "local prodid=KEYS[2];\r\n"+
            "local kcKey=\"sk:\"..prodid..\":qt\";\r\n"+
            "local userKey=\"sk:\"..prodid..\":user\"\r\n"+
            "local userExists=redis.call(\"sismember\",userKey,userid);\r\n"+
            "if tonumber(userExists)==1 then \r\n"+
            "    return 2;\r\n"+
            "end\r\n"+
            "local num=redis.call(\"get\", kcKey);\r\n"+
            "if tonumber(num)<=0 then \r\n"+
            "    return 0;"+
            "else "+
            "    redis.call(\"decr\",kcKey);\r\n"+
            "    redis.call(\"sadd\", userKey, userid);\r\n"+
            "end\r\n"+
            "return 1";
            
    @PostMapping("/secKill2")
    public void TestLua(){
        //用户id,我们使用随机数来表示每次访问的不同用户
        String userid = new Random().nextInt(50000) + "";
        //商品id,我们先写死
        String prodid = "1001";

        //1、连接redis(有密码记得加jedis.auth("密码"))
        Jedis jedis = new Jedis("119.91.153.74", 6379);

        String sha1 = jedis.scriptLoad(secKillScript);
        Object result = jedis.evalsha(sha1, 2, userid, prodid);

        String reString = String.valueOf(result);
        if("0".equals(reString)){
            System.out.println("已经被抢空");
        }else if ("1".equals(reString)){
            System.out.println("抢购成功!");
        }else if ("2".equals(reString)){
            System.out.println("您已经抢购过了,请勿重复抢购!");
        }else{
            System.out.println("抢购异常");
        }
        jedis.close();
    }
}

解释一下源码:

static String secKillScript =
            "local userid=KEYS[1];\r\n"+
            "local prodid=KEYS[2];\r\n"+
            "local kcKey=\"sk:\"..prodid..\":qt\";\r\n"+
            "local userKey=\"sk:\"..prodid..\":user\"\r\n"+
            "local userExists=redis.call(\"sismember\",userKey,userid);\r\n"+
            "if tonumber(userExists)==1 then \r\n"+
            "    return 2;\r\n"+
            "end\r\n"+
            "local num=redis.call(\"get\", kcKey);\r\n"+
            "if tonumber(num)<=0 then \r\n"+
            "    return 0;"+
            "else "+
            "    redis.call(\"decr\",kcKey);\r\n"+
            "    redis.call(\"sadd\", userKey, userid);\r\n"+
            "end\r\n"+
            "return 1";
"local userid=KEYS[1];\r\n"+
            "local prodid=KEYS[2];\r\n"+

local表示变量。相当于我们在js中使用let。
KEYS表示参数,你可以简单地理解成这段脚本是一个函数,KEYS是传过来的参数的数组。其中KEYS[1]表示第一个参数,KEYS[2]表示第二个参数。

"local kcKey=\"sk:\"..prodid..\":qt\";\r\n"

这个也很简单,就是我们java里面字符串的拼接,只不过,这里将+改为了…(两个点)。

userExists=redis.call(\"sismember\",userKey,userid);\r\n"

这一段就是让redis调用命令sismember,然后将他的参数 userKey 和 userid 传进去。

tonumber(userExists)==1

这一段,就是把字符串转化为Number类型。

至于return 0,return 1,return 2。这个其实是我们自己定义的。这里我们规定,返回0表示库存为0,返回1表示用户抢购成功,返回2表示用户已经抢购成功,且打算抢购第二次,对其进行阻止。

String sha1 = jedis.scriptLoad(secKillScript);
        Object result = jedis.evalsha(sha1, 2, userid, prodid);

这里String sha1 = jedis.scriptLoad(secKillScript);表示的是加载LUA脚本,jedis.evalsha(sha1, 2, userid, prodid);这个方法表示执行脚本,四个参数分别表示的是:加载好的脚本、该脚本接收的参数个数,第一个参数,第二个参数)。

ok,代码写完了,现在开始演示:

先清空数据库,然后设置库存为100.

redis lua get 类型 redis lua缺点_nginx


然后,我们用python跑一下我们的ab工具,模拟并发操作:

import os
#D:\Download\httpd-2.4.51-o111l-x64-vc15\Apache24\bin\ab是我们的ab工具路径
ab=os.popen(r'D:\Download\httpd-2.4.51-o111l-x64-vc15\Apache24\bin\ab -n 2000 -c 200 -p ./postfile -T application/x-www-form-urlencoded http://192.168.148.148:8080/secKill')
print(ab.read())

运行结果:

redis lua get 类型 redis lua缺点_负载均衡_02


我们再看看Redis此时的数据:

库存刚好为0。

redis lua get 类型 redis lua缺点_redis lua get 类型_03


添加的用户刚好为100。演示成功!

redis lua get 类型 redis lua缺点_redis lua get 类型_04


redis lua get 类型 redis lua缺点_服务器_05