电商项目实战之商品秒杀

定时任务

corn表达式

  • 定时查询秒杀活动

    https://cron.qqe2.com/

实现方式

基于注解

  • 内容介绍

    基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

  • cron表达式参数

    
      
        Cron表达式参数分别表示:
    
        秒(0~59) 例如0/5表示每5秒
        分(0~59)
        时(0~23)
        日(0~31)的某天,需计算
        月(0~11)
        周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)
        @Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
        // Cron表达式范例:
    
        每隔5秒执行一次:*/5 * * * * ?
    
        每隔1分钟执行一次:0 */1 * * * ?
    
        每天23点执行一次:0 0 23 * * ?
    
        每天凌晨1点执行一次:0 0 1 * * ?
    
        每月1号凌晨1点执行一次:0 0 1 1 * ?
    
        每月最后一天23点执行一次:0 0 23 L * ?
    
        每周星期天凌晨1点实行一次:0 0 1 ? * L26分、29分、33分执行一次:0 26,29,33 * * * ?
    
        每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
    
    
    
    
    
  • 实现源码

    
      @Configuration      //1.主要用于标记配置类,兼备Component的效果。
      @EnableScheduling   // 2.开启定时任务
      public class SaticScheduleTask {
          //3.添加定时任务
          @Scheduled(cron = "0/5 * * * * ?")
          //或直接指定时间间隔,例如:5秒
          //@Scheduled(fixedRate=5000)
          private void configureTasks() {
              System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
          }
      }
    
    
    
    
    
    
  • 方案分析

    显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务。

基于接口

  • 引入依赖

    
      <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
          <version>2.0.4.RELEASE</version>
      </parent>
    
      <dependencies>
          <dependency><!--添加Web依赖 -->
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
          <dependency><!--添加MySql依赖 -->
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
          </dependency>
          <dependency><!--添加Mybatis依赖 配置mybatis的一些初始化的东西-->
              <groupId>org.mybatis.spring.boot</groupId>
              <artifactId>mybatis-spring-boot-starter</artifactId>
              <version>1.3.1</version>
          </dependency>
          <dependency><!-- 添加mybatis依赖 -->
              <groupId>org.mybatis</groupId>
              <artifactId>mybatis</artifactId>
              <version>3.4.5</version>
              <scope>compile</scope>
          </dependency>
      </dependencies>
    
    
    
    
    
  • 表结构创建

    
      DROP DATABASE IF EXISTS `socks`;
      CREATE DATABASE `socks`;
      USE `SOCKS`;
      DROP TABLE IF EXISTS `cron`;
      CREATE TABLE `cron`  (
        `cron_id` varchar(30) NOT NULL PRIMARY KEY,
        `cron` varchar(30) NOT NULL  
      );
      INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');
    
    
    
    
    
  • 开启定时任务

    
      @Configuration      //1.主要用于标记配置类,兼备Component的效果。
      @EnableScheduling   // 2.开启定时任务
      public class DynamicScheduleTask implements SchedulingConfigurer {
    
          @Mapper
          public interface CronMapper {
              @Select("select cron from cron limit 1")
              public String getCron();
          }
    
          @Autowired      //注入mapper
          @SuppressWarnings("all")
          CronMapper cronMapper;
    
          /**
          * 执行定时任务.
          */
          @Override
          public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    
              taskRegistrar.addTriggerTask(
                      //1.添加任务内容(Runnable)
                      () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
                      //2.设置执行周期(Trigger)
                      triggerContext -> {
                          //2.1 从数据库获取执行周期
                          String cron = cronMapper.getCron();
                          //2.2 合法性校验.
                          if (StringUtils.isEmpty(cron)) {
                              // Omitted Code ..
                          }
                          //2.3 返回执行周期(Date)
                          return new CronTrigger(cron).nextExecutionTime(triggerContext);
                      }
              );
          }
    
      }
    
    
    
  • 多线程定时任务

    
    
      //@Component注解用于对那些比较中立的类进行注释;
      //相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
      @Component
      @EnableScheduling   // 1.开启定时任务
      @EnableAsync        // 2.开启多线程
      public class MultithreadScheduleTask {
    
          @Async
          @Scheduled(fixedDelay = 1000)  //间隔1秒
          public void first() throws InterruptedException {
              System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
              System.out.println();
              Thread.sleep(1000 * 10);
          }
    
          @Async
          @Scheduled(fixedDelay = 2000)
          public void second() {
              System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
              System.out.println();
          }
      }
    
    
    
    

实战

  • Spring定时任务

    
    
      /**
      * 定时任务
      *      1、@EnableScheduling 开启定时任务(注解类上)
      *      2、@Scheduled开启一个定时任务(注解方法)
      *
      * 异步任务
      *      1、@EnableAsync:开启异步任务(注解类上)
      *      2、@Async:给希望异步执行的方法标注(注解方法)
      */
    
      @Slf4j
      // 开启异步任务
      //@EnableAsync
      //@EnableScheduling
      //@Component
      public class HelloSchedule {
    
        /**
        * 1、在Spring中表达式是6位组成,不允许第七位的年份
        * 2、在周几的的位置,1-7代表周一到周日
        * 3、定时任务不该阻塞。默认是阻塞的
        *      1)、可以让业务以异步的方式,自己提交到线程池
        *              CompletableFuture.runAsync(() -> {
        *         },execute);
        *
        *      2)、支持定时任务线程池;设置 TaskSchedulingProperties
        *        spring.task.scheduling.pool.size: 5
        *
        *      3)、让定时任务异步执行
        *        异步任务自动配置类 TaskExecutionAutoConfiguration
        *
        *      解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
        *
        */
        @Async
        @Scheduled(cron = "*/5 * * ? * 1")
        public void hello(){
          log.info("定时任务...");
          try {
            Thread.sleep(3000);
          } catch (InterruptedException e) { }
        }
      }
    
    
    
    
    
    

秒杀系统

秒杀系统关注问题

  • 服务单一职责+独立部署

    秒杀服务即使自己扛不住压力,挂掉,也不要影响别人

  • 秒杀链接加密

    防止恶意攻击,模拟秒杀请求,1000次/s攻击

    防止链接暴露,自己工作人员,提前秒杀商品。

  • 库存预热+快速扣减

    秒杀读多写少,无需每次实时校验库存。我们库存预热,放到redis中,通过信号量控制进来秒杀的请求

  • 动静分离

    nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用CDN网络,分担本集群压力

  • 恶意请求拦截

    识别非法攻击请求并进行拦截,网关层

  • 流量错峰

    使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车

  • 限流&熔断&降级

    前端限流+后端限流

    限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩

  • 队列削峰

    1万个商品,每个1000件秒杀。双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可

秒杀架构设计

  • 架构流程

    nginx -> gateway -> redis分布式信号量 -> 秒杀服务

    1. 独立部署:独立部署秒杀模块gulimall-seckill;

    2. 定时任务:每天三点上架最新秒杀商品,削减高峰期压力;

    3. 秒杀链接加密:为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口;

    4. 库存预热:先从数据库中扣除一部分库存以 redisson 信号量的形式存储在redis中

    5. 队列削峰:秒杀成功后立即返回,然后以发送消息的形式创建订单

  • redis数据存储设计

    秒杀活动:存储于scekill:sesssions这个redis-key里,value为 skuIds[]

    秒杀活动里具体商品项:是一个map,redis-key是seckill:skus,map-key是skuId+商品随机码

  • redis存储模型设计

    redis存储模型

    1. 秒杀场次存储的List可以当做hash key在SECKILL_CHARE_PREFIX中获得对应的商品数据;

    2. 随机码防止人在秒杀一开放就秒杀完,必须携带上随机码才能秒杀

    3. 结束时间;

    4. 设置分布式信号量,这样就不会每个请求都访问数据库。seckill:stock:#(商品随机码);

    5. session里存了session-sku[]的列表,而seckill:skus的key也是session-sku,不要只存储sku,不能区分场次

    秒杀活动场次存储数据效果图示

  • 案例代码设计

    Redis中存放的skuInfo的信息

    
        @Data
        public class SeckillSkuRedisTo {
    
            /**
            * 活动id
            */
            private Long promotionId;
            /**
            * 活动场次id
            */
            private Long promotionSessionId;
            /**
            * 商品id
            */
            private Long skuId;
            /**
            * 秒杀价格
            */
            private BigDecimal seckillPrice;
            /**
            * 秒杀总量
            */
            private Integer seckillCount;
            /**
            * 每人限购数量
            */
            private Integer seckillLimit;
            /**
            * 排序
            */
            private Integer seckillSort;
    
            //sku的详细信息
            private SkuInfoVo skuInfo;
    
            //当前商品秒杀的开始时间
            private Long startTime;
    
            //当前商品秒杀的结束时间
            private Long endTime;
    
            //当前商品秒杀的随机码
            private String randomCode;
        }
    
    
    

    Redis中存储Key-Value值

    
        // 存储的秒杀场次对应数据
        // K: SESSION_CACHE_PREFIX + startTime + "_" + endTime;
        // V: sessionId(活动场次id)+"-"+skuId(商品id)的List
        private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    
    
        // 存储的秒杀商品数据
        // K: 固定值SECKILL_CHARE_PREFIX
        // V: hash,k为sessionId(活动场次id)+"-"+skuId(商品id),v为对应的商品信息SeckillSkuRedisTo
        private final String SECKILL_CHARE_PREFIX = "seckill:skus";
    
    
        // K: SKU_STOCK_SEMAPHORE+商品随机码
        // V: 秒杀的库存件数
        private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; 
    
        // 使用库存作为分布式信号量
        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
        // 商品可以秒杀的数量作为信号量
        semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
    
    
    

商品上架

  • 定时上架

    1. 开启对定时任务支持

      
        @EnableAsync //开启对异步的支持,防止定时任务之间相互阻塞
        @EnableScheduling //开启对定时任务的支持
        @Configuration
        public class ScheduledConfig {
      
        }
      
      
      
      
    2. 每天凌晨三点远程调用coupon(优惠券)服务上架最近三天的秒杀商品;

    3. 由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法

    4. 上架后无需再次上架,用分布式锁做好幂等性

      
      
          /**
          * 秒杀商品定时上架
          *  每天晚上3点,上架最近三天需要三天秒杀的商品
          *  当天00:00:00 - 23:59:59
          *  明天00:00:00 - 23:59:59
          *  后天00:00:00 - 23:59:59
          */
      
          @Slf4j
          @Service
          public class SeckillScheduled {
      
              @Autowired
              private SeckillService seckillService;
      
              @Autowired
              private RedissonClient redissonClient;
      
              //秒杀商品上架功能的锁
              private final String upload_lock = "seckill:upload:lock";
      
              //TODO 保证幂等性问题
              // @Scheduled(cron = "*/5 * * * * ? ")
              @Scheduled(cron = "0 0 1/1 * * ? ")
              public void uploadSeckillSkuLatest3Days() {
                  //1、重复上架无需处理
                  log.info("上架秒杀的商品...");
      
                  //分布式锁
                  RLock lock = redissonClient.getLock(upload_lock);
                  try {
                      //加锁
                      lock.lock(10, TimeUnit.SECONDS);
                      seckillService.uploadSeckillSkuLatest3Days();
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
                      lock.unlock();
                  }
              }
          }
      
      
          /**
          * 秒杀服务接口实现类
          */
          @Slf4j
          @Service
          public class SeckillServiceImpl implements SeckillService {
      
              @Autowired
              private StringRedisTemplate redisTemplate;
      
              @Autowired
              private CouponFeignService couponFeignService;
      
              @Autowired
              private ProductFeignService productFeignService;
      
              @Autowired
              private RedissonClient redissonClient;
      
              @Autowired
              private RabbitTemplate rabbitTemplate;
      
              private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
      
              private final String SECKILL_CHARE_PREFIX = "seckill:skus";
      
              private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码
      
              @Override
              public void uploadSeckillSkuLatest3Days() {
      
                  //1、扫描最近三天的商品需要参加秒杀的活动
                  R lates3DaySession = couponFeignService.getLates3DaySession();
                  if (lates3DaySession.getCode() == 0) {
                      //上架商品
                      List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
                      });
                      //缓存到Redis
                      //1、缓存活动信息
                      saveSessionInfos(sessionData);
      
                      //2、缓存活动的关联商品信息
                      saveSessionSkuInfo(sessionData);
                  }
      
              }
          }
      
      
  • 获取最近三天的秒杀信息

    1. 获取最近三天的秒杀场次信息通过秒杀场次id查询对应的商品信息

    2. 防止集群多次上架

    
        @Override
        public List<SeckillSessionEntity> getLates3DaySession() {
    
            //计算最近三天
            //查出这三天参与秒杀活动的商品
            List<SeckillSessionEntity> list = this.baseMapper.selectList(new QueryWrapper<SeckillSessionEntity>()
                    .between("start_time", startTime(), endTime()));
    
            // 这里循环查表,应考虑优化
            if (list != null && list.size() > 0) {
                List<SeckillSessionEntity> collect = list.stream().map(session -> {
                    Long id = session.getId();
                    //查出sms_seckill_sku_relation表中关联的skuId
                    List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>()
                            .eq("promotion_session_id", id));
                    session.setRelationSkus(relationSkus);
                    return session;
                }).collect(Collectors.toList());
                return collect;
            }
    
            return null;
        }
    
        /**
        * 当前时间
        * @return
        */
        private String startTime() {
            LocalDate now = LocalDate.now();
            LocalTime min = LocalTime.MIN;
            LocalDateTime start = LocalDateTime.of(now, min);
    
            //格式化时间
            String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return startFormat;
        }
    
        /**
        * 结束时间
        * @return
        */
        private String endTime() {
            LocalDate now = LocalDate.now();
            LocalDate plus = now.plusDays(2);
            LocalTime max = LocalTime.MAX;
            LocalDateTime end = LocalDateTime.of(plus, max);
    
            //格式化时间
            String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return endFormat;
        }
    
    
    
    
  • Redis保存秒杀活动场次信息

    
      /**
      * 缓存秒杀活动信息
      * @param sessions
      */
      private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
    
          sessions.stream().forEach(session -> {
    
              //获取当前活动的开始和结束时间的时间戳
              long startTime = session.getStartTime().getTime();
              long endTime = session.getEndTime().getTime();
    
              //存入到Redis中的key
              String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
    
              //判断Redis中是否有该信息,如果没有才进行添加
              Boolean hasKey = redisTemplate.hasKey(key);
              //缓存活动信息
              if (!hasKey) {
                  //获取到活动中所有商品的skuId
                  List<String> skuIds = session.getRelationSkus().stream()
                          .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                  redisTemplate.opsForList().leftPushAll(key,skuIds);
              }
          });
    
      }
    
    
    
    
    
    
    
  • Redis保存秒杀商品信息

    
    
        /**
        * 缓存秒杀活动所关联的商品信息
        * @param sessions
        */
        private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
    
            sessions.stream().forEach(session -> {
                //准备hash操作,绑定hash
                BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                    //生成随机码
                    String token = UUID.randomUUID().toString().replace("-", "");
                    String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                    // 缓存中没有再添加
                    if (!operations.hasKey(redisKey)) {
    
                        //缓存我们商品信息
                        SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                        Long skuId = seckillSkuVo.getSkuId();
                        //1、先查询sku的基本信息,调用远程服务
                        R info = productFeignService.getSkuInfo(skuId);
                        if (info.getCode() == 0) {
                            SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
                            redisTo.setSkuInfo(skuInfo);
                        }
    
                        //2、sku的秒杀信息
                        BeanUtils.copyProperties(seckillSkuVo,redisTo);
    
                        //3、设置当前商品的秒杀时间信息
                        redisTo.setStartTime(session.getStartTime().getTime());
                        redisTo.setEndTime(session.getEndTime().getTime());
    
                        //4、设置商品的随机码(防止恶意攻击)
                        redisTo.setRandomCode(token);
    
                        //活动id-skuID   秒杀sku信息 序列化json格式存入Redis中
                        String seckillValue = JSON.toJSONString(redisTo);
                        operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);
    
                        //如果当前这个场次的商品库存信息已经上架就不需要上架
                        //5、使用库存作为分布式Redisson信号量(限流)
                        // 使用库存作为分布式信号量
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                        // 商品可以秒杀的数量作为信号量
                        semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                    }
                });
            });
        }
    
    
    
    
    
    
    

获取当前秒杀商品

  • 根据redis中缓存秒杀活动的各种信息,获取缓存中当前时间段在秒杀的sku

    
        /**
        * 获取到当前可以参加秒杀商品的信息
        * @return
        */
        @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
        @Override
        public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    
            try (Entry entry = SphU.entry("seckillSkus")) {
                //1、确定当前属于哪个秒杀场次
                long currentTime = System.currentTimeMillis();
    
                //从Redis中查询到所有key以seckill:sessions开头的所有数据
                Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
                for (String key : keys) {
                    //seckill:sessions:1594396764000_1594453242000
                    String replace = key.replace(SESSION_CACHE_PREFIX, "");
                    String[] s = replace.split("_");
                    //获取存入Redis商品的开始时间
                    long startTime = Long.parseLong(s[0]);
                    //获取存入Redis商品的结束时间
                    long endTime = Long.parseLong(s[1]);
    
                    //判断是否是当前秒杀场次
                    if (currentTime >= startTime && currentTime <= endTime) {
                        //2、获取这个秒杀场次需要的所有商品信息
                        List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                        BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                        assert range != null;
                        List<String> listValue = hasOps.multiGet(range);
                        if (listValue != null && listValue.size() >= 0) {
    
                            List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
                                String items = (String) item;
                                SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
                                // redisTo.setRandomCode(null);当前秒杀开始需要随机码
                                return redisTo;
                            }).collect(Collectors.toList());
                            return collect;
                        }
                        break;
                    }
                }
            } catch (BlockException e) {
                log.error("资源被限流{}",e.getMessage());
            }
    
            return null;
        }
    
    
    
    
    
    

获取当前商品的秒杀信息

  • 点击秒杀商品

    用户点击秒杀商品,如果时间段正确,返回随机码,购买时带着

    注意:不要redis-map中的key

    
        /**
        * 根据skuId查询商品是否参加秒杀活动
        * @param skuId
        * @return
        */
        @Override
        public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
    
            //1、找到所有需要秒杀的商品的key信息---seckill:skus
            BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
    
            //拿到所有的key
            Set<String> keys = hashOps.keys();
            if (keys != null && keys.size() > 0) {
                //4-45 正则表达式进行匹配
                String reg = "\\d-" + skuId;
                for (String key : keys) {
                    //如果匹配上了
                    if (Pattern.matches(reg,key)) {
                        //从Redis中取出数据来
                        String redisValue = hashOps.get(key);
                        //进行序列化
                        SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);
    
                        //随机码
                        Long currentTime = System.currentTimeMillis();
                        Long startTime = redisTo.getStartTime();
                        Long endTime = redisTo.getEndTime();
                        //如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间
                        if (currentTime >= startTime && currentTime <= endTime) {
                            return redisTo;
                        }
                        redisTo.setRandomCode(null);
                        return redisTo;
                    }
                }
            }
            return null;
        }
    
    
    
    
  • 查询秒杀对应信息

    注意所有的时间都是距离1970的差值

    
          /**
          * 根据skuId查询商品异步线程查询商品基本信息
          * @param skuId
          * @return
          */
          @Override
          public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
    
              SkuItemVo skuItemVo = new SkuItemVo();
    
              CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
                  //1、sku基本信息的获取  pms_sku_info
                  SkuInfoEntity info = this.getById(skuId);
                  skuItemVo.setInfo(info);
                  return info;
              }, executor);
    
    
              CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
                  //3、获取spu的销售属性组合
                  List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
                  skuItemVo.setSaleAttr(saleAttrVos);
              }, executor);
    
    
              CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
                  //4、获取spu的介绍    pms_spu_info_desc
                  SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
                  skuItemVo.setDesc(spuInfoDescEntity);
              }, executor);
    
    
              CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
                  //5、获取spu的规格参数信息
                  List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
                  skuItemVo.setGroupAttrs(attrGroupVos);
              }, executor);
    
    
              // Long spuId = info.getSpuId();
              // Long catalogId = info.getCatalogId();
    
              //2、sku的图片信息    pms_sku_images
              CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
                  List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId);
                  skuItemVo.setImages(imagesEntities);
              }, executor);
    
    
              CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
                  //3、远程调用查询当前sku是否参与秒杀优惠活动
                  R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
                  if (skuSeckilInfo.getCode() == 0) {
                      //查询成功
                      SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
                      });
                      skuItemVo.setSeckillSkuVo(seckilInfoData);
    
                      if (seckilInfoData != null) {
                          long currentTime = System.currentTimeMillis();
                          if (currentTime > seckilInfoData.getEndTime()) {
                              skuItemVo.setSeckillSkuVo(null);
                          }
                      }
                  }
              }, executor);
    
    
              //等到所有任务都完成
              CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();
    
              return skuItemVo;
          }
    
    
    
    

秒杀最终处理

  • 秒杀流程图示

    秒杀流程图示

  • 秒杀业务

    1. 点击立即抢购时,会发送请求;

    2. 秒杀会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单

  • 秒杀方案

    秒杀方案流程图示

  • 消息队列

    秒杀消息队列流程图_1

    秒杀消息队列流程图_2

  • 样例源码

    创建订单发消息

    
    
        /**
        * 当前商品进行秒杀(秒杀开始)
        * @param killId
        * @param key
        * @param num
        * @return
        */
        @Override
        public String kill(String killId, String key, Integer num) throws InterruptedException {
    
            long s1 = System.currentTimeMillis();
            //获取当前用户的信息
            MemberResponseVo user = LoginUserInterceptor.loginUser.get();
    
            //1、获取当前秒杀商品的详细信息从Redis中获取
            BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            String skuInfoValue = hashOps.get(killId);
            if (StringUtils.isEmpty(skuInfoValue)) {
                return null;
            }
            //(合法性效验)
            SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
            Long startTime = redisTo.getStartTime();
            Long endTime = redisTo.getEndTime();
            long currentTime = System.currentTimeMillis();
            //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
            if (currentTime >= startTime && currentTime <= endTime) {
    
                //2、效验随机码和商品id
                String randomCode = redisTo.getRandomCode();
                String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
                if (randomCode.equals(key) && killId.equals(skuId)) {
                    //3、验证购物数量是否合理和库存量是否充足
                    Integer seckillLimit = redisTo.getSeckillLimit();
    
                    //获取信号量
                    String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                    Integer count = Integer.valueOf(seckillCount);
                    //判断信号量是否大于0,并且买的数量不能超过库存
                    if (count > 0 && num <= seckillLimit && count > num ) {
                        //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
                        //SETNX 原子性处理
                        String redisKey = user.getId() + "-" + skuId;
                        //设置自动过期(活动结束时间-当前时间)
                        Long ttl = endTime - currentTime;
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            //占位成功说明从来没有买过,分布式锁(获取信号量-1)
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            //TODO 秒杀成功,快速下单
                            boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                            //保证Redis中还有商品库存
                            if (semaphoreCount) {
                                //创建订单号和订单信息发送给MQ
                                // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                                String timeId = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(user.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                                orderTo.setSkuId(redisTo.getSkuId());
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                                long s2 = System.currentTimeMillis();
                                log.info("耗时..." + (s2 - s1));
                                return timeId;
                            }
                        }
                    }
                }
            }
            long s3 = System.currentTimeMillis();
            log.info("耗时..." + (s3 - s1));
            return null;
        }
    
    
    
    
    

    创建秒杀消息队列

    
        /**
        * 商品秒杀队列
        * @return
        */
        @Bean
        public Queue orderSecKillOrrderQueue() {
            Queue queue = new Queue("order.seckill.order.queue", true, false, false);
            return queue;
        }
    
    
        @Bean
        public Binding orderSecKillOrrderQueueBinding() {
            //String destination, DestinationType destinationType, String exchange, String routingKey,
            // 			Map<String, Object> arguments
            Binding binding = new Binding(
                    "order.seckill.order.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.seckill.order",
                    null);
    
            return binding;
        }
    
    
    
    

    消息接收监听队列

    
        @Slf4j
        @Component
        @RabbitListener(queues = "order.seckill.order.queue")
        public class OrderSeckillListener {
    
            @Autowired
            private OrderService orderService;
    
            @RabbitHandler
            public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
    
                log.info("准备创建秒杀单的详细信息...");
    
                try {
                    orderService.createSeckillOrder(orderTo);
                    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
                } catch (Exception e) {
                    channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
                }
    
            }
    
        }
    
    
    
    
    
    
    

    创建秒杀订单

    
    
        /**
        * 创建秒杀单
        * @param orderTo
        */
        @Override
        public void createSeckillOrder(SeckillOrderTo orderTo) {
    
            //TODO 保存订单信息
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setOrderSn(orderTo.getOrderSn());
            orderEntity.setMemberId(orderTo.getMemberId());
            orderEntity.setCreateTime(new Date());
            BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
            orderEntity.setPayAmount(totalPrice);
            orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    
            //保存订单
            this.save(orderEntity);
    
            
    
            //保存商品的spu信息
            R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
    
            if (spuInfo.getCode() == 0) {
              
              SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {});
              
              //保存订单项信息
              OrderItemEntity orderItem = new OrderItemEntity();
              orderItem.setOrderSn(orderTo.getOrderSn());
              orderItem.setRealAmount(totalPrice);
              orderItem.setSkuQuantity(orderTo.getNum());
              
    
    
              orderItem.setSpuId(spuInfoData.getId());
              orderItem.setSpuName(spuInfoData.getSpuName());
              orderItem.setSpuBrand(spuInfoData.getBrandName());
              orderItem.setCategoryId(spuInfoData.getCatalogId());
    
              //保存订单项数据
              orderItemService.save(orderItem);
            }
            
        }
    
    
    

    统一响应实体

    
    
      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.TypeReference;
      import org.apache.http.HttpStatus;
    
      import java.util.HashMap;
      import java.util.Map;
    
      /**
      * 返回数据
      *
      */
      public class R extends HashMap<String, Object> {
        private static final long serialVersionUID = 1L;
    
        public R setData(Object data) {
          put("data",data);
          return this;
        }
    
        //利用fastjson进行反序列化
        public <T> T getData(TypeReference<T> typeReference) {
          Object data = get("data");	//默认是map
          String jsonString = JSON.toJSONString(data);
          T t = JSON.parseObject(jsonString, typeReference);
          return t;
        }
    
        //利用fastjson进行反序列化
        public <T> T getData(String key,TypeReference<T> typeReference) {
          Object data = get(key);	//默认是map
          String jsonString = JSON.toJSONString(data);
          T t = JSON.parseObject(jsonString, typeReference);
          return t;
        }
    
        public R() {
          put("code", 0);
          put("msg", "success");
        }
        
        public static R error() {
          return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
        }
        
        public static R error(String msg) {
          return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
        }
        
        public static R error(int code, String msg) {
          R r = new R();
          r.put("code", code);
          r.put("msg", msg);
          return r;
        }
    
        public static R ok(String msg) {
          R r = new R();
          r.put("msg", msg);
          return r;
        }
        
        public static R ok(Map<String, Object> map) {
          R r = new R();
          r.putAll(map);
          return r;
        }
        
        public static R ok() {
          return new R();
        }
    
        public R put(String key, Object value) {
          super.put(key, value);
          return this;
        }
    
        public Integer getCode() {
    
          return (Integer) this.get("code");
        }
    
      }
    
    
    
    

参考链接

  • 【谷粒商城】分布式事务与下单

    https://blog.csdn.net/hancoder/article/details/114983771

  • 全网最强电商教程《谷粒商城》对标阿里P6/P7,40-60万年薪

    https://www.bilibili.com/video/BV1np4y1C7Yf?p=284

  • mall源码工程

    https://github.com/CharlesKai/mall

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值