You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

26 KiB

秒杀服务

一、商品上架

秒杀活动的结构图

image.png

通过定时任务触发:

/**
 * 定时上架秒杀商品信息
 */
@Slf4j
@Component
public class SeckillSkuSchedule {

    @Autowired
    SeckillService seckillService;

    @Autowired
    RedissonClient redissonClient;

    /**
     *
     */
    @Async
    @Scheduled(cron = "*/5 * * * * *")
    public void uploadSeckillSku3Days(){
        log.info("定时上架秒杀商品执行了...." + new Date());
        // 分布式锁
        RLock lock = redissonClient.getLock("seckill:upload:lock");
        lock.lock(10, TimeUnit.SECONDS);
        try {
            // 调用上架商品的方法
            seckillService.uploadSeckillSku3Days();
        }catch (Exception e){
            lock.unlock();
        }
    }

}

进入到Service中处理

@Override
    public void uploadSeckillSku3Days() {
        // 1. 通过OpenFegin 远程调用Coupon服务中接口来获取未来三天的秒杀活动的商品
        R r = couponFeignService.getLates3DaysSession();
        if(r.getCode() == 0){
            // 表示查询操作成功
            String json = (String) r.get("data");
            List<SeckillSessionEntity> seckillSessionEntities = JSON.parseArray(json,SeckillSessionEntity.class);
            // 2. 上架商品  Redis数据保存
            // 缓存商品
            //  2.1 缓存每日秒杀的SKU基本信息
            saveSessionInfos(seckillSessionEntities);
            // 2.2  缓存每日秒杀的商品信息
            saveSessionSkuInfos(seckillSessionEntities);

        }
    }

/**
     * 保存每日活动的信息到Redis中
     * @param seckillSessionEntities
     */
    private void saveSessionInfos(List<SeckillSessionEntity> seckillSessionEntities) {
        for (SeckillSessionEntity seckillSessionEntity : seckillSessionEntities) {
            // 循环缓存每一个活动  key start_endTime
            long start = seckillSessionEntity.getStartTime().getTime();
            long end = seckillSessionEntity.getEndTime().getTime();
            // 生成Key
            String key = SeckillConstant.SESSION_CHACE_PREFIX+start+"_"+end;
            Boolean flag = redisTemplate.hasKey(key);
            if(!flag){// 表示这个秒杀活动在Redis中不存在也就是还没有上架那么需要保存
                // 需要存储到Redis中的这个秒杀活动涉及到的相关的商品信息的SKUID
                List<String> collect = seckillSessionEntity.getRelationEntities().stream().map(item -> {
                    // 秒杀活动存储的 VALUE是 sessionId_SkuId
                    return item.getPromotionSessionId()+"_"+item.getSkuId().toString();
                }).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key,collect);
            }
        }
    }

    /**
     * 存储活动对应的 SKU信息
     * @param seckillSessionEntities
     */
    private void saveSessionSkuInfos(List<SeckillSessionEntity> seckillSessionEntities) {
        seckillSessionEntities.stream().forEach(session -> {
            // 循环取出每个Session然后取出对应SkuID 封装相关的信息
            BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
            session.getRelationEntities().stream().forEach(item->{
                String skuKey = item.getPromotionSessionId()+"_"+item.getSkuId();
                Boolean flag = redisTemplate.hasKey(skuKey);
                if(!flag){
                    SeckillSkuRedisDto dto = new SeckillSkuRedisDto();
                    // 1.获取SKU的基本信息
                    R info = productFeignService.info(item.getSkuId());
                    if(info.getCode() == 0){
                        // 表示查询成功
                        String json = (String) info.get("skuInfoJSON");
                        dto.setSkuInfoVo(JSON.parseObject(json,SkuInfoVo.class));
                    }
                    // 2.获取SKU的秒杀信息
                    /*dto.setSkuId(item.getSkuId());
                    dto.setSeckillPrice(item.getSeckillPrice());
                    dto.setSeckillCount(item.getSeckillCount());
                    dto.setSeckillLimit(item.getSeckillLimit());
                    dto.setSeckillSort(item.getSeckillSort());*/
                    BeanUtils.copyProperties(item,dto);
                    // 3.设置当前商品的秒杀时间
                    dto.setStartTime(session.getStartTime().getTime());
                    dto.setEndTime(session.getEndTime().getTime());

                    // 4. 随机码
                    String token = UUID.randomUUID().toString().replace("-","");
                    dto.setRandCode(token);
                    // 分布式信号量的处理  限流的目的
                    RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + token);
                    // 把秒杀活动的商品数量作为分布式信号量的信号量
                    semaphore.trySetPermits(item.getSeckillCount().intValue());
                    hashOps.put(skuKey,JSON.toJSONString(dto));
                }
            });
        });
    }

启动服务数据会被保存到Redis中

image.png

image.png

二、秒杀商品查询

通过当前时间获取对应的秒杀活动及对应的SKU信息。

   /**
     * 查询出当前时间内的秒杀活动及对应的商品SKU信息
     * @return
     */
    @Override
    public List<SeckillSkuRedisDto> getCurrentSeckillSkus() {
        // 1.确定当前时间是属于哪个秒杀活动的
        long time = new Date().getTime();
        // 从Redis中查询所有的秒杀活动
        Set<String> keys = redisTemplate.keys(SeckillConstant.SESSION_CHACE_PREFIX + "*");
        for (String key : keys) {
            //seckill:sessions1656468000000_1656469800000
            String replace = key.replace(SeckillConstant.SESSION_CHACE_PREFIX, "");
            // 1656468000000_1656469800000
            String[] s = replace.split("_");
            Long start = Long.parseLong(s[0]); // 活动开始的时间
            Long end = Long.parseLong(s[1]); // 活动结束的时间
            if(time > start && time < end){
                // 说明的秒杀活动就是当前时间需要参与的活动
                // 取出来的是SKU的ID  2_9
                List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
                List<String> list = ops.multiGet(range);
                if(list != null && list.size() > 0){
                    List<SeckillSkuRedisDto> collect = list.stream().map(item -> {
                        SeckillSkuRedisDto seckillSkuRedisDto = JSON.parseObject(item, SeckillSkuRedisDto.class);
                        return seckillSkuRedisDto;
                    }).collect(Collectors.toList());
                    return collect;
                }
            }
        }
        return null;
    }

然后定义相关的Controller接口就可以访问了

@RestController
@RequestMapping("/seckill")
public class SeckillController {

    @Autowired
    SeckillService seckillService;

    @GetMapping("/currentSeckillSessionSkus")
    public R getCurrentSeckillSessionSkus(){
        List<SeckillSkuRedisDto> currentSeckillSkus = seckillService.getCurrentSeckillSkus();

        return R.ok().put("data", JSON.toJSONString(currentSeckillSkus));
    }
}

然后对应的访问效果:

image.png

三、页面渲染

1.网关配置

首先在host中配置域名

image.png

然后在网关中配置路由信息

image.png

然后重启服务访问:

image.png

能访问到数据就表示域名配置成功

2.首页配置

通过Ajax来访问获取秒杀的相关信息

  $.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp){
     if(resp.data.length > 0){
        // 说明有秒杀的数据
        console.log($.parseJSON(resp.data))
       $.parseJSON(resp.data).forEach(function(item){
         $("<li></li>").append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
                 .append("<p>"+item.skuInfoVo.skuSubtitle+"</p>")
                 .append("<span>"+item.seckillPrice+"</span>")
                 .append("<s>"+item.skuInfoVo.price+"</s>")
                 .appendTo("#seckillSessionContent");
       })
       /*<li>
       <img src="/static/index/img/section_second_list_img1.jpg" alt="">
               <p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿9-14千克 (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿9-14千</p>
       <span>¥83.9</span><s>¥99.9</s>
     </li>*/

     }
  })

展示的效果

image.png

3.商品详情

  在购买商品的时候,进入到商品详情页,如果该商品也参与了秒杀活动,那么对应的需要展示相关的信息

image.png

首先我们需要在秒杀服务中提供一个根据SKUID查询相关的秒杀活动的接口

   /**
     * 根据SKUID查询秒杀活动对应的信息
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisDto getSeckillSessionBySkuId(Long skuId) {
        // 1.找到所有需要参与秒杀的商品的sku信息
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
        Set<String> keys = ops.keys();
        if(keys != null && keys.size() > 0){
            String regx = "\\d_"+ skuId;
            for (String key : keys) {
                boolean matches = Pattern.matches(regx, key);
                if(matches){
                    // 说明找到了对应的SKU的信息
                    String json = ops.get(key);
                    SeckillSkuRedisDto dto = JSON.parseObject(json, SeckillSkuRedisDto.class);
                    return dto;
                }
            }
        }
        return null;
    }

然后在查询商品详情的时候异步查询出对应的秒杀活动信息

image.png

然后在模板页面中展示相关的信息

<div class="box-summary clear">
                    <ul>
                        <li>京东价</li>
                        <li>
                            <span></span>
                            <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
                        </li>
                        <li style="color: red">
                            <span th:if="${#dates.createNow().getTime() < item.seckillVO.startTime}">
                                商品将在:[[${#dates.format(new java.util.Date(item.seckillVO.startTime),'yyyy-MM-dd HH:mm:ss')}]] 开始秒杀
                            </span>
                            <span th:if="${#dates.createNow().getTime() > item.seckillVO.startTime
                             && #dates.createNow().getTime() < item.seckillVO.endTime }">
                                秒杀价: [[${#numbers.formatDecimal(item.seckillVO.seckillPrice,1,2)}]]
                            </span>
                        </li>
                        <li>
                            <a href="/static/item/">
                                预约说明
                            </a>
                        </li>
                    </ul>
                </div>

首页调整到商品详情页

function goItem(skuId){
    location.href="http://item.msb.com/"+skuId+".html"
  }

  $.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp){
     if(resp.data.length > 0){
        // 说明有秒杀的数据
        console.log($.parseJSON(resp.data))
       $.parseJSON(resp.data).forEach(function(item){
         $("<li onclick='goItem("+item.skuId+")'></li>")
                 .append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
                 .append("<p>"+item.skuInfoVo.skuSubtitle+"</p>")
                 .append("<span>"+item.seckillPrice+"</span>")
                 .append("<s>"+item.skuInfoVo.price+"</s>")
                 .appendTo("#seckillSessionContent");
       })
       /*<li>
       <img src="/static/index/img/section_second_list_img1.jpg" alt="">
               <p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿9-14千克 (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿9-14千</p>
       <span>¥83.9</span><s>¥99.9</s>
     </li>*/

     }
  })

四、秒杀活动

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>

对应的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;
    });

访问测试:

image.png

3.后端逻辑处理

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

3.1 登录校验

秒杀活动必须是在登录状态下进行的如果没有认证就不让秒杀。这时我们需要整合进来SpringSession。

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

然后添加对应的配置信息

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

配置拦截器

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

}

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

最后在启动类中开启

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

配置拦截部分请求

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

}

合法性校验

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

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

MQ异步下单

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

image.png

然后在订单服务中订阅对应的信息

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

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

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"/>
    <title></title>
    <script type="text/javascript" src="/static/cart/js/jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="/static/cart/bootstrap/js/bootstrap.js"></script>
    <script type="text/javascript" src="/static/cart/js/swiper.min.js"></script>
    <script src="js/swiper.min.js"></script>
    <link rel="stylesheet" type="text/css" href="/static/cart/css/swiper.min.css"/>
    <link rel="stylesheet" type="text/css" href="/static/cart/bootstrap/css/bootstrap.css"/>
    <link rel="stylesheet" type="text/css" href="/static/cart/css/success.css"/>

</head>

<body>
<!--头部-->
<div class="alert-info">
    <div class="hd_wrap_top">
        <ul class="hd_wrap_left">
            <li class="hd_home"><i class="glyphicon glyphicon-home"></i>
                <a href="http://mall.msb.com/home">马士兵商城首页</a>
            </li>

        </ul>

        <ul class="hd_wrap_right">

            <li th:if="${session.loginUser == null}"><a href="http://auth.msb.com/login.html" style="color: red;">你好,请登录</a></li>
            <li th:if="${session.loginUser != null}"><span style="color: red;">[[${session.loginUser.nickname}]]</span></li>

            <li class="spacer"></li>

            <li>
                <a href="/javascript:;">我的订单</a>
            </li>


        </ul>

    </div>
</div>

<div class="nav-tabs-justified">
    <div class="nav_wrap">

        <div class="nav_top">
            <div class="nav_top_one">
                <a href="http://mall.msb.com/home"><img src="/static/cart/img/logo1.jpg"
                                                        style="height: 60px;width:180px;"/></a>
            </div>
            <div class="nav_top_two"><input type="text"/>
                <button>搜索</button>
            </div>


        </div>

    </div>
</div>

<div class="main">

    <div class="success-wrap">
        <div class="w" id="result">
            <div class="m succeed-box">
                <div class="mc success-cont" th:if="${orderSn!=null}">
                    <h1>恭喜您!秒杀成功,订单号[[${orderSn}]]</h1>
                    <h2>正在准备订单数据...10秒后跳转到支付页面</h2>
                    <a href="#">去支付</a>
                </div>
                <div class="mc success-cont" th:if="${orderSn==null}">
                    <h1>很遗憾~没有抢购到!欢迎下次再来参与....</h1>
                </div>
            </div>
        </div>
    </div>

</div>
</body>
<script type="text/javascript" src="/static/cart/js/success.js"></script>

</html>

image.png

image.png

好了~到这儿秒杀活动搞定!