Day437&438.秒杀服务 -谷粒商城

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day437&438.秒杀服务 -谷粒商城相关的知识,希望对你有一定的参考价值。

秒杀服务

一、定时任务-Quartz

Cron表达式

执行定时任务需要给一个时间计划,这个时间计划可以用 Cron 表达式来编写

官方文档 Cron 表达式是一个字符串,是用空格分割的六到七个属性。

语法:秒 分 时 日 月 周 年(可忽略年,Spring 不支持年)

定时任务只能精确到秒

Seconds:0-59,举例:0 就是整秒执行,1 就是在第1秒的时候执行

Day of week:值可以写 1-7,也可以写 SUN-SAT,1 就是周日,7 就是周六

①特殊字符

,:枚举

  • (cron="7,9,23 * * * * ?"):代表任意时刻的7,9,23秒启动这个任务;

-:范围

  • (cron="7-20 * * * * ?"):任意时刻的 7-20 秒之间,每秒启动一次

*:任意

  • 指定位置的任意时刻都可以

/:步长

  • (cron="7/5 * * * * ?"):第 7 秒启动,每 5 秒一次;
  • (cron="*/5 * * * * ?"):任意时间启动之后,每 5 秒一次;
?`:(出现在日和周几的位置)为了防止日和周冲突,如果1个精确了,另一个就得写`?
  • (cron="* * * 1 * ?"):每月的 1 号,启动这个任务,如果两个都写精确值的话,可能会导致冲突,所以其中一个要使用?

L:(出现在日和周的位置)”,last:最后一个

  • (cron="* * * ? * 3L"):每月的最后一个周二

W: Work Day:工作日

  • (cron="* * * W * ?"):每个月的工作日触发
  • (cron="* * * LW * ?"):每个月的最后一个工作日触发

#: 第几个

  • (cron="* * * ? * 5#2"):5 代表周 4,#2 代表第 2 个,合起来就是每个月的第 2 个周 4

②示例

表达式意义
0 0 12 ?每天的12点执行一次
0 15 10 ?每天的10:15执行
0 15 10 ?每天的10:15执行
0 15 10 ? *每天的10:15执行
0 15 10 ? 20052005年每天10:15执行都执行
0 14 * ?每天14点启动,每隔1分钟执行一次
0 0/5 14 ?每天14点启动,每隔5分钟执行一次
0 0/5 14,18 ?每天的14点、18点启动,每隔5分钟执行一次
0 0-5 14 ?14:00-14:05执行,每分钟执行一次
0 10,44 14 ? 3 WED3月的每个星期三的14:10 、14:44启动这个任务,每分钟执行一次
0 15 10 ? * MON-FRI每月的周一到周五,10:15执行
0 15 10 ? * 6L每月的最后一个周五,10:15执行
0 15 10 ? * 6#3每月的第3个周5,10:15执行

二、Spring Boot整合定时任务

1、与Quarts的区别

自动配置类参考 TaskSchedulingAutoConfiguration

@Slf4j
@Component
@EnableScheduling   // 开启定时功能
public class HelloSchedule {

    /**
     *  一、与Quarts的区别
     *      1、Spring中的定时任务由6位组成,不支持第7位的年
     *      2、第6位的数字格式,1-7,代表周一到周日,当然也可以写成MON-SUN
     *      3、其它普遍与Quarts一致
     */
    @Scheduled(cron = "*/5 * * ? * 1") // 开启定时任务
    public void hello(){
        log.info("hello");
    }
}

2、定时任务默认是阻塞的

        /**
         * 二、定时任务默认是阻塞的
         *      只要当前任务没执行完,下一个任务就执行不了
         * @throws InterruptedException
         */
        @Scheduled(cron = "* * * ? * 1")
        public void block() throws InterruptedException {
            log.info("hello......");
            Thread.sleep(3000);
        }

3、解决定时任务阻塞的方法

  1. 可以使用异步任务的方式,CompletableFuture.runAsync(),自己提交到线程池

  2. 修改配置文件,spring.task.scheduling.pool.size=5

  3. 让定时任务异步执行

①异步任务

  1. 首先在类上面标注@EnableAsync,开启异步任务功能
  2. 然后在方法上标注@Async,执行异步任务
  3. 这个异步任务不是只能搭配定时任务,它可以替代CompletableFuture
  4. 自动配置类参考 TaskExecutionAutoConfiguration
  5. 它在配置文件中的线程池属性是:spring.task.execution.pool.xxx

②最终

使用异步+定时任务来实现定时任务不阻塞


三、定时上架秒杀商品

1、简介

每天凌晨3点,上架最近3天所需要秒杀的商品

因为这个时间段服务器压力较小,并且比较空闲,

上架最近3天的商品,可以给用户一个预告的功能,让用户提前知道哪个商品什么时间将要开启秒杀


2、随机码

为了防止有用户在得知秒杀请求时,发送大量请求对商品进行秒杀,我们采取了随机码的方式,即每个要参加秒杀的商品,都有一个随机码,只有通过正常提交请求的流程才可以获取,否则谁都无法得知随机码是多少,避免了恶意秒杀


3、商品的分布式信号量

信号量保存了当前秒杀商品的库存信息

我们的库存秒杀不应该是实时去数据库扣库存,因为几百万请求进来的时候,如果都去扣,那会直接把数据库压垮。

所以现在秒杀最大的问题就是,如何应对这些高并发的流量

首先,这么大的流量进到服务器的话,肯定有一些流量是无效的,比如秒杀不成功,假设我们现在就一百个商品要被秒杀,哪怕放进来一百万请求,最终也只有一百个请求,能成功的去数据库扣掉库存。

所以我们可以提前在 redis 里边设置一个信号量,这个信号量可以认为是一个自增量,假设这个信号量叫 count,它专门用来计数,它的初始值是 100,每进来一个请求,我们就让这个值减一,如果有用户想要秒杀这个商品,我们先去 redis 里边获取一个信号量,也就是给这一百的库存减一,然后这个值就变成九十九,如果能减成功了,那就把这个请求放行,然后再做后边的处理数据库。如果不能减,那就不用进行后续的操作了,我们只会阻塞很短的时间,就会释放这个请求,我们只有每一个请求都能很快的释放,能很快的做完,我们才能拥有处理大并发的能力。

这块有一个注意点,由于每一个请求进来减这个信号量的值,就是当前商品的库存信息,只有请求里携带了我们给秒杀商品设计的随机码,才可以来减信号量,如果不带随机码的话,直接减信号量的话,就会出现问题,可能秒杀还没开始,有一些恶意请求,就把信号量就减了了。

所以上面说的随机码是一种保护机制。


4、代码

  • 创建秒杀项目模块

  • 引入依赖

  • com.achang.achangmall.coupon.controller.SeckillSessionController

        /**
         * 查询最近三天需要参加秒杀商品的信息
         * @return
         */
        @GetMapping(value = "/Lates3DaySession")
        public R getLates3DaySession() {

            List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession();

            return R.ok().setData(seckillSessionEntities);
        }
  • com.achang.achangmall.coupon.service.impl.SeckillSessionServiceImpl
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {

    @Autowired
    private SeckillSkuRelationService seckillSkuRelationService;

    @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;
    }


    /**
     * 当前时间
     */
    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;
    }
}
  • com.achang.achangmall.coupon.entity.SeckillSessionEntity
@TableField(exist = false)
private List<SeckillSkuRelationEntity> relationSkus;
  • com.achang.achangmall.seckill.vo.SeckillSkuVo
@Data
public class SeckillSkuVo {

    private Long id;
    /**
         * 活动id
         */
    private Long promotionId;
    /**
         * 活动场次id
         */
    private Long promotionSessionId;
    /**
         * 商品id
         */
    private Long skuId;
    /**
         * 秒杀价格
         */
    private BigDecimal seckillPrice;
    /**
         * 秒杀总量
         */
    private Integer seckillCount;
    /**
         * 每人限购数量
         */
    private Integer seckillLimit;
    /**
         * 排序
         */
    private Integer seckillSort;

}
  • com.achang.achangmall.seckill.config.ScheduledConfig
@Configuration
@EnableAsync
@EnableScheduling
public class ScheduledConfig {}
  • com.achang.achangmall.seckill.feign.CouponFeignService
@FeignClient("achangmall-coupon")
public interface CouponFeignService {

    /**
     * 查询最近三天需要参加秒杀商品的信息
     */
    @GetMapping(value = "/coupon/seckillsession/Lates3DaySession")
    R getLates3DaySession();
}
  • com.achang.achangmall.seckill.to.SeckillSkuRedisTo
    @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;
    }
  • com.achang.achangmall.seckill.vo.SeckillSessionWithSkusVo
@Data
public class SeckillSessionWithSkusVo {

    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;

    private List<SeckillSkuVo> relationSkus;

}

  • com.achang.achangmall.seckill.feign.ProductFeignService
@FeignClient("achangmall-product")
public interface ProductFeignService {
    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);
}
  • 引入分布式锁redisson
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>
  • com.achang.achangmall.seckill.config.MyRedissonConfig
@Configuration
public class MyRedissonConfig {
    /**
     * 所有对Redisson的使用都是通过RedissonClient
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redissonClient() throws IOException {
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.109.101:6379");

        //2、根据Config创建出RedissonClient实例
        //Redis url should start with redis:// or rediss://
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}
  • com.achang.achangmall.seckill.service.impl.SeckillServiceImpl
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private CouponFeignService couponFeignService;

    private final String SESSION__CACHE_PREFIX = "seckill:sessions:";

    private final String SECKILL_CHARE_PREFIX = "seckill:skus";

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public void uploadSeckillSkuLatest3Days() {
        R lates3DaySession = couponFeignService.getLates3DaySession();
        if (lates3DaySession.getCode()==0){
            List<SeckillSessionWithSkusVo> vo = lates3DaySession.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            //缓存到Redis
            //1、缓存活动信息
            saveSessionInfos(vo);

            //2、缓存活动的关联商品信息
            saveSessionSkuInfo(vo);

        }
    }

    /**
     * 缓存秒杀活动所关联的商品信息
     */
    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);

                    //序列化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());
                }
            });
        });
    }


    /**
     * 缓存秒杀活动信息
     */
    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;

            //获取到活动中所有商品的skuId
            List<String> skuIds = session.getRelationSkus().stream()
                .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
            redisTemplate.opsForList().leftPushAll(key,skuIds);
        });

    }
}

四、上架秒杀商品的幂等性保证

1、定时任务在分布式下的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传<p>以上是关于Day437&438.秒杀服务 -谷粒商城的主要内容,如果未能解决你的问题,请参考以下文章</p> 
<p > <a style=Day762.Redis秒杀场景 -Redis 核心技术与实战

Codeforces Round #438 C

秒杀链路兜底方案之限流&降级实战

秒杀ecshop的前台写shell 0day

hdu 1069 &uva 437

*Leetcode 437. 路径总和 III