[Business Functions Chapter 106] Microservices-springcloud-springboot-e-commerce order module--flash service-scheduled tasks [Part 2]

4. Flash sale activities

1. Focus on Flash Sale activities

  The biggest feature of flash sales activities is high concurrency and high concurrency in a short period of time, so the requirements for our services are very high. For the common problems caused by this situation, the corresponding solutions are:

image.png

2. Flash kill service front-end

  When we click 秒杀抢购the button, we need to submit the current product information to the back-end service. Activity number + "_" + SkuId, Code random code, and the number of products to be purchased.

<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>

Corresponding js operations

$("#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;
    });

Access test:

image.png

3. Back-end logic processing

  Flash sale requests submitted by the front end are specifically processed at the back end.

3.1 Login verification

  The flash sale activity must be carried out in the logged-in state. If there is no authentication, the flash sale will not be allowed. At this time we need to integrate SpringSession. Of course, we usually store user information in reids cache, so redis dependency also needs to be introduced.

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

Then add the corresponding configuration information

image.png

Then add the interceptor

/**
 * 秒杀活动的拦截器 确认是杂登录的状态下操作的
 */
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;
    }
}

Configure interceptor

@Configuration
public class MyWebInterceptorConfig implements WebMvcConfigurer {
    
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/seckill/kill");
    }

}

Set cookie configuration

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

Finally, enable it in the startup class

image.png

3.2 Flash sale activity process

image.png

Login verification

Processing through interceptors: Not all requests in flash sale activities need to be logged in, so this interceptor should only intercept some requests.

/**
 * 秒杀活动的拦截器 确认是杂登录的状态下操作的
 */
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;
    }
}

Configure to intercept some requests

@Configuration
public class MyWebInterceptorConfig implements WebMvcConfigurer {
    
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/seckill/kill");
    }

}
/**
 * 秒杀活动涉及的常量
 */
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:";
}

Flash request interface
controller layer

 /**
     * 秒杀抢购   跳转到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";
    }
}

service layer

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

Legality check

  There are four areas of verification: timeliness, whether the random code is legal, whether the purchase restriction conditions are met, and idempotence.

image.png

Semaphore handling

  Use semaphores to control the number of flash sale items. Reduced operations on inventory items and improved processing capabilities

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

MQ asynchronous order placing

  After the flash sale is successful, a message is sent to RocketMQ, and the order service subscribes to the message to implement asynchronous order placement, thus reducing the impact on the flash sale system.

image.png

  • Then subscribe to the corresponding information in the order service: create a consumer class to inject the bean and act as a listener to asynchronously listen to the specified message producer topic = OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC. The message producer is created by the flash kill service, and the message consumer is created by the order service. Or, because both services use the same RocketMQ, they are both accessible.
  • After listening to the corresponding message, it means that the flash sale is successful. Then the data passed through the formal parameter in onMessage is the data passed by the message producer JSON.toJSONString(orderDto)). The order encapsulates the relevant data class. After parsing, call Quick order interface in its own order service to insert corresponding order data information
  • Subsequent payment is required, which will not be shown here. It can be completed by calling the payment service based on the previous text.
@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);
    }
}
    /**
     * 快速完成订单的处理  秒杀活动
     * @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);
    }

The flash sale successfully jumps to the success page:

image.png

image.png

Guess you like

Origin blog.csdn.net/studyday1/article/details/132814378