商场秒杀服务实战
秒杀服务
后台添加秒杀商品
添加场次,添加场次中的商品
单独创建一个秒杀服务,这样不影响正常交易的进行
定时任务&Cron表达式
SpringBoot整合定时任务与异步任务
/**
* 定时任务
* 1、 @EnableScheduling 开启定时任务
* 2、 开启一个定时任务
*
* 异步任务
* 1、@EnableAsync 类上注解,开启定时任务
* 2、@Async 开启一个定时任务
*/
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
* 1、 Spring中 6位组成,不允许第7位第年
* 2、 在周几第位置,1-7代表周一到周日: 也可以写英文缩写
* 3、 定时任务不应该阻塞。默认是阻塞的
* 1)可以让业务运行以异步的方式,自己提交到线程池
* CompletableFuture.runAsync(()->{
* xxxService.hello();
* },executor);
*
* 2)支持任务定时任务线程池:设置 spring.task.scheduling.pool.size=5 不好使
*
* 3)让定时任务异步执行
* 异步任务:
* 1、@EnableAsync 类上注解,开启定时任务
* 2、@Async 开启一个定时任务
* 解决:使用异步+定时任务来完成定时任务不阻塞的功能
*/
@Async
@Scheduled(cron = "* * * ? * *")
public void hello() throws InterruptedException {
log.info("hello");
Thread.sleep(5000);
}
}
时间日期处理
定时任务这边,远程调用优惠服务,计算最近三天内需要上架的秒杀商品
秒杀商品上架
将远程查询返回的需要上架的商品存入redis
//活动信息
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
//sku信息
private final String SKUKILL_CACHE_PREFIX = "seckill:skus";//多了个:
//高并发 信号量
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+随机码
设置随机码
抢商品的必须带上随机码才能减到库存
设置商品的 分布式 redis 信号量,商品可以秒杀的数量作为信号,信号量值为商品的库存
1、引入redisson依赖,配置参数
@Configuration
public class MyRedissonConfig {
/**
* 所有对redis的使用通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
//1、创建单节点模式
config.useSingleServer().setAddress("redis://192.168.33.10:6379");
//多节点模式
// config.useClusterServers()
// .addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
//2、创建实例
return Redisson.create(config);
}
}
幂等性保证
定时任务分布式下的问题
查询秒杀商品
查询时判断 当前时间商品秒杀是否开始,没有开始就设置随机码为空,不发放随机码
秒杀系统设计
网关层拦截恶意请求
我们做到了
1、秒杀单独抽取出来了
2、链接加密,只有秒杀时间到了,才能获取到随机码
3、库存预热,定时任务上架上要秒杀到商品到redis,商品的库存是redis的信号量控制,原子减量
4、动静分离
登陆检查
前端限流,前端判断当前商品是否是秒杀商品,若是秒杀商品,先判断登陆。
后端判断登陆,引入 session 依赖
<!-- 整合Spring Session完成session共享问题 微服务自治,就不放在common里了-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
session配置类
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {
/**
* 自定义session作用域:整个网站
* 使用一样的session配置,能保证全网站共享一样的session
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setDomainName("gulimall.com");
defaultCookieSerializer.setCookieName("GULISESSION");
return defaultCookieSerializer;
}
/**
* 序列化机制
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
拦截器
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/kill", uri);
if (match ) {
return true;
}
HttpSession session = request.getSession();
MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
loginUser.set(attribute);
return true;
} else {
request.getSession().setAttribute("msg", "请先登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
将拦截器注册
@Configuration
public class SecKillWebConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginUserInterceptor()).addPathPatterns("/**");
}
}
秒杀流程
选择这套流程,用到消息队列,做到流量削峰
先判断这人是否参与过秒杀,通过设置redis key 占位,占位设置成功说明没有参与过,反之
//4、验证这个人是否买过,幂等性;如果只要秒杀成功,就去占位,好让。 userId_SessionId_skuId
String redisKey = respVo.getId() + "_" + redis.getPromotionSessionId() + "_" + redis.getSkuId();
//自动过期
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
前置判断成功后,获取redis信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//semaphore.acquire(num);//这是个阻塞方式获取信号量,获取不到就不走了,不好
//semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);//尝试100毫妙获取信号量
semaphore.tryAcquire(num);//尝试获取一次,获取不到就 返回false
获取到
秒杀效果完成
MQ的设计
1、引入 RabbitMQ依赖,添加配置和转换器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.33.10
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
2、创建队列,并指定 交换机的绑定
@Bean
public Queue orderSeckillOrderQueue() {
//是否持久化的 是否排他的(大家都能监听,谁抢到算谁的) 是否自动删除
return new Queue("order.seckill.order.queue", true, false, false);
}
@Bean
public Binding orderSeckillOrderQueueBinding() {
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
}
3、创建监听类
@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 {
System.out.println("准备创建秒杀单单详细信息"+secKillOrder.getOrderSn());
try {
orderService.createSecKillOrder(secKillOrder);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}catch (IOException e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
监听到秒杀订单队列的队列,然后创建待支付订单
测试完成秒杀下单 时间为80毫秒