秒杀业务设计与实现

秒杀特点

瞬时流量特别大

定时任务

参考

1. cron 表达式
  • 语法:秒 分 时 日 月 周 年

  • 说明

    名称是否必须出现允许的值允许的特殊字符
    Seconds0~59, - * /
    Minutes0~59, - * /
    Hours0~23, - * /
    Day of month1-31, - * ? / L W
    Month1~12 or JAN~DEC, - * /
    Day of week1~7 or SUN~SAT, - * ? / L #
    Yearempty, 1970~2099, - * /
  • 特殊字符说明:

  1. , :枚举
    (cron=“7,9,23 * * * * ?”) :任意时刻的 7、9、23秒启动任务
  2. - :范围
    (cron=“7-20 * * * * ?"):任意时刻的7~20秒之间,每秒启动一次任务。
  3. * :任意
  4. / :步长
    (cron=“7/5 * * * * ?”):第7秒启动,每5秒一次。
    (cron="*/5 * * * * ?"):任意秒启动,每5秒一次。
  5. ?:出现在日和周几的位置,为了防止日和周冲突,在周和日如果要写,请使用通配符?
    (cron="* * * 1 * ?"):每月的1号启动这个任务
  6. L:出现在日和周的位置,L表示Last意思
    (cron="* * * ? * 3L"):每个月的最后一个周二启动任务
  7. W:表示工作日,Work Day的意思。
    (cron="* * * LW * ? "):每个月的最后一个工作日启动任务
  8. #:第几
    (cron="* * * ? * 5#2“):每个月的第2个周4启动任务
Spring 的定时调度
  1. 每秒执行一次定时任务

    @Slf4j
    @Component
    @EnableScheduling
    public class HelloSchedule {
    
        @Scheduled(cron = "* * * * * ?")
        public void hello() {
            log.info("hello...");
        }
    }
    
  2. 定时任务默认是阻塞的

    @Slf4j
    @Component
    @EnableScheduling
    public class HelloSchedule {
    
        @Scheduled(cron = "* * * * * ?")
        public void hello() throws InterruptedException {
            log.info("hello...");
            Thread.sleep(3000);
        }
    }
    

    定时任务要求每秒执行一次,但是线程执行需要3秒才能执行完成。因此两个hello打印间隔时常为3秒。定时任务默认是阻塞的。

  3. 解决定时任务不阻塞,方式一:异步编排加线程池

    @Slf4j
    @Component
    @EnableScheduling
    public class HelloSchedule {
    
        public static ExecutorService pool = Executors.newFixedThreadPool(10);
    
        @Scheduled(cron = "* * * * * ?")
        public void hello() {
            CompletableFuture.runAsync(() -> {
                log.info("hello...");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, pool);
        }
    }
    
  4. 配置定时任务线程池(不生效
    看定时任务源码

    @ConditionalOnClass(ThreadPoolTaskScheduler.class)
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(TaskSchedulingProperties.class)
    @AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
    public class TaskSchedulingAutoConfiguration {
    ...
    }
    
    @ConfigurationProperties("spring.task.scheduling")
    public class TaskSchedulingProperties {
    
    	private final Pool pool = new Pool();
    
    	private final Shutdown shutdown = new Shutdown();
    
    	/**
    	 * Prefix to use for the names of newly created threads.
    	 */
    	private String threadNamePrefix = "scheduling-";
    
    	public Pool getPool() {
    		return this.pool;
    	}
    
    	public Shutdown getShutdown() {
    		return this.shutdown;
    	}
    
    	public String getThreadNamePrefix() {
    		return this.threadNamePrefix;
    	}
    
    	public void setThreadNamePrefix(String threadNamePrefix) {
    		this.threadNamePrefix = threadNamePrefix;
    	}
    
    	public static class Pool {
    
    		/**
    		 * Maximum allowed number of threads.
    		 */
    		private int size = 1;
    
    		public int getSize() {
    			return this.size;
    		}
    
    		public void setSize(int size) {
    			this.size = size;
    		}
    	}
    
    	...
    
    }
    

    定时任务是放在线程池中执行,通过源码分析,我们指定,执行定时任务的线程池的大小是1,这也就解释了,定时任务为啥是阻塞执行的。因为是单线程运行定时任务的。
    在application.yml 修改定时任务默认的线程池大小:

    spring:
      task:
        scheduling:
          pool:
            size: 10
    

    测试之后,发现没有生效。

  5. 解决定时任务不阻塞,方式二:使用@EnableAsync 和 @Async 注解

    @Slf4j
    @Component
    @EnableAsync
    @EnableScheduling
    public class HelloSchedule {
    
        @Async
        @Scheduled(cron = "* * * * * ?")
        public void hello() throws InterruptedException {
            log.info("hello...");
            Thread.sleep(3000);
        }
    }
    

    @EnableAsync 注解对应的自动配置类是

    @ConditionalOnClass(ThreadPoolTaskScheduler.class)
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(TaskSchedulingProperties.class)
    @AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
    public class TaskSchedulingAutoConfiguration {
    ...
    }
    
    @ConfigurationProperties("spring.task.execution")
    public class TaskExecutionProperties {
    
    	private final Pool pool = new Pool();
        ...
    	public static class Pool {
    
    		/**
    		 * Queue capacity. An unbounded capacity does not increase the pool and therefore
    		 * ignores the "max-size" property.
    		 */
    		private int queueCapacity = Integer.MAX_VALUE;
    
    		/**
    		 * Core number of threads.
    		 */
    		private int coreSize = 8;
    
    		/**
    		 * Maximum allowed number of threads. If tasks are filling up the queue, the pool
    		 * can expand up to that size to accommodate the load. Ignored if the queue is
    		 * unbounded.
    		 */
    		private int maxSize = Integer.MAX_VALUE;
    		....
    	}
    	...
    }
    

    可以看到线程池的默认大小是8,最大是 Integer.MAX_VALUE。最大值过大,可以在配置文件中修改。比如修改为100

    spring:
      task:
        execution:
          pool:
            max-size: 100
    

秒杀功能之商品定时上架流程

为了应对瞬时高并发,秒杀的商品应该放在 Redis 中。因此需要提前将商品的相关信息存储在Redis中,此称之为商品上架。
在这里插入图片描述
使用定时任务,在特定时间点,将需要参与秒杀活动的商品存储在Redis中。

  1. 商品后台管理系统,需要策划秒杀活动,并录入系统。秒杀活动需要:活动开始时间,活动结束时间、活动关联的商品种类、商品数量,商品价格等一系列信息。
  2. 定时任务读取秒杀活动相关信息。
  3. 秒杀活动的信息存入Redis中:活动开始时间,活动结束时间、活动id,参与活动的商品种类。可以以 活动开始时间和结束时间组合,作为key,而以活动ID 和 商品种类ID(skuId)组合后,形成的list作为Value,存入Redis中。
  4. 秒杀商品信息存入Reids中:活动ID 和 商品种类ID(skuId)组合形成key,商品详细的信息作为Value存入Redis中的hash结构中。其中商品的详细信息中,应该有一个随机码字段,用于关联分布式信号量,作为库存扣减的依据。
  5. 设置每种商品的分布式信号量,信号量的名字为:特定前缀(自定义)+ 商品的随机码(代表了该商品),值为该种商品,参与这次秒杀活动的数量。
  6. 设置随机码,主要是为了防止用户利用工具,将商品秒杀。随机码只有在秒杀活动开始的那一刻,用户才能活动。

秒杀活动流程

秒杀(高并发)系统需要关注的问题
  1. 服务单一职责 + 独立部署
    秒杀服务应该以单独微服务部署,即使自己扛不住压力,也不要影响其它微服务。
  2. 秒杀连接加密
    防止恶意攻击:比如用工具短时间内发送大量的请求。
    防止链接暴漏:比如自己工作人员提前秒杀商品。
  3. 库存预热 + 快速扣减
    秒杀是读多写少的请求。无需实时校验库存,只要我们提前预热库存,让在redis中即可。用信号量控制进来的秒杀请求,快速扣减。
  4. 动静分离
    nginx做好动静分离,保证只有动态请求才打到后端微服务集群。前端资源,访问到nginx后,立即返回。
  5. 恶意请求拦截
    在网关层,识别非法请求,并进行拦截。保证到达后端微服务的请求,是正常非恶意请求。
  6. 流量错峰
    使用各种手段,将流量分担到更大宽度的时间点。比如验证码,购物车等。
  7. 限流、熔断、降级
    前端限流:限制按钮在规定时间内点击的次数
    后端限流:短时间内,同一用户的多次请求,放行一两次。
    熔断:完成整个秒杀业务,中间可能需要多个微服务的配合。当某个微服务出现问题的时候,快速返回,无需等待。
    降级:当流量太大,处理不过来的时候。可以将部分流量引导到降级页面。给出温馨提示。
  8. 队列削峰
    秒杀成功的请求,进入队列中。创建订单,扣减库存可以在后台慢慢处理。
秒杀流程设计
  1. 方案一
    在这里插入图片描述
  2. 方案二
    在这里插入图片描述
  3. 方案一和方案二的区别
    方案一:可以复用加入购物车的流程,并且能将秒杀瞬时流量分摊在更宽的时间维度。但是从到成功返回秒杀结果,调用了购物车微服务,库存微服务。
    方案二:响应用户请求更快,因为从秒杀开始到告诉用户秒杀成功,只是在秒杀服务中创建了秒杀单。而实际的扣减库存,创建订单这些还没做。缺点:订单服务可能挂掉或者阻塞,导致用户支付不能成功。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值