秒杀特点
瞬时流量特别大
定时任务
1. cron 表达式
-
语法:
秒 分 时 日 月 周 年
-
说明
名称 是否必须出现 允许的值 允许的特殊字符 Seconds 是 0~59 , - * / Minutes 是 0~59 , - * / Hours 是 0~23 , - * / Day of month 是 1-31 , - * ? / L W Month 是 1~12 or JAN~DEC , - * / Day of week 是 1~7 or SUN~SAT , - * ? / L # Year 否 empty, 1970~2099 , - * / -
特殊字符说明:
- , :枚举
(cron=“7,9,23 * * * * ?”) :任意时刻的 7、9、23秒启动任务 - - :范围
(cron=“7-20 * * * * ?"):任意时刻的7~20秒之间,每秒启动一次任务。 - * :任意
- / :步长
(cron=“7/5 * * * * ?”):第7秒启动,每5秒一次。
(cron="*/5 * * * * ?"):任意秒启动,每5秒一次。 - ?:出现在日和周几的位置,为了防止日和周冲突,在周和日如果要写,请使用通配符?
(cron="* * * 1 * ?"):每月的1号启动这个任务 - L:出现在日和周的位置,L表示Last意思
(cron="* * * ? * 3L"):每个月的最后一个周二启动任务 - W:表示工作日,Work Day的意思。
(cron="* * * LW * ? "):每个月的最后一个工作日启动任务 - #:第几
(cron="* * * ? * 5#2“):每个月的第2个周4启动任务
- 使用在线网站,测试自己的 Cron 表达式 https://cron.qqe2.com/
Spring 的定时调度
-
每秒执行一次定时任务
@Slf4j @Component @EnableScheduling public class HelloSchedule { @Scheduled(cron = "* * * * * ?") public void hello() { log.info("hello..."); } }
-
定时任务默认是阻塞的
@Slf4j @Component @EnableScheduling public class HelloSchedule { @Scheduled(cron = "* * * * * ?") public void hello() throws InterruptedException { log.info("hello..."); Thread.sleep(3000); } }
定时任务要求每秒执行一次,但是线程执行需要3秒才能执行完成。因此两个hello打印间隔时常为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); } }
-
配置定时任务线程池(
不生效
)
看定时任务源码@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
测试之后,发现没有生效。
-
解决定时任务不阻塞,方式二:使用@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中。
- 商品后台管理系统,需要策划秒杀活动,并录入系统。秒杀活动需要:活动开始时间,活动结束时间、活动关联的商品种类、商品数量,商品价格等一系列信息。
- 定时任务读取秒杀活动相关信息。
- 秒杀活动的信息存入Redis中:活动开始时间,活动结束时间、活动id,参与活动的商品种类。可以以 活动开始时间和结束时间组合,作为key,而以活动ID 和 商品种类ID(skuId)组合后,形成的list作为Value,存入Redis中。
- 秒杀商品信息存入Reids中:活动ID 和 商品种类ID(skuId)组合形成key,商品详细的信息作为Value存入Redis中的hash结构中。其中商品的详细信息中,应该有一个随机码字段,用于关联分布式信号量,作为库存扣减的依据。
- 设置每种商品的分布式信号量,信号量的名字为:特定前缀(自定义)+ 商品的随机码(代表了该商品),值为该种商品,参与这次秒杀活动的数量。
- 设置随机码,主要是为了防止用户利用工具,将商品秒杀。随机码只有在秒杀活动开始的那一刻,用户才能活动。
秒杀活动流程
秒杀(高并发)系统需要关注的问题
- 服务单一职责 + 独立部署
秒杀服务应该以单独微服务部署,即使自己扛不住压力,也不要影响其它微服务。 - 秒杀连接加密
防止恶意攻击:比如用工具短时间内发送大量的请求。
防止链接暴漏:比如自己工作人员提前秒杀商品。 - 库存预热 + 快速扣减
秒杀是读多写少的请求。无需实时校验库存,只要我们提前预热库存,让在redis中即可。用信号量控制进来的秒杀请求,快速扣减。 - 动静分离
nginx做好动静分离,保证只有动态请求才打到后端微服务集群。前端资源,访问到nginx后,立即返回。 - 恶意请求拦截
在网关层,识别非法请求,并进行拦截。保证到达后端微服务的请求,是正常非恶意请求。 - 流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,购物车等。 - 限流、熔断、降级
前端限流:限制按钮在规定时间内点击的次数
后端限流:短时间内,同一用户的多次请求,放行一两次。
熔断:完成整个秒杀业务,中间可能需要多个微服务的配合。当某个微服务出现问题的时候,快速返回,无需等待。
降级:当流量太大,处理不过来的时候。可以将部分流量引导到降级页面。给出温馨提示。 - 队列削峰
秒杀成功的请求,进入队列中。创建订单,扣减库存可以在后台慢慢处理。
秒杀流程设计
- 方案一
- 方案二
- 方案一和方案二的区别
方案一:可以复用加入购物车的流程,并且能将秒杀瞬时流量分摊在更宽的时间维度。但是从到成功返回秒杀结果,调用了购物车微服务,库存微服务。
方案二:响应用户请求更快,因为从秒杀开始到告诉用户秒杀成功,只是在秒杀服务中创建了秒杀单。而实际的扣减库存,创建订单这些还没做。缺点:订单服务可能挂掉或者阻塞,导致用户支付不能成功。