Redis系列-1.超卖问题解决

2 篇文章 0 订阅
2 篇文章 0 订阅
本文通过一个实际的秒杀场景,展示了在高并发下使用Redis可能出现的超卖问题。详细分析了问题产生的原因,并提供了一种使用Lua脚本来确保操作原子性的解决方案,从而避免了库存的负数和超卖情况。同时,通过JMeter进行并发测试验证了解决方案的有效性。
摘要由CSDN通过智能技术生成

目录

前言

背景

 表

说明

超卖问题

展示主要代码

jmeter测试

使用lua脚本解决超卖问题

展示主要代码

controller层

serviceImpl层

lua脚本

config配置

jmeter测试

 源码


前言

数据库脚本与测试用例在文末

背景

        在抢购商品或优惠券的时候,如果是大量用户同时抢购,容易出现多抢购的情况,订单的数量超过库存。

        本文章主要使用redis模拟演示秒杀优惠券过程出现的超卖现象与解决。

 表

使用到的三张表。

voucher 优惠券表

vouchers_seckill 优惠券秒杀活动列表

voucher_order 优惠券订单表

说明

  优惠券秒杀过程:

①新建优惠券信息;

②选择优惠券添加到秒杀活动列表,并保存到redis;

③抢购优惠券,生产优惠券订单。

      本文主要演示将秒杀活动缓存到redis后的抢购演示。

超卖问题

展示主要代码

        说明:模拟用户登录后抢购的过程。因为redis的递减操作不为increment原子性,容易出现超卖问题。

1.新建controller

    @PostMapping("/{userId}/{voucherId}")
    @ApiOperation("用户秒杀优惠券-redis-有超卖问题")
    public Result  seckillVoucherByredisNo(@PathVariable("userId") Integer userId, @PathVariable("voucherId") Integer voucherId){


       Result result =  seckillService.seckillVoucherByredis(userId,voucherId);
       
        return Result.success("成功");
    }

2.新建serviceimpl

@Override
    public Result seckillVoucherByredis(Integer userId, Integer voucherId) {
        //1.判断用户是否登录
        Assert.isFalse(userId == null,"用户未登录","");

        //2.优惠券是否有效 表达式为true抛出异常
        Assert.isFalse(voucherId < 0 && voucherId == null,"优惠券无效","");
        //3.活动是否开始

        String seckillKey = RedisContants.VOUCHER_SECKill.getKey()+voucherId;
        Map<String,Object> map = redisTemplate.opsForHash().entries(seckillKey);
        VouchersSeckill vouchersSeckill = BeanUtil.mapToBean(map, VouchersSeckill.class, true, CopyOptions.create());

        Date startTime = vouchersSeckill.getStartTime();
        Date endTime = vouchersSeckill.getEndTime();

        Assert.isFalse(new Date().getTime() < startTime.getTime(),"活动未开始");
        //4.活动是否结束
        Assert.isFalse(new Date().getTime() > endTime.getTime(),"活动已结束");
        //5.数量是否抢购完
        Assert.isFalse(vouchersSeckill.getAmount()<=0,"已经抢购一空");
        //7.扣减库存
        //递减操作不是原子性,并发情况下,操作优惠券库存会不正确。
        redisTemplate.opsForHash().increment(seckillKey,"amount",-1);
        //8.新增订单
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setFkUserId(userId);
        voucherOrder.setFkSeckillId(vouchersSeckill.getId());
        voucherOrder.setFkVoucherId(voucherId);
        voucherOrder.setOrderNo(IdUtil.simpleUUID());
        voucherOrderService.save(voucherOrder);

        return  Result.success();
    }

jmeter测试

使用jmeter模拟1000个用户抢购优惠券。配置如下:

演示过程:

1.访问http:localhost:8084/doc.html.往redis添加优惠券id为2,库存100的秒杀活动。在redis中查到如下数据,说明添加成功。

 2.使用jmeter测试1000用户并发

点击超卖问题脚本后,待脚本执行结束,查看redis,发现库存为-100,生成的订单数量为200.超出了100的库存

使用lua脚本解决超卖问题

展示主要代码

在原先有超卖问题的代码上进行了稍微的改动,从redis查询库存再到修改使用了lua脚本保证原子性。

controller层

@PostMapping("/lua/{userId}/{voucherId}")
    @ApiOperation("用户秒杀优惠券-redis-使用lua脚本解决超卖问题")
    public Result  seckillVoucherByredisLua(@PathVariable("userId") Integer userId, @PathVariable("voucherId") Integer voucherId){


        Result result =  seckillService.seckillVoucherByredisLua(userId,voucherId);


        return Result.success("成功");
    }

serviceImpl层

@Override
    public Result seckillVoucherByredisLua(Integer userId, Integer voucherId) {
        //1.判断用户是否登录
        Assert.isFalse(userId == null,"用户未登录","");

        //2.优惠券是否有效 表达式为true抛出异常
        Assert.isFalse(voucherId < 0 && voucherId == null,"优惠券无效","");
        //3.活动是否开始

        String seckillKey = RedisContants.VOUCHER_SECKill.getKey()+voucherId;
        Map<String,Object> map = redisTemplate.opsForHash().entries(seckillKey);
        VouchersSeckill vouchersSeckill = BeanUtil.mapToBean(map, VouchersSeckill.class, true, CopyOptions.create());

        Date startTime = vouchersSeckill.getStartTime();
        Date endTime = vouchersSeckill.getEndTime();

        Assert.isFalse(new Date().getTime() < startTime.getTime(),"活动未开始");
        //4.活动是否结束
        Assert.isFalse(new Date().getTime() > endTime.getTime(),"活动已结束");
        //5.数量是否抢购完
        //使用lua脚本保证原子性
        List<String> keys = new ArrayList<>();
        keys.add(seckillKey);
        keys.add("amount");

        Long stockOversold = (Long) redisTemplate.execute(stockOversoldScript, keys);
        Assert.isFalse(stockOversold == null || stockOversold < 1,"已经抢购一空");
        //8.新增订单
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setFkUserId(userId);
        voucherOrder.setFkSeckillId(vouchersSeckill.getId());
        voucherOrder.setFkVoucherId(voucherId);
        voucherOrder.setOrderNo(IdUtil.simpleUUID());
        voucherOrderService.save(voucherOrder);

        return  Result.success();
    }

lua脚本

config配置

 

jmeter测试

模拟1000用户并发操作,除了接口其他配置与前面相同。

 1.清空优惠券订单表数据

2.往redis添加优惠券id为2,库存100的秒杀活动。在redis中查到如下数据,说明添加成功。

 

 3.启动jmeter测试

 脚本执行完毕后,可以看到redis中库存为0,优惠券订单表生成100个订单。成功解决超卖问题。

 源码

源码地址

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿孟呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值