1. 秒杀系统设计
秒杀(高并发)系统关注的问题
1、服务单独职责+独立部署
秒杀系统为单独的服务,即使自己扛不住压力挂掉,也不要影响其他服务
2、秒杀链接加密
防止恶意攻击,模拟秒杀请求,1000次/s的攻击;防止链接暴露,自己工作人员,提前秒杀商品;我们使用了带uuid随机码的机制;
3、库存预热+快速扣减
秒杀读多写少,无需每次实时校验库存,我们库存预热,放到redis中,信号量控制进来秒杀的请求;为了保证redis可以保证千万并发,可以给redis做集群,做成分片高可用;我们是用定时任务提取三天写到缓存中;
4、动静分离
Nginx做好动静分离,保证秒杀和商品详情页的动态请求才打到后端的服务集群,使用CDN网络,分担本集群的压力;比如访问静态资源,阿里云CDN会在最快的节点返回静态资源;
5、恶意请求拦截
识别非法攻击请求并进行拦截(网关层),比如伪造的请求没带令牌;保证能放到后端的请求都是正常行为;
6、流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码(小米商城)、加入购物车(结账,锁库存还有一段时间);
7、限流&熔断&降级(必须)
前端限流+后端限流(限流把不合理的去除掉,如:一秒发送1w次的请求;就算合理的,次数太多也应该限制起来);限制次数,限制总量,快速失败降级运行(一部分流量引导到降级页面),熔断隔离防止雪崩;
8、队列削峰
1万个商品,每个1000件秒杀,双11;所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可;
2.秒杀核心流程
秒杀流程+消息队列监听流程
3. 秒杀系统编写
秒杀请求
@Autowired
private SeckillService seckillService;
@GetMapping("/kill")
public String secKill(@RequestParam("killId") String killId, // session_skuID
@RequestParam("key") String key,
@RequestParam("num") Integer num, Model model){
String orderSn = seckillService.kill(killId,key,num);
// 1.判断是否登录
model.addAttribute("orderSn", orderSn);
return "success";
}
登录验证:编写拦截器
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> threadLocal = 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){
HttpSession session = request.getSession();
MemberRespVo MemberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(MemberRespVo != null){
threadLocal.set(MemberRespVo);
return true;
}else{
// 没登陆就去登录
session.setAttribute("msg", AuthServerConstant.NOT_LOGIN);
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
return true;
}
}
将登录验证拦截器添加到webmvc的配置中
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
流量削峰
配置rabbitmq
ipAddr: "192.168.56.10"
spring:
rabbitmq:
virtual-host: /
host: ${ipAddr}
引入rabbitmq配置类,使用JSON序列化器,进行消息转换
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
设置交换机和队列的名字常量
package com.atguigu.common.constant;
public class RabbitInfo {
public static class Order{
// 其实厂里应该大写,但是我们为了区分,这里也不改了
public static final String exchange = "order-event-exchange";
public static final String delayQueue = "order.delay.queue";
public static final String delayRoutingKey = "order.locked";
public static final String releaseQueue = "order.release.queue";
public static final String releaseRoutingKey="order.release";
// 其他路由key也是跳到releaseQueue
public static final String baseRoutingKey="order.#";
public static final int ttl = 900000;
}
public static class Stock{
public static final String exchange="stock-event-exchange";
public static final String delayQueue="stock.delay.queue";
public static final String delayRoutingKey="stock.locked";
public static final String releaseQueue="stock.release.queue";
public static final String releaseRoutingKey="stock.release.queue";
public static final String baseRoutingKey="stock.#";
public static final int ttl = 900000;
}
public static class SecKill{
public static final String exchange="seckill-event-exchange";
public static final String delayQueue="seckill.delay.queue";
public static final String delayRoutingKey="seckill.locked";
public static final String releaseQueue="seckill.release.queue";
public static final String releaseRoutingKey="seckill.release.queue";
public static final int ttl = 900000;
}
}
给交换机发请求
// 3. userId+skuId在redis中标识买过商品
String redisKey = memberRespVo.getId() + "-" + skuId;
// 让数据自动过期
long ttl = redisTo.getEndTime() - redisTo.getStartTime();
// SETNX,也就是不存在的时候才占位,如果能占位成功,说明这个人没买过
Boolean aBoolean = stringRedisTemplate.opsForValue()
.setIfAbsent(redisKey,
num.toString(),
ttl<0?0:ttl,
TimeUnit.MILLISECONDS);
if(aBoolean){
// 占位成功 说明从来没买过
RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);
//使用tryAcquire()方法,因为acquire()是阻塞的
boolean acquire = semaphore.tryAcquire(num);
if(acquire){
// 秒杀成功
// 快速下单 发送MQ
/**生成订单号,这样数据库通过消息队列保存后,订单支付页面也知道保存的id是多少 */
String orderSn = IdWorker.getTimeId() + UUID.randomUUID().toString().replace("-","").substring(7,8);
SecKillOrderTo orderTo = new SecKillOrderTo();
orderTo.setOrderSn(orderSn);
orderTo.setMemberId(memberRespVo.getId());
orderTo.setNum(num);
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
rabbitTemplate.convertAndSend(RabbitInfo.Order.exchange,
RabbitInfo.SecKill.delayRoutingKey, orderTo);
// 返回订单号
return orderSn;
}
监听秒杀单的队列
package com.atguigu.gulimall.order.listener;
@RabbitListener(queues = RabbitInfo.SecKill.delayQueue)
@Component
public class OrderSecKillListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(SecKillOrderTo secKillOrderTo, Channel channel, Message message) throws IOException {
try {
// 秒杀的时候没有订单,这时候才创建订单
orderService.createSecKillOrder(secKillOrderTo);
// 手动ack确认消费
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
本文完!!!