22.高并发秒杀

MySQL版本

1.数据准备

#抢购活动表
DROP TABLE IF EXISTS `voucher`;
CREATE TABLE `voucher`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `voucher_id` int(11) NULL DEFAULT NULL,
  `amount` int(11) NULL DEFAULT NULL,
  `start_time` datetime(0) NULL DEFAULT NULL,
  `end_time` datetime(0) NULL DEFAULT NULL,
  `is_valid` int(11) NULL DEFAULT NULL,
  `create_date` datetime(0) NULL DEFAULT NULL,
  `update_date` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
#订单表
DROP TABLE IF EXISTS `voucher_order`;
CREATE TABLE `voucher_order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(100) NULL DEFAULT NULL,
  `voucher_id` int(11) NULL DEFAULT NULL,
  `diner_id` int(11) NULL DEFAULT NULL,
  `status` tinyint(1) NULL DEFAULT NULL COMMENT '订单状态:-1=已取消 0=未支付 1=已支付 2=已消费 3=已过期',
  `order_type` int(11) NULL DEFAULT NULL COMMENT '订单类型:0=正常订单 1=抢购订单',
  `create_date` datetime(0) NULL DEFAULT NULL,
  `update_date` datetime(0) NULL DEFAULT NULL,
  `is_valid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

插入抢购活动

http://localhost:8080/seckill/addSeckillVoucher
{
    "voucherId":1,
    "amount":100,
    "startTime":"2023-01-30 08:00:00",
    "endTime":"2023-03-30 08:00:00"
}

2.超卖(库存为负数)

@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
    //参数校验
    if (Objects.isNull(voucherOrderParams)) {
        return ServerResponse.createByErrorMessage("参数错误");
    }

    Integer voucherId = voucherOrderParams.getVoucherId();
    if (Objects.isNull(voucherId) || voucherId < 0) {
        return ServerResponse.createByErrorMessage("请选择需要抢购的代金券");
    }

    Integer dinerId = voucherOrderParams.getDinerId();
    if (Objects.isNull(dinerId) || dinerId < 0) {
        return ServerResponse.createByErrorMessage("食客参数错误");
    }

    //判断此代金券是否加入抢购
    Voucher voucher = voucherMapper.findVoucherByVoucherId(voucherId);
    if (Objects.isNull(voucher)) {
        return ServerResponse.createByErrorMessage("该代金券并未有抢购活动");
    }

    //判断是否有效
    if (voucher.getIsValid() == 0) {
        return ServerResponse.createByErrorMessage("该活动已结束");
    }

    //判断是否开始、结束
    Date now = new Date();
    if (now.before(voucher.getStartTime())) {
        return ServerResponse.createByErrorMessage("该抢购还未开始");
    }
    if (now.after(voucher.getEndTime())) {
        return ServerResponse.createByErrorMessage("该抢购已结束");
    }

    //判断是否卖完
    /**
      * 超卖问题产生原因
      * 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
      * 会扣120次库存,同一个人产生120个订单
      */
    if (voucher.getAmount() < 1) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    /**
     * 一个人多单问题产生原因
     * 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
     * 会扣20次库存,同一个人产生20个订单
     */
    int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
    if (voucherOrderCountFromDb > 0) {
        return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
    }

    //扣库存
    int count = voucherMapper.decreaseStock(voucherId);
    if (count == 0) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //下单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setDinerId(dinerId);
    voucherOrder.setVoucherId(voucherId);
    String orderNo = UUID.randomUUID().toString();
    voucherOrder.setOrderNo(orderNo);
    voucherOrder.setOrderType(1);
    voucherOrder.setStatus(0);
    int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
    if (saveCount == 0) {
        return ServerResponse.createByErrorMessage("用户抢购失败");
    }
    return ServerResponse.createBySuccessMessage("抢购成功");
}

3.一人多单问题

@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
    //......
    //判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    /**
       * 一个人多单问题产生原因
       * 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
       * 会扣20次库存,同一个人产生20个订单
       */
    int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
    if (voucherOrderCountFromDb > 0) {
        return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
    }

    //扣库存
    int count = voucherMapper.decreaseStock(voucherId);
    if (count == 0) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //下单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setDinerId(dinerId);
    voucherOrder.setVoucherId(voucherId);
    String orderNo = UUID.randomUUID().toString();
    voucherOrder.setOrderNo(orderNo);
    voucherOrder.setOrderType(1);
    voucherOrder.setStatus(0);
    int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
    if (saveCount == 0) {
        return ServerResponse.createByErrorMessage("用户抢购失败");
    }
    //......
}

Redis版本

1.数据准备

#订单表
DROP TABLE IF EXISTS `voucher_order`;
CREATE TABLE `voucher_order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(100) NULL DEFAULT NULL,
  `voucher_id` int(11) NULL DEFAULT NULL,
  `diner_id` int(11) NULL DEFAULT NULL,
  `status` tinyint(1) NULL DEFAULT NULL COMMENT '订单状态:-1=已取消 0=未支付 1=已支付 2=已消费 3=已过期',
  `order_type` int(11) NULL DEFAULT NULL COMMENT '订单类型:0=正常订单 1=抢购订单',
  `create_date` datetime(0) NULL DEFAULT NULL,
  `update_date` datetime(0) NULL DEFAULT NULL,
  `is_valid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

插入抢购活动

http://localhost:8080/seckill/addSeckillVoucher
{
    "voucherId":1,
    "amount":100,
    "startTime":"2023-01-30 08:00:00",
    "endTime":"2023-03-30 08:00:00"
}

3.超卖(库存为负数)
(1).问题分析

@Transactional(rollbackFor = Exception.class)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
    //......

    //判断是否卖完
    /**
     * 超卖问题产生原因
     * 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
     * 会扣120次库存,同一个人产生120个订单
     */
    if (voucher.getAmount() < 1) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    /**
     * 一个人多单问题产生原因
     * 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
     * 会扣20次库存,同一个人产生20个订单
     */
    int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
    if (voucherOrderCountFromDb > 0) {
        return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
    }

    //扣库存
    /**
     * 库存扣为负数问题产生原因
     * 先查后扣两步操作,不是原子操作
     * 使用Redis+lua解决超卖问题
     */
    long count = redisTemplate.opsForHash().increment(key,"amount",-1);

    //下单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setDinerId(dinerId);
    voucherOrder.setVoucherId(voucherId);
    String orderNo = UUID.randomUUID().toString();
    voucherOrder.setOrderNo(orderNo);
    voucherOrder.setOrderType(1);
    voucherOrder.setStatus(0);
    int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
    if (saveCount == 0) {
        return ServerResponse.createByErrorMessage("用户抢购失败");
    }
    return ServerResponse.createBySuccessMessage("抢购成功");
}

(2).解决方案

-- KEYS[1]指的voucher:1,KEYS[2]指的是amount
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;
@Configuration
public class RedisTemplateConfiguration {
    @Bean
    public DefaultRedisScript<Long> stockScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //放在和application.yml 同层目录下
        redisScript.setLocation(new ClassPathResource("stock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}
@Transactional(rollbackFor = Exception.class)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
    //......
    //判断是否卖完
    /**
     * 超卖问题产生原因
     * 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
     * 会扣120次库存,同一个人产生120个订单
     */
    if (voucher.getAmount() < 1) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    /**
     * 一个人多单问题产生原因
     * 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
     * 会扣20次库存,同一个人产生20个订单
     * 扣库存和下订单上锁解决一人多单问题
     */
    int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
    if (voucherOrderCountFromDb > 0) {
        return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
    }

    //扣库存
    /**
     * 库存扣为负数问题产生原因
     * 先查后扣两步操作,不是原子操作
     * 使用Redis+lua解决超卖问题
     */
    List<String> keys = new ArrayList<>();
    keys.add(key);
    keys.add("amount");
    Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
    if (amount == 0) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //下单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setDinerId(dinerId);
    voucherOrder.setVoucherId(voucherId);
    String orderNo = UUID.randomUUID().toString();
    voucherOrder.setOrderNo(orderNo);
    voucherOrder.setOrderType(1);
    voucherOrder.setStatus(0);
    int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
    if (saveCount == 0) {
        return ServerResponse.createByErrorMessage("用户抢购失败");
    }
    return ServerResponse.createBySuccessMessage("抢购成功");
}

3.一人多单问题
(1).问题分析

@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
    //......
    //判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    /**
       * 一个人多单问题产生原因
       * 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
       * 会扣20次库存,同一个人产生20个订单
       */
    int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
    if (voucherOrderCountFromDb > 0) {
        return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
    }

    //扣库存
    List<String> keys = new ArrayList<>();
    keys.add(key);
    keys.add("amount");
    Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
    if (count == 0) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //下单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setDinerId(dinerId);
    voucherOrder.setVoucherId(voucherId);
    String orderNo = UUID.randomUUID().toString();
    voucherOrder.setOrderNo(orderNo);
    voucherOrder.setOrderType(1);
    voucherOrder.setStatus(0);
    int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
    if (saveCount == 0) {
        return ServerResponse.createByErrorMessage("用户抢购失败");
    }
    //......
}

(2).解决方案

@Transactional(rollbackFor = Exception.class)
@Override
public ServerResponse doSeckill(VoucherOrderParams voucherOrderParams) {
    //......
    //判断是否卖完
    /**
     * 超卖问题产生原因
     * 高并发情况下,比如120个请求同时过来,读取库存大于0,那么都会正常执行下去
     * 会扣120次库存,同一个人产生120个订单
     */
    if (voucher.getAmount() < 1) {
        return ServerResponse.createByErrorMessage("该券已经卖完了");
    }

    //判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
    /**
     * 一个人多单问题产生原因
     * 高并发情况下,比如20个请求同时过来,读取记录为空,那么都会正常执行下去
     * 会扣20次库存,同一个人产生20个订单
     * 扣库存和下订单上锁解决一人多单问题
     */
    int voucherOrderCountFromDb = voucherOrderMapper.findVoucherOrderByDinerIdAndVoucherId(voucherOrderParams);
    if (voucherOrderCountFromDb > 0) {
        return ServerResponse.createByErrorMessage("该用户已抢到该代金券,无需再抢");
    }

    //Redisson分布式锁,锁一个账号只能购买一次
    String lockName = RedisKeyConstant.lock_key.getKey() + dinerId + ":" + voucherId;
    long expireTime = voucher.getEndTime().getTime() - now.getTime();
    RLock lock = redissonClient.getLock(lockName);
    try {
        //Redisson分布式锁处理
        boolean isLocked = lock.tryLock(expireTime, TimeUnit.MILLISECONDS);
        if (isLocked) {
            //扣库存
            /**
             * 库存扣为负数问题产生原因
             * 先查后扣两步操作,不是原子操作
             * 使用Redis+lua解决超卖问题
             */
            List<String> keys = new ArrayList<>();
            keys.add(key);
            keys.add("amount");
            Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
            if (amount == 0) {
                return ServerResponse.createByErrorMessage("该券已经卖完了");
            }

            //下单
            VoucherOrder voucherOrder = new VoucherOrder();
            voucherOrder.setDinerId(dinerId);
            voucherOrder.setVoucherId(voucherId);
            String orderNo = UUID.randomUUID().toString();
            voucherOrder.setOrderNo(orderNo);
            voucherOrder.setOrderType(1);
            voucherOrder.setStatus(0);
            int saveCount = voucherOrderMapper.saveVoucherOrder(voucherOrder);
            if (saveCount == 0) {
                return ServerResponse.createByErrorMessage("用户抢购失败");
            }
        }
    }catch (Exception e) {
        //手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        //Redisson解锁
        lock.unlock();
    }
    return ServerResponse.createBySuccessMessage("抢购成功");
}

Jmeter使用

1.刷单
(1).添加线程组
在这里插入图片描述
在这里插入图片描述

(2).添加HTTP请求
在这里插入图片描述
在这里插入图片描述

(3).添加HTTP请求头信息
在这里插入图片描述
在这里插入图片描述

(4).查看结果树
在这里插入图片描述
在这里插入图片描述

(5).保存测试计划并进行测试
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.500用户并发
(1).添加线程组
在这里插入图片描述

(2).添加HTTP请求
dinnerId变量需要从CSV文件中读取。
在这里插入图片描述

{
    "voucherId":1,
    "dinerId":${dinerId}
}

(3).新建CSV文件
第一行为定义在请求体的变量。
在这里插入图片描述
在这里插入图片描述

(4).添加CSV数据文件设置
在这里插入图片描述
在这里插入图片描述

(5).添加HTTP请求头信息
在这里插入图片描述

(6).查看结果树
在这里插入图片描述

(7).保存测试计划并进行测试
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值