Redis高并发防止秒杀超卖实战源码解决方案

目录
  • 1:解决思路
  • 2:添加 redis 常量
  • 3:添加 redis 配置类
  • 4:修改业务层
    • 1:秒杀业务逻辑层
    • 2:添加需要抢购的代金券
    • 3:抢购代金券
  • 5:postman 测试
    • 6:压力测试
      • 8:配置lua
        • 9:修改业务层
          • 1:抢购代金券
        • 10:压力测试

          1:解决思路

          将活动写入 redis 中,通过 redis 自减指令扣除库存。

          2:添加 redis 常量

          commons/constant/rediskeyconstant.java

          seckill_vouchers("seckill_vouchers:","秒杀券的 key"),

          3:添加 redis 配置类

          4:修改业务层

          废话不多说,直接上源码

          1:秒杀业务逻辑层

          @service
          public class seckillservice {
          @resource
          private seckillvouchersmapper seckillvouchersmapper;
          @resource
          2private voucherordersmapper voucherordersmapper;
          @value("${service.name.ms-oauth-server}")
          private string oauthservername;
          @resource
          private resttemplate resttemplate;
          @resource
          private redistemplate redistemplate;

          2:添加需要抢购的代金券

          @transactional(rollbackfor = exception.class)
          public void addseckillvouchers(seckillvouchers seckillvouchers) {
          // 非空校验
          assertutil.istrue(seckillvouchers.getfkvoucherid()== null,"请选择需要抢购的代金券");
          assertutil.istrue(seckillvouchers.getamount()== 0,"请输入抢购总数量");
          date now = new date();
          assertutil.isnotnull(seckillvouchers.getstarttime(),"请输入开始时间");
           
          // 生产环境下面一行代码需放行,这里注释方便测试
          // assertutil.istrue(now.after(seckillvouchers.getstarttime()),"开始时间不能早于当前时间");
          assertutil.isnotnull(seckillvouchers.getendtime(),"请输入结束时间");
          assertutil.istrue(now.after(seckillvouchers.getendtime()),"结束时间不能早于当前时间");
          assertutil.istrue(seckillvouchers.getstarttime().after(seckillvouchers.getendtime()),"开始时间不能晚于结束时间");
           
          // 采用 redis 实现
          string key= rediskeyconstant.seckill_vouchers.getkey() +seckillvouchers.getfkvoucherid();
          // 验证 redis 中是否已经存在该券的秒杀活动,hash 不会做序列化和反序列化,
          有利于性能的提高。entries(key),取到 key
          map<string, object> map= redistemplate.opsforhash().entries(key);
          //如果不为空或 amount 库存>0,该券已经拥有了抢购活动,就不要再创建。
          assertutil.istrue(!map.isempty() && (int) map.get("amount") > 0,"该券已经拥有了抢购活动");
           
          // 抢购活动数据插入 redis
          seckillvouchers.setisvalid(1);
          seckillvouchers.setcreatedate(now);
          seckillvouchers.setupdatedate(now);
          //key 对应的是 map,使用工具集将 seckillvouchers 转成 map
          redistemplate.opsforhash().putall(key,beanutil.beantomap(seckillvouchers));
          }

          3:抢购代金券

          @transactional(rollbackfor = exception.class)
          public resultinfo doseckill(integer voucherid, string accesstoken, string path)
          {
          // 基本参数校验
          assertutil.istrue(voucherid == null || voucherid < 0,"请选择需要抢购的代金券");
          assertutil.isnotempty(accesstoken,"请登录");
           
          // 采用 redis
          string key= rediskeyconstant.seckill_vouchers.getkey() + voucherid;//根据 key 获取 map
          map<string, object> map= redistemplate.opsforhash().entries(key);
          //map 转对象
          seckillvouchers seckillvouchers = beanutil.maptobean(map,seckillvouchers.class, true, null);
           
          // 判断是否开始、结束
          date now = new date();
          assertutil.istrue(now.before(seckillvouchers.getstarttime()),"该抢购还未开始");
          assertutil.istrue(now.after(seckillvouchers.getendtime()),"该抢购已结束");
           
          // 判断是否卖完
          assertutil.istrue(seckillvouchers.getamount() < 1,"该券已经卖完了");
           
          // 获取登录用户信息
          string url = oauthservername +"user/me?access_token={accesstoken}";
          resultinfo resultinfo = resttemplate.getforobject(url, resultinfo.class,accesstoken);
          if (resultinfo.getcode() != apiconstant.success_code) {
          resultinfo.setpath(path);
          return resultinfo;
          }
           
          // 这里的 data 是一个 linkedhashmap,signindinerinfo
          signindinerinfo dinerinfo = beanutil.fillbeanwithmap((linkedhashmap)resultinfo.getdata(), new signindinerinfo(), false);
           
          // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
          voucherorders order =voucherordersmapper.finddinerorder(dinerinfo.getid(),seckillvouchers.getfkvoucherid());
          assertutil.istrue(order != null,"该用户已抢到该代金券,无需再抢");
           
          //扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1
          long count = redistemplate.opsforhash().increment(key,"amount",-1);
          assertutil.istrue(count < 0,"该券已经卖完了");
           
          // 下单存储到数据库
          voucherorders voucherorders = new voucherorders();
          voucherorders.setfkdinerid(dinerinfo.getid());
          // redis 中不需要维护外键信息
          //voucherorders.setfkseckillid(seckillvouchers.getid());
          voucherorders.setfkvoucherid(seckillvouchers.getfkvoucherid());
          string orderno = idutil.getsnowflake(1, 1).nextidstr();
          voucherorders.setorderno(orderno);
          voucherorders.setordertype(1);
          voucherorders.setstatus(0);
          count = voucherordersmapper.save(voucherorders);
          assertutil.istrue(count == 0,"用户抢购失败");
          return resultinfoutil.buildsuccess(path,"抢购成功");
          }
          } 

          5:postman 测试

          http://localhost:8083/add

          {
          "fkvoucherid":1,
          "amount":100,
          "starttime":"2020-02-04 11:12:00",
          "endtime":"2021-02-06 11:12:00"
          }

          查看 redis

          再次运行 http://localhost:8083/add

          6:压力测试

          查看 redis 中的库存出现负值

          在 redis 中修改库存要分两部进行,先要获取库存的值,再扣减库存。所以在高并 发情况下,会导致 redis 扣减库存出问题。可以使用 redis 的弱事务或 lua 脚本解决。 7:安装lua resources/stock.lua

          if (redis.call('hexists', keys[1], keys[2])== 1) then
            local stock = tonumber(redis.call('hget', keys[1], keys[2]));
            if (stock > 0) then
              redis.call('hincrby', keys[1], keys[2],-1);
              return stock;
            end;
              return 0;
          end;

          hexists’, keys[1], keys[2]) == 1
          hexists 是判断 redis 中 key 是否存在。
          keys[1] 是 seckill_vouchers:1 keys[2] 是 amount
          hget 是获取 amount 赋给 stock
          hincrby 是自增,当为-1 是为自减。
          因为在 redis 中没有自减指令,所以当步长为 -1 表示自减。
          现在使用 lua 脚本,将 redis 中查询库存和扣减库存当成原子性操作在一个线程内.

          8:配置lua

          config/redistemplateconfiguration.java

          @bean
          public defaultredisscript<long> stockscript() {
            defaultredisscript<long> redisscript = new defaultredisscript<>();
            //放在和 application.yml 同层目录下
            redisscript.setlocation(new classpathresource("stock.lua"));
            redisscript.setresulttype(long.class);
            return redisscript;
          } 

          9:修改业务层

          ms-seckill/service/seckilservice.java

          1:抢购代金券

          @transactional(rollbackfor = exception.class)
          public resultinfo doseckill(integer voucherid, string accesstoken, string path)
          {
          // 基本参数校验
          assertutil.istrue(voucherid == null || voucherid < 0,"请选择需要抢购的代金券");
          assertutil.isnotempty(accesstoken,"请登录");
          // 采用 redis
          string key= rediskeyconstant.seckill_vouchers.getkey() + voucherid;
          //根据 key 获取 map
          map<string, object> map= redistemplate.opsforhash().entries(key);
          //map 转对象
          seckillvouchers seckillvouchers = beanutil.maptobean(map,seckillvouchers.class, true, null);
          // 判断是否开始、结束
          date now = new date();assertutil.istrue(now.before(seckillvouchers.getstarttime()),"该抢购还未开始");
          assertutil.istrue(now.after(seckillvouchers.getendtime()),"该抢购已结束");
          // 判断是否卖完
          assertutil.istrue(seckillvouchers.getamount() < 1,"该券已经卖完了");
          // 获取登录用户信息
          string url = oauthservername +"user/me?access_token={accesstoken}";
          resultinfo resultinfo = resttemplate.getforobject(url, resultinfo.class,
          accesstoken);
          if (resultinfo.getcode() != apiconstant.success_code) {
          resultinfo.setpath(path);
          return resultinfo;
          }
          // 这里的 data 是一个 linkedhashmap,signindinerinfo
          signindinerinfo dinerinfo = beanutil.fillbeanwithmap((linkedhashmap)
          resultinfo.getdata(), new signindinerinfo(), false);
          // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
          voucherorders order =voucherordersmapper.finddinerorder(dinerinfo.getid(),
          seckillvouchers.getfkvoucherid());
          assertutil.istrue(order != null,"该用户已抢到该代金券,无需再抢");
           
          //扣库存,采用 redis,redis 没有设置自减,所以要自减,将步长设置为-1
          // long count = redistemplate.opsforhash().increment(key,"amount",-1);
          // assertutil.istrue(count < 0,"该券已经卖完了");
          // 下单存储到数据库
          voucherorders voucherorders = new voucherorders();
          voucherorders.setfkdinerid(dinerinfo.getid());
          // redis 中不需要维护外键信息
          //voucherorders.setfkseckillid(seckillvouchers.getid());
          voucherorders.setfkvoucherid(seckillvouchers.getfkvoucherid());
          string orderno = idutil.getsnowflake(1, 1).nextidstr();
          voucherorders.setorderno(orderno);
          voucherorders.setordertype(1);
          voucherorders.setstatus(0);
          long count = voucherordersmapper.save(voucherorders);
          assertutil.istrue(count == 0,"用户抢购失败");
          // 采用 redis + lua 解决问题
          // 扣库存
          list<string> keys = new arraylist<>();
          //将 redis 的 key 放进去keys.add(key);
          keys.add("amount");
          long amount =(long) redistemplate.execute(defaultredisscript, keys);
          assertutil.istrue(amount == null || amount < 1,"该券已经卖完了");
          return resultinfoutil.buildsuccess(path,"抢购成功");
          } 

          10:压力测试

          将 redis 中库存改回 100

          压力测试

          查看 redis 中 amount=0 ,不会变成负值 查看数据库下单表 t_voucher_orders ,共计下 100 个订单。

          到此这篇关于redis高并发防止秒杀超卖实战源码解决方案的文章就介绍到这了,更多相关redis高并发防止秒杀超卖 内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

          (0)
          上一篇 2022年3月21日
          下一篇 2022年3月21日

          相关推荐