秒杀专题-秒杀系统怎么支持高并发而又不影响其他业务?快速响应式秒杀系统设计方案

1.秒杀介绍

最大特点就是瞬时高并发,针对这一特点必须要做到 限流+异步+缓存+独立部署

2. 提前准备

2.1 定时上架秒杀商品

可以在每天凌晨通过定时任务提前上架秒杀商品,然后讲上架的商品存到redis中,秒杀的时候就可以不用经过数据库了。设置分布式信号量作为扣减库存的依据,避免超卖现象发生。这里先简单描述下流程,涉及到的技术及具体实现方案下面会提到。
秒杀专题-秒杀系统怎么支持高并发而又不影响其他业务?快速响应式秒杀系统设计方案_第1张图片

2.2 定时任务

在SpringBoot中使用定时任务,可以使用Quartz框架,也可以使用SpringBoot自带的注解,我们使用注解的方式完成。

2.2.1 SpringBoot中使用定时任务

整合步骤

  1. 在启动类或配置文件中加上注解**@EnableScheduling**,开启定时任务。
  2. 在需要定时执行的方法上加上注解**@Scheduled(cron = “* * * * * ?”)**

注意事项

  • cron原生表达式支持7为,第7位代表年,而在SpringBoot中cron表达式不支持年
  • cron原生表达式1-7代表周日到周六,而在SpringBoot中是1-7代表周一到周日

案例演示

这种方式默认是阻塞的,如果当前任务执行时间过长,可能会影响下次任务的执行。不明白的可以看以下测试代码,模拟业务长时间执行的情况。

//每秒钟执行一次
@Scheduled(cron = "* * * * * ?")
public void test() throws InterruptedException {
    log.info("hello");
    Thread.sleep(3000);
}

在这里插入图片描述
由图可以看出,本应该间隔1秒执行的任务,却变成了间隔4秒。因此要解决这种问题就需要异步的去执行或者开启多线程,不能让业务影响任务的执行。由于多线程的方式在某些版本里支持的不友好,会不起作用,因此我们还是使用异步的方式来解决。

使用异步的话可以在方法内让业务以异步的方式执行,也可以让定时任务异步执行。

2.2.2 SpringBoot中使用异步任务

  • 在启动类或配置文件中加上注解@EnableAsync,开启异步任务
  • 在需要异步执行的方法上加上注解@Async
//每秒钟执行一次
@Async
@Scheduled(cron = "* * * * * ?")
public void test() throws InterruptedException {
    log.info("hello");
    Thread.sleep(3000);
}

秒杀专题-秒杀系统怎么支持高并发而又不影响其他业务?快速响应式秒杀系统设计方案_第2张图片
加上异步任务后,可以看到方法不再阻塞。
异步任务默认的核心线程数是8,最大线程数是Integer.MAX_VALUE,最大线程数太大了,最好可以给个限制50或100什么的。
所有的设置都可以参考org.springframework.boot.autoconfigure.task.TaskExecutionProperties

3. 秒杀注意事项

3.1 独立部署

保证秒杀业务即使扛不住高并发,宕机了,也不会影响别的业务。

3.2 请求加密

防止恶意请求,如果别人提前知道了秒杀连接,通过工具大量请求的话,可能会给秒杀系统大量较大的压力,因此给每个商品加上随机码,只有活动开始的时候才能获取到随机码。
上架秒杀商品的时候最对每个商品设置一个随机码,可以存到redis中。只有秒杀活动开始的时候,前端才能获取到该随机码。秒杀的时候验证随机码是否正确,正确才可以执行秒杀流程。

3.3 库存预热、快速扣减

秒杀活动读多写少,无需实时去数据库查询库存。秒杀商品定时上架的时候库存提前存到redis中,通过分布式信号量来控制秒杀请求,信号量的值就设置为商品的秒杀库存。

3.4 恶意请求拦截

在网关层面,讲高频请求,参数没有随机码的请求等各种异常的请求都拦截掉

3.5 流量错峰

使用各种手段,尽量将请求的时间节点分散开来。比如走购物车,验证码等。

3.6 限流、熔断、降级

限流包括前端限流和后端限流,前端限流比如只能点击一次或者某段时间内只能点击一次。
后端限流、熔断、降级都可以通过Sentinel组件实现。

3.7 队列削峰、服务解耦

秒杀的时候通过信号量进行库存的控制,秒杀成功,直接给前端返回成功信息,然后给消息队列发送一条消息。这样就不需要操作数据库,可以支持更高的并发量。然后订单服务监听队列
慢慢进行下单、扣库存即可。

4. 秒杀流程

秒杀专题-秒杀系统怎么支持高并发而又不影响其他业务?快速响应式秒杀系统设计方案_第3张图片

代码片段

@Override
public String kill(String killId, String key, Integer num) {
     //获取当前登录用户
     LoginUser loginUser = LoginUserInterceptor.threadLocal.get();
     BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(RedisConstant.KEY_SKUKILL_SKUS);
     String json = hashOps.get(killId);
     if (StringUtils.isEmpty(json))
         return null;

     SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
     Long startTime = redis.getStartTime();
     Long endTime = redis.getEndTime();
     long current = new Date().getTime();
     //活动剩余时间
     long ttl = endTime - current;

     //1、校验时间
     if (current < startTime || current > endTime)
         return null;

     //2.幂等性验证,活动时间段内购买过就不能再买了
     String randomCode = redis.getRandomCode();
     //sessionId_skuId
     String redisSkuKey = redis.getPromotionSessionId() + "_" + redis.getSkuId();

     //3.校验随机码和商品Id
     if (!randomCode.equals(key) || !redisSkuKey.equals(killId))
         return null;

     //4.校验限购数量
     if (num > redis.getSeckillLimit())
         return null;

     //5.判断是否已经购买过,幂等性验证。秒杀的时候先占位。userId_sessionId_skuId,再次秒杀的时候就会占位失败
     String redisKey = RedisConstant.PRE_KEY_SKUKILL_HISTORY + loginUser.getId() + "_" + redisSkuKey;
     //ttl时间后,占位的key会自动删除。key是userId_sessionId_skuId,value是购买的数量
     Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
     //占位失败,当前时间段内已经购买过该商品了。
     if (!ifAbsent) {
         return null;
     }
     //占位成功,说明没有买过
     RSemaphore semaphore = redissonClient.getSemaphore(RedisConstant.PRE_KEY_SKUKILL_SKU_STOCK_SEMAPHORE + randomCode);

     //扣减信号量,不阻塞
     boolean b;
     try {
         b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
         if (!b)
             return null;

         //所有校验都通过,正式生成订单
         String orderSn = IdWorker.getTimeId();
         SeckillOrderTo orderTo = new SeckillOrderTo();
         orderTo.setOrderSn(orderSn);
         orderTo.setMemberId(loginUser.getId());
         orderTo.setNum(num);
         orderTo.setSkuId(redis.getSkuInfo().getSkuId());
         orderTo.setPromotionSessionId(redis.getPromotionSessionId());
         orderTo.setSeckillPrice(redis.getSeckillPrice());

         try {
             rabbitTemplate.convertAndSend(MQConstant.ORDER_EVENT_EXCHANGE, MQConstant.ORDER_SECKILL_ORDER_KEY, orderTo);
         } catch (AmqpException e) {
             //消息发送失败,秒杀失败
             return null;
         }
         return orderSn;
     } catch (InterruptedException e) {
         return null;
     }
 }

你可能感兴趣的:(微服务全家桶,rabbitmq,微服务,java)