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.
然后,我们用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此时的数据:
库存刚好为0。
添加的用户刚好为100。演示成功!