一、案例介绍
- 模拟商品秒杀:
通过url请求
,模拟随机用户秒杀 id为1001(id固定) 的商品
。- 每次请求 商品id(1001)固定、用户id随机生成
- 秒杀前判断:
Reids
中key为1001-kcKey
的值是否为空、是否<1。当前用户id是否存在key为1001-killUserKey
的集合中 - 秒杀成功:Redis中key为
1001-kcKey
的值减1
,key为1001-killUserKey
的集合添加用户id
- 案例环境:Spring Boot整合Redis,Centos 7 ,redis 6.2.0
二、秒杀代码—第一版
//秒杀案例api第一版——出现超卖问题
@RequestMapping(value = "/secKill1")
public String secKill1(){
//1.定义:随机用户id、商品id,商品库存key:String,商品秒杀成功用户key:Set
String goodsId="1001";//商品id
String userID = RandomStringUtils.randomNumeric(6);//随机用户id
String kcKey=goodsId+"-kcKey";//商品库存key
String killUser=goodsId+"-killUser";//商品秒杀成功用户key
//2.判断用户是否已秒杀成功过
Boolean hasKill = redisTemplate.opsForSet().isMember(killUser, userID);
if (hasKill){
System.out.println("秒杀失败:已经秒杀过!");
return "秒杀失败:已经秒杀过!";
}
//3.判断库存是否为null
Object o = redisTemplate.opsForValue().get(kcKey);
if (o ==null){
System.out.println("秒杀失败:还未开始!");
return "秒杀失败:还未开始!";
}
//4.判断商品库存是否小于1
if (Integer.parseInt(String.valueOf(o))<1){
System.out.println("秒杀失败:没有库存了!");
return "秒杀失败:没有库存了!";
}
//5.秒杀过程
//5.1 商品库存-1
redisTemplate.opsForValue().decrement(kcKey);
//5.2 将用户添加到商品秒杀成功用户key
redisTemplate.opsForSet().add(killUser,userID);
System.out.println("秒杀成功!");
return "秒杀成功!";
}
三、出现超卖问题
-
注意关闭防火墙
-
Linux环境下载 httpd-tools:yum install httpd-tools
-
127.0.0.1:6379> set 1001-kcKey 10 OK
-
高并发请求: ab -n 20 -c 11 http://192.168.100.1:8081/redis/secKill1
-
秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! ---------------------------------------到低
-
127.0.0.1:6379> get 1001-kcKey "-5"
四、解决超卖—第二版:乐观锁watch()
注意:redisTemplate.setEnableTransactionSupport(true);
//秒杀案例api第二版——解决超卖问题,出现库存遗留问题
@RequestMapping(value = "/secKill2")
public String secKill2(){
//1.定义:随机用户id、商品id,商品库存key:String,商品秒杀成功用户key:Set
String goodsId="1001";//商品id
String userID = RandomStringUtils.randomNumeric(6);//随机用户id
String kcKey=goodsId+"-kcKey";//商品库存key
String killUser=goodsId+"-killUser";//商品秒杀成功用户key
//乐观锁
redisTemplate.watch(kcKey);
//2.判断用户是否已秒杀成功过
Boolean hasKill = redisTemplate.opsForSet().isMember(killUser, userID);
if (hasKill){
System.out.println("秒杀失败:已经秒杀过!");
return "秒杀失败:已经秒杀过!";
}
//3.判断库存是否为null
Object o = redisTemplate.opsForValue().get(kcKey);
if (o ==null){
System.out.println("秒杀失败:还未开始!");
return "秒杀失败:还未开始!";
}
//4.判断商品库存是否小于1
if (Integer.parseInt(String.valueOf(o))<1){
System.out.println("秒杀失败:没有库存了!");
return "秒杀失败:没有库存了!";
}
//开启事务执行秒杀操作
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
//5.秒杀过程
//5.1 商品库存-1
redisTemplate.opsForValue().decrement(kcKey);
//5.2 将用户添加到商品秒杀成功用户key
redisTemplate.opsForSet().add(killUser,userID);
List exec = redisTemplate.exec();
if (exec==null || exec.size()==0){
System.out.println("秒杀失败!");
return "秒杀失败:这个库存被抢先一步,请试试下一个库存";
}
System.out.println(exec);
System.out.println("秒杀成功!");
return "秒杀成功!";
}
五、出现库存遗留:LUA脚本
-
127.0.0.1:6379> set 1001-kcKey 10 OK
-
ab -n 20 -c 11 http://192.168.100.1:8081/redis/secKill2
-
[9, 1] 秒杀成功! [8, 1] 秒杀成功! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! [7, 1] 秒杀成功! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! [6, 1] 秒杀成功! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! 秒杀失败! ---------------------------------------到低
-
127.0.0.1:6379> get 1001-kcKey "6"
六、解决遗留—第三版
//秒杀案例api第三版——解决 超卖问题、库存遗留问题
@RequestMapping(value = "/secKill3")
public String secKill3(){
List<String> list=new ArrayList<>();
list.add(RandomStringUtils.randomNumeric(6));
list.add("1001");
DefaultRedisScript scriptObject=new DefaultRedisScript();
scriptObject.setResultType(Long.class);
String script="local userid=KEYS[1]; \n" +
"local prodid=KEYS[2];\n" +
"local kcKey=\"sk:\"..prodid..\"-kcKey\";\n" +
"local killUserKey=\"sk:\"..prodid..\"-killUserKey\"; \n" +
"local userExists=redis.call(\"sismember\",killUserKey,userid);\n" +
"if tonumber(userExists)==1 then \n" +
" return 2;\n" +
"end\n" +
"local num= redis.call(\"get\" ,kcKey);\n" +
"if tonumber(num)<=0 then \n" +
" return 0; \n" +
"else \n" +
" redis.call(\"decr\",kcKey);\n" +
" redis.call(\"sadd\",killUserKey,userid);\n" +
"end\n" +
"return 1;";
scriptObject.setScriptText(script);
Object result = redisTemplate.execute(scriptObject, list);
if(result.equals(2l)){
System.out.println("秒杀失败:已经秒杀过!");
return "秒杀失败:已经秒杀过!";
}else if(result.equals(0l)){
System.out.println("秒杀失败:没有库存了!");
return "秒杀失败:没有库存了!";
}else if (result.equals(1l)){
System.out.println("秒杀成功!");
return "秒杀成功!";
}else {
System.out.println("秒杀异常!");
return "秒杀异常!";
}
}
七、案例结束
-
127.0.0.1:6379> set 1001-kcKey 10 OK
-
ab -n 20 -c 11 http://192.168.100.1:8081/redis/secKill3
-
秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀成功! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! 秒杀失败:没有库存了! ---------------------------------------到低
-
127.0.0.1:6379> get 1001-kcKey "0"
八、错误总结
-
ab测试出现: The timeout specified has expired (70007)
- 导致原因:接口所在服务器没有关闭防火墙
- 问题解决:关闭防火墙
-
接口报错: java.lang.IllegalStateException
-
导致原因:第三版代码中 scriptObject.setResultType(Integer.class) 设置 LUA脚本 返回值类型为Integer。但是 LUA脚本 并不支持Integer类型。
// ReturnType.class 返回值类型相关源码 public static ReturnType fromJavaType(@Nullable Class<?> javaType) { if (javaType == null) { return ReturnType.STATUS; } if (javaType.isAssignableFrom(List.class)) { return ReturnType.MULTI; } if (javaType.isAssignableFrom(Boolean.class)) { return ReturnType.BOOLEAN; } if (javaType.isAssignableFrom(Long.class)) { return ReturnType.INTEGER; } return ReturnType.VALUE;
-
错误解决:修改第三版代码:scriptObject.setResultType(Long.class),设置脚本返回值类型为Long
-