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


一、秒杀业务介绍

介绍:
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<String, Object> 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。
优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在森林中麋了鹿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值