【Redis】商品秒杀案例(出现的错误解决):事务+锁,LUA脚本,DefaultRedisScript,watch,multi,exec,并发工具httpd-tool(ab)

本文通过一个商品秒杀案例,展示了使用SpringBoot整合Redis在高并发下可能出现的超卖问题及解决方案。从第一版的简单实现导致超卖,到第二版引入乐观锁减少超卖但出现库存遗留,最后到第三版通过LUA脚本彻底解决超卖和库存遗留问题。过程中涉及Redis事务、乐观锁和LUA脚本的应用。
摘要由CSDN通过智能技术生成

一、案例介绍

在这里插入图片描述

  • 模拟商品秒杀:通过url请求模拟随机用户秒杀 id为1001(id固定) 的商品
    1. 每次请求 商品id(1001)固定、用户id随机生成
    2. 秒杀前判断:Reids中key为 1001-kcKey 的值是否为空、是否<1。当前用户id是否存在key为 1001-killUserKey 的集合中
    3. 秒杀成功: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"
    

八、错误总结

  1. ab测试出现: The timeout specified has expired (70007)

    • 导致原因:接口所在服务器没有关闭防火墙
    • 问题解决:关闭防火墙
  2. 接口报错: 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愿你满腹经纶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值