• 谷粒商城 高级篇 (二十四) --------- 秒杀业务



    一、秒杀业务介绍

    介绍:
    a:秒杀是每一个电商系统里,非常重要的模块,商家会不定期的发布一些低价商品,发布到秒杀系统里边。
    b:特点就是,等到秒杀时间一到,瞬时流量特别大
    c:关注:限流、异步、缓存、创建独立的微服务。
    d:秒杀业务:

    • 秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步+ 缓存 (页面静态化)+ 独立部署。
    • 限流方式:
      • 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
      • nginx 限流,直接负载部分请求到错误的静态页面:令牌算法、漏斗算法
      • 网关限流,限流的过滤器
      • 代码中使用分布式信号量
      • rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能

    秒杀流程:

    在这里插入图片描述

    秒杀架构图

    在这里插入图片描述

    二、定时任务

    1. cron 表达式

    在线Cron表达式生成器 (qqe2.com)

    ① cron表达式语法

    语法:秒 分 时 日 月 周 年 (spring 不支持年,所以可以不写)

    quartz表达式格式介绍

    Cron Trigger Tutorial (quartz-scheduler.org)

    A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:

    Field NameMandatoryAllowed ValuesAllowed Special Characters
    SecondsYES0-59, - * /
    MinutesYES0-59, - * /
    HoursYES0-23, - * /
    Day of monthYES1-31, - * ? / L W
    MonthYES1-12 or JAN-DEC, - * /
    Day of weekYES1-7 or SUN-SAT, - * ? / L #
    YearNOempty, 1970-2099, - * /

    So cron expressions can be as simple as this: * * * * ? *

    or more complex, like this: 0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010

    ② cron 表达式特殊字符

    ,:枚举;
    (cron="7,9,23****?"):任意时刻的7,9,23秒启动这个任务;
    
    -:范围:
    (cron="7-20****?""):任意时刻的7-20秒之间,每秒启动一次
    
    *:任意;
    指定位置的任意时刻都可以
    
    /:步长;
    (cron="7/5****?"):7秒启动,每5秒一次;
    (cron="*/5****?"):任意秒启动,每5秒一次;
    
    ? :(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使用?
    (cron="***1*?"):每月的1号,而且必须是周二然后启动这个任务;
    
    L:(出现在日和周的位置)”,
    last:最后一个
    (cron="***?*3L"):每月的最后一个周二
    
    W:Work Day:工作日
    (cron="***W*?"):每个月的工作日触发
    (cron="***LW*?"):每个月的最后一个工作日触发
    
    #:第几个
    (cron="***?*5#2"):每个月的 第2个周4
    

    ③ 案例

     */5 * * * * ? 每隔5秒执行一次
     0 */1 * * * ? 每隔1分钟执行一次
     0 0 5-15 * * ? 每天5-15点整点触发
     0 0/3 * * * ? 每三分钟触发一次
     0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 
     0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
     0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
     0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
     0 0 10,14,16 * * ? 每天上午10点,下午2点,40 0 12 ? * WED 表示每个星期三中午120 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
     0 10,44 14 ? 3 WED 每年三月的星期三的下午2:102:44触发 
     0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
     0 0 23 L * ? 每月最后一天23点执行一次
     0 15 10 L * ? 每月最后一日的上午10:15触发 
     0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 
     0 15 10 * * ? 2005 2005年的每天上午10:15触发 
     0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 
     0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
    
    
    "30 * * * * ?" 每半分钟触发任务
    "30 10 * * * ?" 每小时的1030秒触发任务
    "30 10 1 * * ?" 每天11030秒触发任务
    "30 10 1 20 * ?" 每月2011030秒触发任务
    "30 10 1 20 10 ? *" 每年102011030秒触发任务
    "30 10 1 20 10 ? 2011" 2011102011030秒触发任务
    "30 10 1 ? 10 * 2011" 201110月每天11030秒触发任务
    "30 10 1 ? 10 SUN 2011" 201110月每周日11030秒触发任务
    "15,30,45 * * * * ?"15秒,30秒,45秒时触发任务
    "15-45 * * * * ?" 1545秒内,每秒都触发任务
    "15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
    "15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
    "0 0/3 * * * ?" 每小时的第00秒开始,每三分钟触发一次
    "0 15 10 ? * MON-FRI" 星期一到星期五的10150秒触发任务
    "0 15 10 L * ?" 每个月最后一天的10150秒触发任务
    "0 15 10 LW * ?" 每个月最后一个工作日的10150秒触发任务
    "0 15 10 ? * 5L" 每个月最后一个星期四的10150秒触发任务
    "0 15 10 ? * 5#3" 每个月第三周的星期四的10150秒触发任务
    

    2. 测试

    • 问题:定时任务默认是阻塞的。如何让它不阻塞?

    • 解决:使用异步+定时任务来完成定时任务不阻塞的功能

      • 定时任务:
        • @EnableScheduling 开启定时任务
        • @Scheduled 开启一个定时任务
        • 自动配置类 TaskSchedulingAutoConfiguration
      • 异步任务:
        • @EnableAsync 开启异步任务功能
        • @Async :给我希望异步执行的方法上标注
        • 自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
    package com.fancy.gulimall.seckill.scheduled;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    /**
     * Data time:2022/4/16 20:20
     * StudentID:2019112118
     * Author:hgw
     * Description: 定时调度测试
     * 定时任务:
     *  1、@EnableScheduling 开启定时任务
     *  2、@Scheduled 开启一个定时任务
     *  3、自动配置类 TaskSchedulingAutoConfiguration
     * 异步任务:
     *  1、@EnableAsync 开启异步任务功能
     *  2、@Async :给我希望异步执行的方法上标注
     *  3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
     */
    @Slf4j
    @Component
    @EnableAsync
    @EnableScheduling
    public class HelloSchedule {
    
        /**
         * 1、spring中corn 表达式由6为组成,不允许第7位的年  Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
         * 2、在周几的位置,1-7分别代表:周一到周日(MON-SUN)
         * 3、定时任务默认是阻塞的。如何让它不阻塞?
         *      1)、可以让业务运行以异步的方式,自己提交到线程池
         *      2)、Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
         *              spring.task.scheduling.pool.size=5
         *      3)、让定时任务异步执行
         *          异步任务
         *   解决:使用异步+定时任务来完成定时任务不阻塞的功能
         */
        @Async
        @Scheduled(cron = "* * * * * 6")
        public void hello() throws InterruptedException {
            log.info("hello.....");
            Thread.sleep(3000);
        }
    }
    

    配置异步任务线程池:

    spring.task.execution.pool.core-size=5
    spring.task.execution.pool.max-size=50
    

    定时任务开启后其实也是有线程池的,通过更改配置修改线程池大小,这样也可以解决阻塞问题

    #默认为1,就会阻塞
    spring.task.scheduling.pool.size: 2  
    

    三、秒杀商品上架

    1. 获取开始结束日期

    设定为 3 天

    A、获取当天0点的时间

    /**
     * 当前时间
     * @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;
    }
    

    B、获取加今天三天后的最后时间

    /**
     * 结束时间
     * @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;
    }
    

    2. 获取秒杀商品信息

    /**
     * 查询最近三天需要参加秒杀商品的信息
     * @return
     */
    @GetMapping(value = "/Lates3DaySession")
    public R getLates3DaySession() {
    
        List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession();
    
        return R.ok().setData(seckillSessionEntities);
    }
    
    @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;
    }
    
    package com.fancy.gulimall.coupon.service.impl;
    
    @Service("seckillSessionService")
    public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
    
    
        @Autowired
        SeckillSkuRelationService seckillSkuRelationService;
    
        @Override
        public List<SeckillSessionEntity> getLates3DaySession() {
            // 计算最近3天
            List<SeckillSessionEntity> list = this.list(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();
                    List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                    session.setRelationSkus(relationEntities);
                    return session;
                }).collect(Collectors.toList());
                return collect;
            }
            return null;
        }
    }
    

    3. 在 Redis 中保存秒杀场次信息

    package com.fancy.gulimall.seckill.service.impl;
    
    @Service
    public class SeckillServiceImpl implements SeckillService {
    
        @Autowired
        CouponFeignService couponFeignService;
    
        @Autowired
        StringRedisTemplate redisTemplate;
    
        private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
        private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
    
        /**
         * 缓存活动信息
         * @param sessions
         */
        private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
            sessions.stream().forEach(session ->{
                Long startTime = session.getStartTime().getTime();
                Long endTime = session.getEndTime().getTime();
                String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
                System.out.println(key);
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
                // 缓存活动信息
                redisTemplate.opsForList().leftPushAll(key,collect);
            });
        }
    

    4. 在 Redis 中保存秒杀活动关联的商品信息

    /**
     * 缓存活动的关联商品信息
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
        sessions.stream().forEach(session->{
            // 准备Hash操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                // 缓存商品
                SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                // 1、Sku的基本数据
                R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if (skuInfo.getCode() == 0) {
                    SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    redisTo.setSkuInfo(info);
                }
    
                // 2、Sku的秒杀信息
                BeanUtils.copyProperties(seckillSkuVo,  redisTo);
    
                // 3、设置上当前商品的秒杀时间信息
                redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());
    
                // 4、商品的随机码
                String token = UUID.randomUUID().toString().replace("_", "");
                redisTo.setRandomCode(token);
    
                // 5、引入分布式的信号量 限流
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
    
                String jsonString = JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
            });
        });
    }
    
    

    5. 秒杀商品定时上架

    A、配置定时任务

    package com.fancy.gulimall.seckill.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @EnableAsync
    @EnableScheduling
    @Configuration
    public class ScheduledConfig {
    }
    

    B、定时上架功能

    package com.fancy.gulimall.seckill.scheduled;
    
    
    import com.fancy.gulimall.seckill.service.SeckillService;
    import lombok.extern.slf4j.Slf4j;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Service;
    
    import java.util.concurrent.TimeUnit;
    
    
    /**
     * 秒杀商品的定时上架;
     *     每天晚上3点;上架最近三天需要秒杀的商品。
     *     当天00:00:00  - 23:59:59
     *     明天00:00:00  - 23:59:59
     *     后天00:00:00  - 23:59:59
     */
    @Slf4j
    @Service
    public class SeckillSkuScheduled {
    
        @Autowired
        SeckillService seckillService;
    
        @Autowired
        RedissonClient redissonClient;
    
        private  final String  upload_lock = "seckill:upload:lock";
    
        //TODO 幂等性处理
    //    @Scheduled(cron = "*/3 * * * * ?")
        @Scheduled(cron = "0 * * * * ?") //每分钟执行一次吧,上线后调整为每天晚上3点执行
    //    @Scheduled(cron = "0 0 3 * * ?") 线上模式
        public void uploadSeckillSkuLatest3Days(){
            //1、重复上架无需处理
            log.info("上架秒杀的商品信息...");
            // 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态。
            RLock lock = redissonClient.getLock(upload_lock);
            lock.lock(10, TimeUnit.SECONDS);
            try{
                seckillService.uploadSeckillSkuLatest3Days();
            }finally {
                lock.unlock();
            }
        }
    }
    

    6. 幂等性保证

    在这里插入图片描述

    解决方案:加上分布式锁

    保证在分布式的情况下,锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态

    代码逻辑编写

    当查询Redis中已经上架的秒杀场次和秒杀关联的商品,则不进行上架

    加锁:

    在这里插入图片描述
    在这里插入图片描述

    7. 获取处于秒杀的商品信息

    @Override
    public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
    
        //1、找到所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    
    
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0) {
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                //6_4
                if (Pattern.matches(regx, key)) {
                    String json = hashOps.get(key);
                    SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
                    //TODO 加入非空判断
                    if (skuRedisTo == null) return null;
                    //随机码
                    long current = new Date().getTime();
                    if (current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()) {
                        //TODO
                    } else {
                        //TODO 当前商品已经过了秒杀时间要直接删除
                        hashOps.delete(key);
                        skuRedisTo.setRandomCode(null);
                    }
                    return skuRedisTo;
                }
                ;
            }
        }
    
    
        return null;
    }
    

    四、秒杀

    1. 秒杀流程一

    加入购物车秒杀-----弃用

    优点: 加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好

    缺点: 秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单

    在这里插入图片描述

    2. 秒杀流程二

    独立秒杀业务来处理

    优点: 从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息

    缺点: 如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款

    解决方案: 不使用订单服务处理秒杀消息,需要一套独立的业务来处理

    在这里插入图片描述

    3. 创建秒杀队列

    在这里插入图片描述

    /**
     * 商品秒杀队列
     * 作用:削峰,创建订单
     */
    @Bean
    public Queue orderSecKillOrderQueue() {
        Queue queue = new Queue("order.seckill.order.queue", true, false, false);
        return queue;
    }
    
    @Bean
    public Binding orderSecKillOrderQueueBinding() {
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map arguments
        Binding binding = new Binding(
                "order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
    
        return binding;
    }
    

    4. 秒杀代码

    // TODO 上架秒杀商品的时候,每一个数据都有过期时间。
    // TODO 秒杀后续的流程,简化了收货地址等信息。
    @Override
    public String kill(String killId, String key, Integer num) {
    
        long s1 = System.currentTimeMillis();
        MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
    
        //1、获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(json)) {
            return null;
        } else {
            SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
            //校验合法性
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            long time = new Date().getTime();
    
            long ttl = endTime - time;
    
            //1、校验时间的合法性
            if (time >= startTime && time <= endTime) {
                //2、校验随机码和商品id
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                if (randomCode.equals(key) && killId.equals(skuId)) {
                    //3、验证购物数量是否合理
                    if (num <= redis.getSeckillLimit()) {
                        //4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。  userId_SessionId_skuId
                        //SETNX
                        String redisKey = respVo.getId() + "_" + skuId;
                        //自动过期
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            //占位成功说明从来没有买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            //120  20ms
                            boolean b = semaphore.tryAcquire(num);
                            if (b) {
                                //秒杀成功;
                                //快速下单。发送MQ消息  10ms
                                String timeId = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(respVo.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(redis.getSeckillPrice());
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                                long s2 = System.currentTimeMillis();
                                log.info("耗时...{}", (s2 - s1));
                                return timeId;
                            }
                            return null;
    
                        } else {
                            //说明已经买过了
                            return null;
                        }
    
                    }
                } else {
                    return null;
                }
    
            } else {
                return null;
            }
        }
        return null;
    }
    

    5. 秒杀消息消费

    package com.fancy.gulimall.order.listener;
    
    import com.fancy.common.to.mq.SeckillOrderTo;
    import com.fancy.gulimall.order.service.OrderService;
    import com.rabbitmq.client.Channel;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.RabbitHandler;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    
    @Slf4j
    @RabbitListener(queues = "order.seckill.order.queue")
    @Component
    public class OrderSeckillListener {
    
        @Autowired
        OrderService orderService;
        @RabbitHandler
        public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
    
            try{
                log.info("准备创建秒杀单的详细信息。。。");
                orderService.createSeckillOrder(seckillOrder);
                //手动调用支付宝收单;
                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);
    
        //保存订单项信息
        OrderItemEntity orderItem = new OrderItemEntity();
        orderItem.setOrderSn(orderTo.getOrderSn());
        orderItem.setRealAmount(totalPrice);
    
        orderItem.setSkuQuantity(orderTo.getNum());
    
        //保存商品的spu信息
        R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
        SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
        });
        orderItem.setSpuId(spuInfoData.getId());
        orderItem.setSpuName(spuInfoData.getSpuName());
        orderItem.setSpuBrand(spuInfoData.getBrandName());
        orderItem.setCategoryId(spuInfoData.getCatalogId());
    
        //保存订单项数据
        orderItemService.save(orderItem);
    }
    

    五、总结

    在这里插入图片描述

    在这里插入图片描述

    A、服务单一职责+独立部署

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

    解决:新增秒杀服务

    B、秒杀链接加密

    防止恶意攻击,模拟秒杀请求,1000次ls攻击。
    防止链接暴露.自己工作人员,提前秒杀商品。

    解决:请求需要随机码,在秒杀开始时随机码才会放在商品信息中

    C、库存预热+快速扣减【限流,并发信号量】

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

    解决:库存放入redis中,使用分布式信号量扣减+限流

    D、动静分离

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

    解决:nginx
    例:10万个人来访问商品详情页,这个详情页会发送63个请求,但是只有3个请求到达后端,60个请求是前端的。一共30万请求到达后端,600万个请求到达nginx或cdn

    E、恶意请求拦截

    识别非法攻击请求并进行拦截,网关层拦截,放行到后太服务的请求都是正常请求
    在网关层拦截:一些不带令牌的请求循环发送

    解决:使用网关拦截,本系统做了登录拦截器 [在各微服务创建的,未登录跳转登录页面]

    F、流量错峰

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

    • 1、输入验证码需要时间,将流量错开了【速度有快有慢】
    • 2、加入购物车,然后再结算【速度有快有慢】

    解决:使用购物车逻辑

    G、限流&熔断&降级

    前踹限流+后端限流
    限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩

    前端限流:

    • 1、每次点击1s后才能再次点击
    • 2、验证登录

    后端限流:

    • 1、网关限流,例如访问秒杀的流量到达10W等2S再将请求传过去【其中10W是集群的峰值】
    • 2、就算是合理的10次也只放行1-2次
    • 3、熔断:远程访问失败,快速返回,并且下次不要再请求这个节点【防止请求长时间等待】
    • 4、降级:请求量太大了,直接将请求转发到一个错误页面

    出现一种情况:集群的处理能力是 10 W,网关放行了10W的请求,但此时秒杀服务掉线了2台,处理能力下降导致请求堆积,最后资源耗尽服务器全崩了

    解决:spring alibaba sentinel
    以前是Hystrix,现在不更新了就不用了

    H、队列削峰

    100万个商品,每个商品的秒杀库存是100,会产生1亿的流量到后台,全部放入队列中,然后订单监听队列一个个创建订单扣减库存

    解决:秒杀服务将创建订单的请求存入mq,订单服务监听mq。
    优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】

  • 相关阅读:
    RHCE——二十、Ansible及安装与配置
    BaaS、FaaS、Serverless 都是什么?
    Nginx重写功能(rewrite与location)
    CodeForces..构建美丽数组.[简单].[情况判断].[特殊条件下的最小值奇偶问题]
    四、synchronized、volatile 、Lock
    创维E900V22E_卡刷固件及升级说明
    java-net-php-python-jsp学生党团管理信息系统2020演示录像计算机毕业设计程序
    二、注册功能
    elasticsearch5.6设置用户名密码
    springboot+vue校园篮球比赛预约报名平台java maven
  • 原文地址:https://blog.csdn.net/m0_51111980/article/details/127043550