H5微服务商城商品并发秒杀

    技术2022-07-10  96

    秒杀商品列表

    获取秒杀商品列表的条件,商品的开始和截止时间在当前时间段,并且商品剩余量要大于0

    通过sql获取商品list返回客户端

    public interface ItemKillMapper extends Mapper<ItemKill> { @Select(" SELECT\n" + " a.*,\n" + " b.name AS itemName,\n" + " (\n" + " CASE WHEN (now() BETWEEN a.start_time AND a.end_time AND a.total > 0)\n" + " THEN 1\n" + " ELSE 0\n" + " END\n" + " ) AS canKill\n" + " FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id\n" + " WHERE a.is_active = 1") List<ItemKill> selectList();

    抢购controller

    public ResponseEntity<Object> sha(String id, @CookieValue("COOKNA") String token, HttpServletRequest request, HttpServletResponse response){ //参数校验 //用户登录? UserInfo userInfo = null; try{ //1.从token中解析token信息 userInfo = JwtUtils.getInfoFromToken(token,this.properties.getPublicKey()); //刷新cookie的token时间 //下单 System.out.println("id---------》"+userInfo.getId()); Boolean state = killService.killItem(Integer.parseInt(id),userInfo.getId()); if(!state){ // return ResponseEntity.ok("商品已抢购完毕或者不在抢购时间段哦!"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // return ResponseEntity.ok("抢购成功"); //获取订单code返回 ItemKillSuccess itemKillSuccess = new ItemKillSuccess(); itemKillSuccess.setKillId(Integer.parseInt(id)); itemKillSuccess.setUserId(userInfo.getId()); ItemKillSuccess order = itemKillSuccessServiceiImpl.selectItemOrder(itemKillSuccess); return new ResponseEntity<>(order.getCode(), HttpStatus.CREATED); }catch (Exception e){ e.printStackTrace(); } //5.出现异常,相应401 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); }

    客户端携带商品id请求下单接口,从token中获取到用户信息

    下单service

    public Boolean killItem(Integer killId,long id) { Boolean result=false; //订单查询该用户是否已经买过,yes-》false0 if (itemKillSuccessService.countByKillUserId(killId,id) <= 0){ //分布式锁 final String key=new StringBuffer().append(killId).append(id).append("-RedisLock").toString(); final String value=UUID.randomUUID().toString(); System.out.println(key+"------"+value); Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, value); if(aBoolean){ redisTemplate.expire(key,30, TimeUnit.SECONDS); try { ItemKill itemKill = itemKillMapper.selectByPrimaryKey(killId); //判断该商品是否可以秒杀 if (itemKill !=null && 1 == itemKill.getIsActive()){ //库存-1,在库存大于1的情况下 int i = itemKillMapper.updateKillItem(killId); //>0减库存成功,通知订单入库,mq发送成信息 if(i>0){ //订单 ItemKill itemKill1 = itemKillMapper.selectByPrimaryKey(killId); commonRecordKillSuccessInfo(itemKill1,id); result = true; return result; } } } catch (Exception e) { e.printStackTrace(); } finally { if(value.equals(redisTemplate.opsForValue().get(key))){ redisTemplate.delete(key); }; } } }else { System.out.println("您已经抢购过该商品了!"); } return result; }

    第一步我们去订单表查询,用户是否已经下单,秒杀商品我们做限制,每个用户只能买一件

    @Select(" SELECT\n" + " COUNT(1) AS total\n" + " FROM\n" + " item_kill_success\n" + " WHERE\n" + " user_id = #{userId}\n" + " AND kill_id = #{killId}\n" + " AND status in (0,1)") int countByKillUserId(@Param("killId") Integer killId, @Param("userId") long userId);

    sql语句判断用户订单表的数量,如果>1,表示用户已经购买了,我们把这些用户先排除

    第二步加了redis分布式锁,这里为什么加暂时不说,放到后面

    第三步我们查询该商品的状态是否可以秒杀,确定可以秒杀我们进入下一步

    第四步我们扣减库存

    @Update(" UPDATE item_kill\n" + " SET total = total - 1\n" + " WHERE id = #{killId} AND total>0") int updateKillItem(Integer killId);

    mysql数据库在update操作时,会自动启动锁机制,在并发的情况可以防止出现负数的情况,当商品库存量为0的时候,过滤掉接下来的请求

    第五步库存扣减成功后,进入下单的流程

    private void commonRecordKillSuccessInfo(ItemKill kill,long id){ ItemKillSuccess entity=new ItemKillSuccess(); entity.setCode(UUID.randomUUID().toString());//订单 entity.setItemId(kill.getItemId()); entity.setKillId(kill.getId()); System.out.println("------------"+id); entity.setUserId(id); entity.setStatus(0); entity.setCreateTime(new Date()); entity.setJiage(kill.getJiage()); int i = itemKillSuccessService.insertKill(entity); if(1>0){ System.out.println("=========>成功====>发送私信队列订单编码"+entity.getCode()); //成功发送mq //添加私信队列处理超时的订单 rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE_NAME, RabbitMQConfig.DELAY_QUEUEB_ROUTING_KEY, entity.getCode()); } }

    订单分为两步

    第一步生成订单数据,包括用户信息和商品信息,此时的订单是还没有支付的状态

    一般我们下单之后会马上去支付,但也会出现下单了暂时不支付的行为,所以生成订单之后还有付款并修改订单状态的操作。

    下单后立马付款,我们只需要在付款成功后修改订单状态就可以,这里不需要过多操作,需要操作是下单之后一段时间后再付款的行为。

    现在我们假定现在一个商品秒杀流程已经完成,从下单扣减库存到生成订单这个流程已经完成,我们回到刚才分布式锁的问题。

    为什么要在下订单的过程加上锁的操作呢?

    因为我在进行并发压测时的时候出现了一个问题,通过数据的锁机制虽然没有出现超卖的情况的,但是出现了同一个用户抢到两件以上商品的情况。

    为什么会出现这个问题呢,原因是我们虽然在下单开始的时候的时候判断了订单表还没有下单,但是下单也需要一定的时间才能在订单插入数据,如果在这个过程中同一个用户又发起了一次下单请求呢?

    这个时候订单表依然没有该用户的订单,所以第二次请求又进行来,继而生成了第二件订单

    所以我们将下单的操作加锁,保证下单到订单表成功插入时间的过程,只有当前一个操作。这样即使一个用户并发很多次请求,但是下单的过程中只有一个,该用户下单已成功,那接下来的操作订单表就已经有该用户的订单,就不会继续生成新订单了。

    订单结束我们删除掉redis的key,虽然key有过期时间,但是订单已经完成不需要再等了,直接删除key,然后进行下一个用户下单。

    这样通过分布式锁和数据库的库,保证了一个用户只能成功一个订单,并且不会出现超卖的情况。

    下面我们再说用户下单,但迟迟没有付款的问题

    生成订单后,肯定不能一直等着用户付款,现实生活中的订单都会有一个过期时间,一般是20—30分钟。接下来我们就开始订单一段时间自动失效的操作。

    这里想了两个方案:

    1开启一个定时任务,去查询订单,如果过了到期时间我们修改订单状态为失效

    这种方案存在的问题是,假如每5分钟去查询一次,但这5分钟之类失效的订单,就只能等下一次查询才能修改,存在时效性的问题

    2使用mq死信队列,下单的同时发送一个mq消息,过期时间一到立马查询订单状态,还没有付款立马取消

    这种方案就增加了系统的复杂度,毕竟多加了一个mq组件,考虑的问题就变多了,比如mq服务挂了怎么办

    每种方案都有自己的利弊,这里用了mq队列的方式,纯粹为了学习一下mq的东西

    rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE_NAME, RabbitMQConfig.DELAY_QUEUEB_ROUTING_KEY, entity.getCode());

    这段代码在我们创建订单之后,使用sping提供的template发送一条该订单的消息到mq。关于死信队列的操作,本篇就不详细说了,网上相关信息很详细,我也是从网上搜集了一个demo之后,进行修改拿来用的,也可以下载项目源码看看。

     

     

    Processed: 0.015, SQL: 9