介绍:
a:秒杀是每一个电商系统里,非常重要的模块,商家会不定期的发布一些低价商品,发布到秒杀系统里边。
b:特点就是,等到秒杀时间一到,瞬时流量特别大
c:关注:限流、异步、缓存、创建独立的微服务。
d:秒杀业务:
秒杀流程:
秒杀架构图
语法:秒 分 时 日 月 周 年 (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 Name | Mandatory | Allowed Values | Allowed Special Characters |
---|---|---|---|
Seconds | YES | 0-59 | , - * / |
Minutes | YES | 0-59 | , - * / |
Hours | YES | 0-23 | , - * / |
Day of month | YES | 1-31 | , - * ? / L W |
Month | YES | 1-12 or JAN-DEC | , - * / |
Day of week | YES | 1-7 or SUN-SAT | , - * ? / L # |
Year | NO | empty, 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="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点,4点
0 0 12 ? * WED 表示每个星期三中午12点
0 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2: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 * * * ?" 每小时的10分30秒触发任务
"30 10 1 * * ?" 每天1点10分30秒触发任务
"30 10 1 20 * ?" 每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *" 每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011" 2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011" 2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011" 2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?" 每15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 15到45秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?" 每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10点15分0秒触发任务
问题:定时任务默认是阻塞的。如何让它不阻塞?
解决:使用异步+定时任务来完成定时任务不阻塞的功能
@EnableScheduling
开启定时任务@Scheduled
开启一个定时任务@EnableAsync
开启异步任务功能@Async
:给我希望异步执行的方法上标注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
设定为 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;
}
/**
* 查询最近三天需要参加秒杀商品的信息
* @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;
}
}
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);
});
}
/**
* 缓存活动的关联商品信息
* @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);
});
});
}
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();
}
}
}
解决方案:加上分布式锁
保证在分布式的情况下,锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态
代码逻辑编写
当查询Redis中已经上架的秒杀场次和秒杀关联的商品,则不进行上架
加锁:
@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;
}
加入购物车秒杀-----弃用
优点: 加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好
缺点: 秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单
独立秒杀业务来处理
优点: 从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息
缺点: 如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款
解决方案: 不使用订单服务处理秒杀消息,需要一套独立的业务来处理
/**
* 商品秒杀队列
* 作用:削峰,创建订单
*/
@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;
}
// 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;
}
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、流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
解决:使用购物车逻辑
G、限流&熔断&降级
前踹限流+后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
前端限流:
后端限流:
出现一种情况:集群的处理能力是 10 W,网关放行了10W的请求,但此时秒杀服务掉线了2台,处理能力下降导致请求堆积,最后资源耗尽服务器全崩了
解决:spring alibaba sentinel
以前是Hystrix,现在不更新了就不用了
H、队列削峰
100万个商品,每个商品的秒杀库存是100,会产生1亿的流量到后台,全部放入队列中,然后订单监听队列一个个创建订单扣减库存
解决:秒杀服务将创建订单的请求存入mq,订单服务监听mq。
优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】