第二十篇商城系统-秒杀功能设计与实现

Posted 波波烤鸭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第二十篇商城系统-秒杀功能设计与实现相关的知识,希望对你有一定的参考价值。

秒杀服务

一、商品上架

秒杀活动的结构图

通过定时任务触发:

/**
 * 定时上架秒杀商品信息
 */
@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中

二、秒杀商品查询

  通过当前时间获取对应的秒杀活动及对应的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));
    


然后对应的访问效果:

三、页面渲染

1.网关配置

首先在host中配置域名

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

然后重启服务访问:

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

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

     
  )

展示的效果

3.商品详情

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

首先我们需要在秒杀服务中提供一个根据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;
    

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

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

<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 οnclick='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.秒杀活动关注点

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

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

访问测试:

3.后端逻辑处理

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

3.1 登录校验

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

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

然后添加对应的配置信息

第二十篇商城系统-秒杀功能设计与实现

第二十篇:类操作符重载的相关规定与具体实现示例

Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_汇总贴

第二十篇:Summarisation摘要

第二十篇:Summarisation摘要

第二十篇:Summarisation摘要