秒杀系统设计

一、秒杀系统设计

1.秒杀系统介绍

1.1 秒杀(高并发)系统关注的问题

在这里插入图片描述

二、登录检查

1.前端限流

秒杀必须登录,进行前端限流。

   <div class="box-btns-two"
       th:if="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
     <a id="seckillA"
                  th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfo.promotionSessionId},code=${item.seckillInfo.randomCode}">
                            立即抢购
      </a>
   </div>
   <div class="box-btns-two"
                         th:if="${#dates.createNow().getTime() < item.seckillInfo.startTime || #dates.createNow().getTime() > item.seckillInfo.endTime}">
      <a id="addCart" th:attr="skuId=${item.info.skuId}">
                            加入购物车
      </a>
  </div>
    //立即抢购
    $("#seckillA").click(function () {
        var isLogin = [[${session.loginUser!=null}]];//true
        if (isLogin) {
            var killId = $(this).attr("sessionid") + "_" + $(this).attr("skuid");
            var key = $(this).attr("code");
            var num = $("#numInput").val();
            location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + key + "&num=" + num;
        }else {
            alert("秒杀前请先登录")
        }
        return false;
    })```

2.控制器请求拦截

2.1).依赖、配置

1).引入redis,session依赖
 <!--整合spring-session完成Session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <!--spring整合redis依赖,lettuce容易内存溢出,使用jedis不会-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--导入jedis依赖-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
2).引入redis,session配置
#session保存到redis
spring.session.store-type=redis

#配置redis
spring.redis.host=192.168.56.10
spring.redis.port=6379
3).引入session配置类
/**
 * @Description: spring Session配置类
 **/
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

        //放大作用域
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");

        return cookieSerializer;
    }


    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

}

4).引入拦截器
/**
 * @Description: 登录拦截器
 **/

@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();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/kill", uri);
        if (match) {
            HttpSession session = request.getSession();
            //获取登录的用户信息
            MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
            if (attribute != null) {
                //把登录后用户的信息放在ThreadLocal里面进行保存
                loginUser.set(attribute);
                return true;
            } else {
                //未登录,返回登录页面
                response.setContentType("text/html;charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
                // session.setAttribute("msg", "请先进行登录");
                // response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

5).使用拦截器-web Config
/**
 * @Description: web配置
 **/

@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {

    @Autowired
    LoginUserInterceptor loginUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

三、秒杀流程

1.流程一:添加购物车、支付(不采用)

像京东一样,秒杀就是优惠,添加购物车、支付。
在这里插入图片描述
缺点:秒杀需要级联其他服务、页面。流量大,每个页面可能扛不住压力
优点:整个业务是统一的、数据模型需要设计兼容。普通商品的信息与秒杀商品的信息,就差优惠、库存。

2.流程二、秒杀作为信号量(采用)

面对高流量,使用信号量。
在这里插入图片描述
优点:很少访问数据库。
缺点:极限下,订单服务宕机,会一直无法消费订单,造成一直支付不成功。

2.1 代码1-创建快速秒杀

1).秒杀操作-controller
    //立即抢购
    //http://seckill.gulimall.com/kill?killId=8_1&key=e965f89b-dd6a-4324-bb20-7634919edbba&num=1
    @GetMapping("/kill")
    public R kill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num) {
       //1.判断是否登录
        String orderSn = seckillService.kill(killId,key,num);
        System.out.println("秒杀商品的订单号: " + orderSn);
        return R.ok().setData(orderSn);
    }
2). 秒杀操作-实现类
    //秒杀
    @Override
    public String kill(String killId, String key, Integer num) {
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //0.登录检查。已经前端、拦截器已判断
        //1.获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = ops.get(killId);
        if (StringUtils.isEmpty(json)) {
            return null;
        } else {
            SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            //1.1校验合法性(秒杀时间、随机码、对应关系、幂等性)
            //校验秒杀时间
            Long startTime = redisTo.getStartTime();
            Long endTime = redisTo.getEndTime();
            long current = new Date().getTime();
            long ttl = startTime - endTime;
            if (current >= startTime && startTime <= endTime) {
                System.out.println("秒杀时间验证合法!");
                //校验随机码与商品id
                String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                String randomCode = redisTo.getRandomCode();
                if (randomCode.equals(key) && skuId.equals(killId)) {
                    System.out.println("随机码与商品id验证合法!");
                    //验证购物数量是否合理
                    if (num <= redisTo.getSeckillLimit()) {
                        System.out.println("验证购物数量 合理");
                        //幂等性:如果只要秒杀成功,就去redis占位,userId_sessionId_skuId
                        // 验证此人是否已经购买过
                        String redisKey = memberRespVo.getId() + "_" + redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                        //自动过期(秒杀场次结束,就过期)
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            //占位成功,此人未买过
                            //1.2 获取信号量
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            try {
                                boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                            //1.3 秒杀成功,快速下单(发送MQ消息)
                                String orderNo = IdWorker.getTimeId();
                                return orderNo;
                            } catch (InterruptedException e) {
                                return null;
                            }
                        } else {
                            //此人已经买过
                            return null;
                        }
                    } else {
                        return null;
                    }
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }
    }

2.2 代码2-发送MQ到订单服务

在这里插入图片描述

1).MQ依赖、配置
        <!--MQ依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
2).MyRabbitMQConfig
@Configuration
public class MyRabbitMQConfig {
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
3).秒杀订单的数据-common项目
@Data
public class SeckillOrderTo {
    // 订单号
    private String orderSn;
    // 活动场次id
    private Long promotionSessionId;
    //商品id
    private Long skuId;
    //秒杀价格
    private BigDecimal seckillPrice;
    //购买数量
    private Integer num;
    // 会员ID
    private Long memberId;
}
4).发送MQ消息

代码一2)实现类处,添加下面代码
在这里插入图片描述

	@Autowired
    RabbitTemplate rabbitTemplate;

   SeckillOrderTo orderTo = new SeckillOrderTo();
   orderTo.setOrderSn(orderSn);
   orderTo.setMemberId(memberRespVo.getId());
   orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
   orderTo.setSeckillPrice(redisTo.getSeckillPrice());
   orderTo.setSkuId(redisTo.getSkuId());
   orderTo.setNum(num);
   
   rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo );

2.3.订单项目

1).MyRabbitMQConfig 添加队列、绑定
    // 商品秒杀队列
    @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<String, Object> arguments
        Binding binding = new Binding(
                "order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
        return binding;
    }
2).订单秒杀监听器
/**
 * @Description: 订单秒杀监听器
 **/

@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
        log.info("准备创建秒杀单的详细信息...");
        try {
            orderService.createSeckillOrder(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}
3).订单实现类-createSeckillOrder方法
    //创建秒杀订单
    @Override
    public void createSeckillOrder(SeckillOrderTo seckillOrder) {
        //TODO 保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(seckillOrder.getOrderSn());
        orderEntity.setMemberId(seckillOrder.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());

        BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
        orderEntity.setPayAmount(multiply);
        this.save(orderEntity);

        //TODO 保存订单项信息
        OrderItemEntity itemEntity = new OrderItemEntity();
        itemEntity.setOrderSn(seckillOrder.getOrderSn());
        itemEntity.setSkuId(seckillOrder.getSkuId());
        itemEntity.setRealAmount(multiply);
        itemEntity.setSkuQuantity(seckillOrder.getNum());
        //todo 获取当前Sku详细信息进行设置
//        productFeignService.getSpuInfoBySkuId(seckillOrder.getSkuId());
        orderItemService.save(itemEntity);
    }

2.4.渲染商品页面-秒杀项目success.html

1).导入thymeleaf依赖、配置
  <!-- thymeleaf模板引擎 -->
   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
   </dependency>
        
spring.thymeleaf.cache=false
2).把cart项目的success.html复制到seckill中
a.修改静态资源,指向cart
src="http://cart.gulimall.com/static/cart/js/jquery-3.1.1.min.js"
b.支付成功,页面跳转支付
<div class="m succeed-box">
                <div th:if="${orderSn !=null}" class="mc success-cont">
                    <h1>恭喜,秒杀成功,订单号[[${orderSn}]]</h1>
                    <h2>正在准备订单数据,10s以后自动跳转支付页面
                        <a style="color: red" th:href="${'http://order.gulimall.com/payOrder?orderSn=' + orderSn}">点击支付</a>
                    </h2>
                </div>
                <div th:if="${orderSn==null}" class="mc success-cont">
                    <h1>秒杀失败,手气不好,下次再来!!</h1>
                </div>
            </div>
c.修改秒杀的控制器–抢购方法
@Controller
public class SeckillController {
    @Autowired
    SeckillService seckillService;

    //立即抢购
    //http://seckill.gulimall.com/kill?killId=8_1&key=e965f89b-dd6a-4324-bb20-7634919edbba&num=1
    @GetMapping("/kill")
    public String kill(@RequestParam("killId") String killId,
                       @RequestParam("key") String key,
                       @RequestParam("num") Integer num, Model model) {

        //1.判断是否登录
        String orderSn = seckillService.kill(killId,key,num);
        model.addAttribute("orderSn",orderSn);
         return "success";
    }
}

四、秒杀的完善

1 . TODO 上架秒杀商品的时候,每个数据都有过期时间

2 . TODO 秒杀后续的流程,简化了收货地址的信息

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值