springboot商品秒杀系统遇到的高并发问题

基于springboot开发的商品秒杀系统所遇到的高并发问题

使用jmeter进行测试所遇到的各种高并发带来的问题:
原始业务代码:

/**
     * 测试用下单
     * @param itemId 商品id
     * @param userId 用户id
     * @return true false
     */
    @Override
    public boolean testKill(Long itemId, Long userId) {
    
    
        SecItem item = selectItemByItemId(itemId);
        if(!boughtOrNot(userId,itemId)) {
    
    
            if (userId!=null && item != null) {
    
    
                if (item.getIsok() == '1') {
    
    
                    return generateOrder(item, userId);
                }
            }
            log.warn("testKill----userId或item为Null");
        }
        return false;
    }

    /**
     * 判断是否买过同一个商品
     * @param userId 用户id
     * @param itemId 商品id
     * @return true false
     */
    private boolean boughtOrNot(Long userId, Long itemId) {
    
    
        List<SecOrder> orders = orderMapper.selectOrderByUserId(userId);
        if (orders!=null&&orders.size()!=0) {
    
    
            for (SecOrder order : orders) {
    
    
                if (order.getItemId().longValue() == itemId) {
    
    
                    log.info("boughtOrNot----找到订单");
                    return true;
                }
            }
            log.warn("boughtOrNot----没有找到对应itemId");
            return false;
        }
        return false;
    }

    /**
     * 产生订单
     * @param item 商品信息
     * @param userId 用户id
     * @return 结果
     */
    public boolean generateOrder(SecItem item,Long userId){
    
    
        SecOrder order=new SecOrder();
        order.setOrderId(SecKillUtils.getOrdeIdBySnow());
        order.setState('0');
        order.setCreateTime(new Date());
        order.setItemId(item.getItemId());
        order.setUserId(userId);
        order.setPrice(item.getPrice());
        if (updateStock(item, 1)){
    
    
            orderMapper.insertOrder(order);
            //发送邮件消息
            //sendService.sendMsg(order.getOrderId());
            //发送订单消息到死信队列
            sendService.sendDeadMsg(order.getOrderId());
            log.info("generateOrder----成功");
            return true;
        }
        log.warn("generateOrder----updateStock返回false");
        return false;
    }
    /**
     * 更新库存
     * @param item 商品信息
     * @param i 改变库存的数量
     * @return 是否修改成功
     */
    private boolean updateStock(SecItem item, int i) {
    
    
        if (item!=null) {
    
    
            if(item.getItemStock()>0) {
    
    
                item.setItemStock(item.getItemStock() - i);
                int result = itemMapper.updateItem(item);
                if (result > 0) {
    
    
                    log.info("generateOrder----成功");
                    return true;
                }
                log.warn("updateStock----item更新失败,受影响行数为:"+result);
                return false;
            }
            log.warn("updateStock----库存小于0");
            return false;
        }
        log.warn("updateStock----item为Null");
        return false;
    }

问题1.1000个用户同时下单,只有99个下单成功

控制台输出情况如下,推测应该是高并发的情况下,mysql数据库更新库存数据失败。
在这里插入图片描述
想到的解决思路

使用redis来对商品库存进行一个缓存,然后在redis中使用decr这一个操作来减少库存,当redis库存变成0的时候,更新数据库的库存。同时,用户下单成功后,在redis服务器中存储一个唯一标识,这样,进行秒杀时就不需要从数据库中查询订单了,直接在redis中查看是否有对应的key就行。

主要改变的是更新库存的方法,如下:

private boolean updateStock(Long itemId, int i) {
    
    
        String stockKey=MyproCostant.REDIS_STOCK_KEY+itemId;
        if (itemId!=null) {
    
    
            //判断是否有库存数据
            if (redisTemplate.hasKey(stockKey)) {
    
    
                Long stock = Long.valueOf(String.valueOf(redisTemplate.opsForValue().get(stockKey)));
                if (stock>0) {
    
    
                    //库存减一
                    Long decr_stock = redisTemplate.opsForValue().decrement(stockKey);
                    //再次判断
                    if (decr_stock>=0){
    
    
                        //库存为空时更新数据库
                        if (decr_stock==0){
    
    
                            SecItem item = itemMapper.selectItemByItemId(itemId);
                            item.setItemStock(0L);
                            int result = itemMapper.updateItem(item);
                            if (result!=1){
    
    
                                log.warn("库存更新失败");
                            }else {
    
    
                                log.info("库存已经为0");
                                //删除缓存的商品信息,使redis重新获取数据库的新数据
                                redisTemplate.delete(MyproCostant.REDIS_ITEM_KEY);
                            }
                        }
                        return true;
                    }
                }
            }
        }
        return false;
    }

在redis中存储的数据如下:
在这里插入图片描述
这种方法,测试到5000并发的时候,发现有一个userID下了两个单,在redis中order标识只有999个,在mysql中的确有一个userId有俩订单。
在这里插入图片描述
在这里插入图片描述
导致这样的原因应该是,同时有俩线程携带同一个userid同时进行一个插入订单的操作,解决方法想到使用redis的setnx来设置标识,修改后的更新库存代码如下:

private boolean updateStock(Long itemId,Long userId, int i) {
    
    
        String stockKey=MyproCostant.REDIS_STOCK_KEY+itemId;
        if (itemId!=null) {
    
    
            //判断是否有库存数据
            if (redisTemplate.hasKey(stockKey)) {
    
    
                Long stock = Long.valueOf(String.valueOf(redisTemplate.opsForValue().get(stockKey)));
                if (stock>0) {
    
    
                    //库存减一
                    Long decr_stock = redisTemplate.opsForValue().decrement(stockKey,i);
                    //再次判断
                    if (decr_stock>=0){
    
    
                        //插入购买标识,如果已存在
                        if (redisTemplate.opsForValue().setIfAbsent("order:"+userId+":"+itemId,1)) {
    
    
                            //库存为空时更新数据库
                            if (decr_stock==0){
    
    
                                SecItem item = itemMapper.selectItemByItemId(itemId);
                                item.setItemStock(0L);
                                int result = itemMapper.updateItem(item);
                                if (result!=1){
    
    
                                    log.warn("库存更新失败");
                                }else {
    
    
                                    log.info("库存已经为0");
                                    //删除缓存的商品信息,使redis重新获取数据库的新数据
                                    redisTemplate.delete(MyproCostant.REDIS_ITEM_KEY);
                                }
                            }
                            return true;
                        }else {
    
    
                            //如果购买标识已存在,则库存回退
                            redisTemplate.opsForValue().increment(stockKey,i);
                        }
                    }
                }
            }
        }
        return false;
    }

这种方法解决问题后,经过测试,发现redis的库存出现超卖的情况。

问题2.解决超卖

在redis的库存减少时同时有多个线程执行减少操作,导致超卖问题,解决思路为使用redis的乐观锁(watch) 监视库存的key,然后使用事务进行减少库存操作。
修改过的更新库存方法的代码如下:

private boolean updateStock(Long itemId, Long userId, int i) {
    
    
        String stockKey = MyproCostant.REDIS_STOCK_KEY + itemId;
        if (itemId != null) {
    
    
            //判断是否有库存数据
            if (redisTemplate.hasKey(stockKey)) {
    
    
                Long stock = redisUtils.getLong(stockKey);
                if (stock != null && stock > 0) {
    
    
                    //redis事务
                    List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>() {
    
    
                        @Override
                        public <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {
    
    
                            redisTemplate.watch(stockKey);
                            operations.multi();
                            redisTemplate.opsForValue().decrement(stockKey, i);
                            return operations.exec();
                        }
                    });
                    //事务提交成功则结果不为空
                    if (results != null && !results.isEmpty()) {
    
    
                        Long decr_stock = Long.parseLong(String.valueOf(redisTemplate.opsForValue().get(stockKey)));
                        if (decr_stock >= 0) {
    
    
                            //插入购买标识,如果已存在
                            if (redisTemplate.opsForValue().setIfAbsent("order:" + userId + ":" + itemId, 1, Duration.ofMinutes(30))) {
    
    
                                //库存为空时更新数据库
                                if (decr_stock == 0) {
    
    
                                    SecItem item = itemMapper.selectItemByItemId(itemId);
                                    item.setItemStock(0L);
                                    int result = itemMapper.updateItem(item);
                                    if (result != 1) {
    
    
                                        log.warn("库存更新失败");
                                    } else {
    
    
                                        log.info("库存已经为0");
                                        //删除缓存的库存数据
                                        redisTemplate.delete(stockKey);
                                        //删除缓存的商品信息
                                        redisTemplate.delete(MyproCostant.REDIS_ITEM_KEY);
                                    }
                                }
                                return true;
                            } else {
    
    
                                //如果购买标识已存在,则库存回退
                                redisTemplate.opsForValue().increment(stockKey, i);
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

猜你喜欢

转载自blog.csdn.net/qq_41120971/article/details/107551441