谷粒商城:秒杀系统设计与编写

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);
		}
	}
}

本文完!!!

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值