秒杀(四)Jmeter演示秒杀中的超卖和重复购买并解决问题

目录

1、超卖现象

2、重复购买现象

3、Jmeter压测演示

4、Redis解决方案


 

1、超卖现象

超卖现象大家都知道是什么,我们思考一下,为什么会超卖?

当库存接近于0的时候,在高并发的情况下会出现某时刻多个线程查询库存够的,但下一时刻某个线程秒杀成功,对库存进行减操作,使得库存变为0,照理现在的状态是不能下单成功的,因为库存已经不够了,但别的线程仍然认为数量还够,对库存进行减操作,从而导致库存出现负数的情况,那这就是超卖了。那么有小伙伴说这个问题简单,对库存加锁啊,Lock、Synchronized或者cas乐观锁,那不就解决了。加锁是一个思路,那我们再考虑一个问题,我们的秒杀服务部署是单机还是分布式呢?如果是单机的,加锁当然可以解决问题,就是可能性能会差点,并发量没有那么大,那如果是分布式部署,单机的锁已经无法解决问题了。所以我们需要换一个思路,使用redis来解决超卖的问题,以下上干货!

2、重复购买现象

同一个用户在同一时刻多次秒杀同一个商品,造成同一个用户可以购买多个秒杀商品的现象,这个问题与上面的超卖问题类似。

3、Jmeter压测演示

首先我们看下如何使用Jmeter来压测,着急的小伙伴直接通过目录跳到解决方案。

下载地址:Apache JMeter - Download Apache JMeter

下载之后解压,双击/bin/jmeter.bat,会出现dos窗口,接着会出现jmeter界面,注意不要关闭dos窗口,否则jmeter也会跟着关闭了

具体的使用方法,这里也不说了,百度一下,自行解决。下面看下配置就行了

这里的线程组配置的是1000

one-one表示同一个人多次秒杀同一件商品,many-one表示多个人多次秒杀同一件商品。

这里配置了一个同步定时器,表示的意思是依次启动1000个线程,当到达1000个线程的时候,同时去请求接口

这里可以看到redis缓存的库存数已经被减到负数了,这就是超卖的现象。。

4、Redis解决方案

方案一:使用Redis的Watch机制解决

具体的机制理解参考:https://blog.csdn.net/qq_43371004/article/details/103439599

简单来说就是:对一个键设置监听器,当没有别的线程对这个键进行操作的时候,执行操作,代码类似下面这样

public boolean preOrder(Long userId, String goodsId) {
        //判断库存是否足够
        int preStock = goodsService.getStock(goodsId);
        if (preStock <= 0) {
            return false;
        }

        //先生成预订单,分布式锁,表示用户已经购买过了,否则并发情况下会产生重复购买的情况
        PreOrder preOrder = new PreOrder(userId, goodsId);
        long result = stringRedisService.hSet(RedisConstant.PREFIX_GOODS_SECKILLING + goodsId, userId.toString(), preOrder);
        if (result != 1) {
            return false;
        }

        //如果result == 1则成功,再扣库存
        boolean flag = goodsService.decrStock(goodsId);
        //可能存在并发情况,扣减之后的库存  < 0,表示库存扣减失败
        if (!flag) {
            logger.info("秒杀失败");
            //删除已经创建的预订单
            stringRedisService.hDel(RedisConstant.PREFIX_GOODS_SECKILLING + goodsId, userId.toString());
            return false;
        }
        //判断库存是否小于等于0,更新标志位已售完
        int afterStock = goodsService.getStock(goodsId);
        logger.info("秒杀成功");
        if (afterStock <= 0) {
            goodsService.setSaleOver(goodsId);
        }
        return true;
    }

更新库存的代码使用到watch机制,如下:

    public boolean decr(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.watch(key);// watch key
            String result = jedis.get(key);
            if (StringUtils.isEmpty(result) || Integer.parseInt(result) <= 0) {//获取值,如果为0,则返回失败
                return false;
            }
            Transaction tx = jedis.multi();// 开启事务
            tx.decr(key);
            return !CollectionUtils.isEmpty(tx.exec());//提交事务,如果此时key被改动了,则返回null,否则返回非空
        } finally {
            returnToPool(jedis);
        }
    }

这种方案虽然可以解决超卖问题,但是会存在问题,那就是不公平,先来的用户不一定先秒杀到,如下所示:

先来的线程都秒杀失败了,后面的反而成功了,当前如果不在意这些,使用这样方案基本可以解决超卖现象

方案二:使用Lua脚本借本

使用lua脚本解决公平性的问题,使得让先来的用户能够秒杀的商品,如果秒杀10个商品,先来的10个用户可以秒杀到商品,后面的用户没有机会秒杀到商品

代码如下,lua脚本的逻辑很容易看明白,这里不多解释,注意这里在扣减库存的同时也加入用户秒杀成功的订单,防止用户重复秒杀成功

 /**
     * 输入参数
     * 1: goodsId
     * 2: userId
     * 3: goods_seckilling_key 用户预订单key
     * 4: goods_stock_key 库存key
     * 5:stock_over_key 售完的key
     * 6: stock_over_value 售完标志位
     * 7: pre_order 预订单
     * 返回结果:
     * 0:表示已经抢光了
     * 1: 表示抢成功了
     * 2:表示已经抢过了
     */
    private static final String decrScript =
            "local goods_id=KEYS[1];\r\n" +
                    "local user_id=KEYS[2];\r\n" +
                    "local goods_seckilling_key=KEYS[3];\r\n" +
                    "local stock_key=KEYS[4];\r\n" +
                    "local stock_over_key=KEYS[5];\r\n" +
                    "local stock_over_value=KEYS[6];\r\n" +
                    "local pre_order=KEYS[7];\r\n" +
                    "local userExists=redis.call(\"hexists\", goods_seckilling_key, user_id);\r\n" +
                    "if tonumber(userExists)==1 then \r\n" +
                    "   return 2;\r\n" +
                    "end\r\n" +
                    "local num = redis.call(\"get\" , stock_key);\r\n" +
                    "if tonumber(num)<=0 then \r\n" +
                    "   redis.call(\"hset\", stock_over_key, goods_id, stock_over_value);\r\n" +
                    "   return 0;\r\n" +
                    "else \r\n" +
                    "   redis.call(\"decr\", stock_key);\r\n" +
                    "   redis.call(\"hset\", goods_seckilling_key, user_id, pre_order);\r\n" +
                    "end\r\n" +
                    "return 1";


public String decrStockAndSavePreOrder(String goodsId, String userId, PreOrder preOrder) {
        return stringRedisService.execLua(decrScript, goodsId, userId, RedisConstant.PREFIX_GOODS_SECKILLING + goodsId,
                RedisConstant.PREFIX_GOODS_STOCK + goodsId, RedisConstant.GOODS_SALE_OVER, "1", JSON.toJSONString(preOrder)).toString();
    }

jmeter启动10000线程同时秒杀,效果如下:可以看到只有先来的线程能够秒杀成功,后面的线程全部显示秒杀结束,这样对所有的用户都是公平的,先来先得!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值