• 【业务功能篇106】 微服务-springcloud-springboot-电商订单模块--秒杀服务-定时任务【下篇】


    四、秒杀活动

    1.秒杀活动关注点

      秒杀活动的最大特点就是高并发而且是短时间内的高并发,那么对我们的服务要求就非常高,针对这种情况所产生的共性问题,对应的解决方案:

    image.png

    2. 秒杀服务前端

      当我们点击 秒杀抢购按钮后,对应我们需要把当前的商品信息提交到后端服务。活动编号+“_”+SkuId,Code随机码,抢购商品的数量。

    <div class="box-btns-two" th:if="${#dates.createNow().getTime() < item.seckillVO.startTime
                                || #dates.createNow().getTime() >  item.seckillVO.endTime }">
                            <a href="#" id="addCart" th:attr="skuId=${item.info.skuId}">
                                加入购物车
                            a>
                        div>
                        <div class="box-btns-two" th:if="${#dates.createNow().getTime() > item.seckillVO.startTime
                                 && #dates.createNow().getTime() < item.seckillVO.endTime }">
                            <a href="#" id="seckillId" th:attr="skuId=${item.info.skuId},sessionId=${item.seckillVO.promotionSessionId},code=${item.seckillVO.randCode}">
                                抢购商品
                            a>
                        div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对应的js操作

    $("#seckillId").click(function(){
            var isLogin = [[${session.loginUser !=null}]]
            if(isLogin){
                // 1. 获取活动编号和SkuId 2_10
                var killId = $(this).attr("sessionId") + "_" + $(this).attr("skuId");
                // 2. 获取对应的随机码
                var code = $(this).attr("code");
                // 3. 获取秒杀的商品数量
                var num = $("#numInput").val();
                location.href="http://seckill.msb.com/seckill/kill?killId="+killId + "&code="+code+"&num="+num;
            }else{
                alert("请先登录才能参加秒杀活动!!!");
            }
    
            return false;
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    访问测试:

    image.png

    3.后端逻辑处理

      前端提交的秒杀请求,在后端具体的处理

    3.1 登录校验

      秒杀活动必须是在登录状态下进行的,如果没有认证就不让秒杀。这时我们需要整合进来SpringSession。 当然我们一般是把用户信息存在reids缓存,所以redis依赖也是要引入的

            <dependency>
                <groupId>org.springframework.sessiongroupId>
                <artifactId>spring-session-data-redisartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后添加对应的配置信息

    image.png

    然后添加拦截器

    /**
     * 秒杀活动的拦截器 确认是杂登录的状态下操作的
     */
    public class AuthInterceptor implements HandlerInterceptor {
    
        public static ThreadLocal threadLocal = new ThreadLocal();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            // 通过HttpSession获取当前登录的用户信息
            HttpSession session = request.getSession();
            Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS);
            if(attribute != null){
                MemberVO memberVO = (MemberVO) attribute;
                threadLocal.set(memberVO);
                return true;
            }
            // 如果 attribute == null 说明没有登录,那么我们就需要重定向到登录页面
            session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"请先登录");
            response.sendRedirect("http://auth.msb.com/login.html");
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    配置拦截器

    @Configuration
    public class MyWebInterceptorConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/seckill/kill");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    设置Cookie的配置

    @Configuration
    public class MySessionConfig {
    
        /**
         * 自定义Cookie的配置
         * @return
         */
        @Bean
        public CookieSerializer cookieSerializer(){
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setDomainName("msb.com"); // 设置session对应的一级域名
            cookieSerializer.setCookieName("msbsession");
            return cookieSerializer;
        }
    
        /**
         * 对存储在Redis中的数据指定序列化的方式
         * @return
         */
        @Bean
        public RedisSerializer<Object> redisSerializer(){
            return new GenericJackson2JsonRedisSerializer();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    最后在启动类中开启

    image.png

    3.2 秒杀活动流程

    image.png

    登录校验

    通过拦截器处理:在秒杀活动中并不是所有的请求都是需要在登录状态下的,所有这个拦截器应该只需要拦截部分的请求。

    /**
     * 秒杀活动的拦截器 确认是杂登录的状态下操作的
     */
    public class AuthInterceptor implements HandlerInterceptor {
    
        public static ThreadLocal threadLocal = new ThreadLocal();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            // 通过HttpSession获取当前登录的用户信息
            HttpSession session = request.getSession();
            Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS);
            if(attribute != null){
                MemberVO memberVO = (MemberVO) attribute;
                threadLocal.set(memberVO);
                return true;
            }
            // 如果 attribute == null 说明没有登录,那么我们就需要重定向到登录页面
            session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"请先登录");
            response.sendRedirect("http://auth.msb.com/login.html");
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    配置拦截部分请求

    @Configuration
    public class MyWebInterceptorConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/seckill/kill");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    /**
     * 秒杀活动涉及的常量
     */
    public class SeckillConstant {
    
        public static final String SESSION_CHACE_PREFIX = "seckill:sessions";
        public static final String SKU_CHACE_PREFIX = "seckill:skus";
        public static final String SKU_STOCK_SEMAPHORE = "seckill:stock:";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    秒杀请求接口
    controller层

     /**
         * 秒杀抢购   跳转到success页面 显示是否秒杀成功
         * killId=1_9&code=69d55333c9ec422381024d34fdfd3e85&num=1
         * @return
         */
    @Controller
    @RequestMapping("/seckill")
    public class SeckillController {
        @GetMapping("/kill")
        public String seckill(@RequestParam("killId") String killId,
                              @RequestParam("code") String code,
                              @RequestParam("num") Integer num,
                              Model model){
            String orderSN = seckillService.kill(killId,code,num);
            model.addAttribute("orderSn",orderSN);
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    service层

        @Autowired
        StringRedisTemplate redisTemplate;
    
        @Autowired
        RedissonClient redissonClient;
    
        @Autowired
        RocketMQTemplate rocketMQTemplate;
     /**
         * 实现秒杀逻辑
         * @param killId
         * @param code
         * @param num
         * @return
         */
        @Override
        public String kill(String killId, String code, Integer num) {
            // 1.根据killId获取当前秒杀的商品的信息  Redis中
            BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
            String json = ops.get(killId);
            if(StringUtils.isNotBlank(json)){
                SeckillSkuRedisDto dto = JSON.parseObject(json, SeckillSkuRedisDto.class);
                // 校验合法性  1.校验时效性
                Long startTime = dto.getStartTime();
                Long endTime = dto.getEndTime();
                long now = new Date().getTime();
                if(now > startTime && now < endTime){
                    // 说明是在秒杀活动时间范围内容的请求
                    // 2.校验 随机和商品 是否合法
                    String randCode = dto.getRandCode();
                    Long skuId = dto.getSkuId();
                    String redisKillId = dto.getPromotionSessionId() + "_" + skuId;
                    if(randCode.equals(code) && killId.equals(redisKillId)){
                        // 随机码校验合法
                        // 3.判断抢购商品数量是否合法
                        if(num <= dto.getSeckillLimit().intValue()){
                            // 满足限购的条件
                            // 4.判断是否满足 幂等性
                            // 只要抢购成功我们就在Redis中 存储一条信息 userId + sessionID + skuId
                            MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
                            Long id = memberVO.getId();
                            String redisKey = id + "_" + redisKillId;
                            Boolean aBoolean = redisTemplate.opsForValue()
                                    .setIfAbsent(redisKey, num.toString(), (endTime - now), TimeUnit.MILLISECONDS);
                            if(aBoolean){
                                // 表示数据插入成功 是第一次操作
                                RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE+randCode);
                                try {
                                    boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                    if(b){
                                        // 表示秒杀成功
                                        String orderSN = UUID.randomUUID().toString().replace("-", "");
                                        // 继续完成快速下订单操作  --> RocketMQ
                                        SeckillOrderDto orderDto = new SeckillOrderDto() ;
                                        orderDto.setOrderSN(orderSN);
                                        orderDto.setSkuId(skuId);
                                        orderDto.setSeckillPrice(dto.getSeckillPrice());
                                        orderDto.setMemberId(id);
                                        orderDto.setNum(num);
                                        orderDto.setPromotionSessionId(dto.getPromotionSessionId());
                                        // 通过RocketMQ 发送异步消息
                                        rocketMQTemplate.sendOneWay(OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC
                                                ,JSON.toJSONString(orderDto));
                                        return orderSN;
                                    }
                                } catch (InterruptedException e) {
                                    return null;
                                }
                            }
                        }
    
                    }
                }
            }
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    合法性校验

      校验的内容有四块:时效性,随机码是否合法,是否满足限购条件,还有幂等性

    image.png

    信号量处理

      通过信号量来控制秒杀的商品数量。降低了对库存商品操作,提升了处理能力

    if(aBoolean){
                                // 表示数据插入成功 是第一次操作
                                RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE+randCode);
                                try {
                                    boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                    if(b){
                                        // 表示秒杀成功
                                        String orderSN = UUID.randomUUID().toString().replace("-", "");
                                        // 继续完成快速下订单操作  --> RocketMQ
                                        SeckillOrderDto orderDto = new SeckillOrderDto() ;
                                        orderDto.setOrderSN(orderSN);
                                        orderDto.setSkuId(skuId);
                                        orderDto.setSeckillPrice(dto.getSeckillPrice());
                                        orderDto.setMemberId(id);
                                        orderDto.setNum(num);
                                        orderDto.setPromotionSessionId(dto.getPromotionSessionId());
                                        // 通过RocketMQ 发送异步消息
                                        rocketMQTemplate.sendOneWay(OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC
                                                ,JSON.toJSONString(orderDto));
                                        return orderSN;
                                    }
                                } catch (InterruptedException e) {
                                    return null;
                                }
                            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    MQ异步下单

      秒杀成功后给RocketMQ发送消息,订单服务订阅消息,实现异步下单,从而降低了对秒杀系统的影响。

    image.png

    • 然后在订单服务中订阅对应的信息:创建一个消费者类注入bean同时作为监听器异步去监听指定的消息生产者topic = OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC 由秒杀服务来创建 消息生产者,由订单服务来创建消息消费者,因两个服务用的都是同个RocketMQ,所以都是可以访问到的。
    • 监听到有对应消息后,表示秒杀成功,那么在onMessage中通过形参传递过来的数据,就是消息生产者所传递的数据JSON.toJSONString(orderDto)),订单封装相关数据类,解析之后,再调用自身订单服务中的快速下单接口,去插入对应的订单数据信息
    • 而后续还需要支付,这里不再展示,结合先前文字的调用支付服务去完成即可
    @RocketMQMessageListener(topic = OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC,consumerGroup = "test")
    @Component
    public class SeckillOrderConsumer implements RocketMQListener<String> {
        @Autowired
        OrderService orderService;
        @Override
        public void onMessage(String s) {
            // 订单关单的逻辑实现
            SeckillOrderDto orderDto = JSON.parseObject(s,SeckillOrderDto.class);
            orderService.quickCreateOrder(orderDto);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
        /**
         * 快速完成订单的处理  秒杀活动
         * @param orderDto
         */
        @Transactional
        @Override
        public void quickCreateOrder(SeckillOrderDto orderDto) {
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setOrderSn(orderDto.getOrderSN());
            orderEntity.setStatus(OrderConstant.OrderStateEnum.FOR_THE_PAYMENT.getCode());
            orderEntity.setMemberId(orderDto.getMemberId());
            orderEntity.setTotalAmount(orderDto.getSeckillPrice().multiply(new BigDecimal(orderDto.getNum())));
            this.save(orderEntity);
            OrderItemEntity itemEntity = new OrderItemEntity();
            // TODO 根据SKUID查询对应的SKU信息和SPU信息
            itemEntity.setOrderSn(orderDto.getOrderSN());
            itemEntity.setSkuPrice(orderDto.getSeckillPrice());
            itemEntity.setSkuId(orderDto.getSkuId());
            itemEntity.setRealAmount(orderDto.getSeckillPrice().multiply(new BigDecimal(orderDto.getNum())));
            itemEntity.setSkuQuantity(orderDto.getNum());
    
            orderItemService.save(itemEntity);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    秒杀成功跳转到成功页面:

    image.png

    image.png

  • 相关阅读:
    PHP和Mysql前后端交互效果实现
    Java项目源码基于javaweb的学籍管理系统
    授权控制-数据库用户分类、授权(GRANT)、收权(REVOKE)
    51单片机应用从零开始(六)·逻辑运算
    ffmeg 中fftools/Makefile 分析
    九州未来参与编制的《基于C-V2X的智能化网联化融合发展路线图》正式发布!
    Ansible中的角色使用
    基于intel D4系列深度相机 (D455)的单点测距和两点距离测量(代码讲解超详细)
    Netty02 - 回显服务器实例
    c语言中文件的定义
  • 原文地址:https://blog.csdn.net/studyday1/article/details/132814378