谷粒商城-分布式高级篇[商城业务-秒杀服务]

  1. 谷粒商城-分布式基础篇【环境准备】
  2. 谷粒商城-分布式基础【业务编写】
  3. 谷粒商城-分布式高级篇【业务编写】持续更新
  4. 谷粒商城-分布式高级篇-ElasticSearch
  5. 谷粒商城-分布式高级篇-分布式锁与缓存
  6. 项目托管于gitee


秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署

  • 限流方式

    • 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
    • Nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
    • 网关限流,限流的过滤器
    • 代码中使用分布式信号量
    • RabbitMq限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
  • 秒杀架构思路

    • 项目独立部署,独立秒杀模块gulimall-seckill
    • 使用定时任务每天三点上架最新秒杀商品,削减高峰期压力
    • 秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口
    • 库存预热,先从数据库中扣除一部分库存以redisson信号量的形式存储在redis中
    • 队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单
  • 秒杀系统设计
    谷粒商城-分布式高级篇[商城业务-秒杀服务]_第1张图片谷粒商城-分布式高级篇[商城业务-秒杀服务]_第2张图片

一、搭建秒杀服务环境

1、秒杀服务后台管理系统调整


1、配置网关

谷粒商城-分布式高级篇[商城业务-秒杀服务]_第3张图片

        - id: coupon_route
          uri: lb://gulimall-coupon
          predicates:
            - Path=/api/coupon/**
          filters:
            - RewritePath=/api/(?>.*),/$\{segment}

2、新增场次,关联商品

修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”代码如下:

package com.atguigu.gulimall.coupon.service.impl;


@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
        String promotionSessionId = (String) params.get("promotionSessionId");
        // 场次id不是null
        if (StringUtils.isEmpty(promotionSessionId)) {
            queryWrapper.eq("promotion_session_id",promotionSessionId);
        }
        IPage<SeckillSkuRelationEntity> page = this.page(
                new Query<SeckillSkuRelationEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

}


2、搭建秒杀服务环境


秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署

1、创建微服务模块

2、导入依赖

<dependency>
  <groupId>org.redissongroupId>
  <artifactId>redissonartifactId>
  <version>3.12.0version>
dependency>
<dependency>
    <groupId>com.atguigu.gulimallgroupId>
    <artifactId>gulimall-commonartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <exclusions>
        <exclusion>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        exclusion>
    exclusions>
dependency>

3、添加配置

spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=124.222.223.222

4、主启动类添加注解

package com.atguigu.gulimall.seckill;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallSeckillApplication.class, args);
    }

}



二、定时任务

由于秒杀服务是在高并发的情况下访问,每次访问都要查询数据库的话,可能会把数据库压垮!

我们可以在秒杀的商品在秒杀之前,将其上架 (放在缓存当中),每次从缓存中拿。秒杀需要用到的库存也可以存到缓存中。

谷粒商城-分布式高级篇[商城业务-秒杀服务]_第4张图片

使用 Cron Trigger Tutorial 框架,来做定时任务。

2.1、cron 表达式


  • 语法:秒 分 时 日 月 周 年(Spring不支持)
字段 允许值 允许的特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * ? / L C #
年(可选) 留空, 1970-2099 , - * /

特殊符号:

  • , :枚举,表示附加一个可能值
    • (cron=“7,9,23,* * * * ?”) :任意时刻的 7,9,23 秒启动这个任务
  • - : 表示一个指定的范围;
    • (cron=“7-20,* * * * ?”) :任意时刻的 7-20 秒之间,每秒启动一次
  • * :任意,所有值
    • 指定位置的任意时刻都可以
  • / :步长,符号前表示开始时间,符号后表示每次递增的值;
    • (cron=“7/5,* * * * ?”) :第7秒启动,每5秒一次
    • (cron=“/5,* * * * ?”) :任意秒启动,每5秒一次
  • ? :表示未说明的值,即不关心它为何值(出现在日和周几的位置,为了防止日和周几冲突,在周和日上如果要写通配符使用?)
    • (cron=“* * * 1 * ?”):每月的1号启动这个任务
    • (cron=“* * * 1 * 2”) :每月的1号,而且必须是周二启动这个任务
  • L :(出现在日和周的位置)
    • last :最后一个
    • (cron=“* * * ? * 2L”) :每月的最后一个周二
  • W
    • Work Day:工作日
    • (cron=“* * * W * ?”) :每个月的工作日出发
    • (cron=“* * * LW * ?”) :每个月的最后一个工作日出发
  • # :第几个,只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
    • (cron=“* * * ? * 5#2”) :每个月的第2个周5

一些cron表达式案例

*/5 * * * * ? 每隔5秒执行一次
 0 */1 * * * ? 每隔1分钟执行一次
 0 0 5-15 * * ? 每天5-15点整点触发
 0 0/3 * * * ? 每三分钟触发一次
 0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 
 0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
 0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
 0 0 10,14,16 * * ? 每天上午10点,下午2点,40 0 12 ? * WED 表示每个星期三中午120 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:102:44触发 
 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
 0 0 23 L * ? 每月最后一天23点执行一次
 0 15 10 L * ? 每月最后一日的上午10:15触发 
 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 
 0 15 10 * * ? 2005 2005年的每天上午10:15触发 
 0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 
 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发


"30 * * * * ?" 每半分钟触发任务
"30 10 * * * ?" 每小时的1030秒触发任务
"30 10 1 * * ?" 每天11030秒触发任务
"30 10 1 20 * ?" 每月2011030秒触发任务
"30 10 1 20 10 ? *" 每年102011030秒触发任务
"30 10 1 20 10 ? 2011" 2011102011030秒触发任务
"30 10 1 ? 10 * 2011" 201110月每天11030秒触发任务
"30 10 1 ? 10 SUN 2011" 201110月每周日11030秒触发任务
"15,30,45 * * * * ?"15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 1545秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第00秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10150秒触发任务
"0 15 10 L * ?" 每个月最后一天的10150秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10150秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10150秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10150秒触发任务

2.2、测试


  • 问题:定时任务默认是阻塞的。如何让它不阻塞?
  • 解决:使用异步+定时任务来完成定时任务不阻塞的功能
    • 定时任务:
      1. @EnableScheduling 开启定时任务
      2. @Scheduled 开启一个定时任务
      3. 自动配置类 TaskSchedulingAutoConfiguration
    • 异步任务:
      1. @EnableAsync 开启异步任务功能
      2. @Async :给我希望异步执行的方法上标注
      3. 自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
package com.atguigu.gulimall.seckill.scheduled;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * Data time:2022/4/16 20:20
 * StudentID:2019112118
 * Author:hgw
 * Description: 定时调度测试
 * 定时任务:
 *  1、@EnableScheduling 开启定时任务
 *  2、@Scheduled 开启一个定时任务
 *  3、自动配置类 TaskSchedulingAutoConfiguration
 * 异步任务:
 *  1、@EnableAsync 开启异步任务功能
 *  2、@Async :给我希望异步执行的方法上标注
 *  3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
 */
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {

    /**
     * 1、spring中corn 表达式由6为组成,不允许第7位的年  Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
     * 2、在周几的位置,1-7分别代表:周一到周日(MON-SUN)
     * 3、定时任务默认是阻塞的。如何让它不阻塞?
     *      1)、可以让业务运行以异步的方式,自己提交到线程池
     *      2)、Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
     *              spring.task.scheduling.pool.size=5
     *      3)、让定时任务异步执行
     *          异步任务
     *   解决:使用异步+定时任务来完成定时任务不阻塞的功能
     */
    @Async
    @Scheduled(cron = "* * * * * 6")
    public void hello() throws InterruptedException {
        log.info("hello.....");
        Thread.sleep(3000);
    }
}

配置定时任务参数



三、商品上架


在这里插入图片描述

第一步、远程查询最近 3 天内秒杀的活动 以及 秒杀活动的关联的商品信息

1)、gulimall-seckill服务中编写 gulimall-coupon服务的远程调用接口

1、gulimall-seckill服务中编写 gulimall-coupon服务的远程调用接口

gulimall-seckill服务 的 com.atguigu.gulimall.seckill.feign 路径下的 CouponFeignService类

package com.atguigu.gulimall.seckill.feign;

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Data time:2022/4/16 21:05
 * StudentID:2019112118
 * Author:hgw
 * Description: 远程调用优惠服务接口
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @GetMapping("/coupon/seckillsession/lates3DaySession")
    R getLates3DaySession();
}

2、gulimall-seckill服务中编写 gulimall-coupon服务获取的数据的Vo

package com.atguigu.gulimall.seckill.vo;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
public class SeckillSessionsWithSkus {

    /**
     * id
     */
    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;

    private List<SeckillSkuVo> relationSkus;
}
package com.atguigu.gulimall.seckill.vo;

import com.baomidou.mybatisplus.annotation.TableId;

import java.math.BigDecimal;

@Data
public class SeckillSkuVo {

    /**
     * id
     */
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private BigDecimal seckillCount;
    /**
     * 每人限购数量
     */
    private BigDecimal seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
}

2)、gulimall-coupon服务 编写扫描数据库最近3天需要上架的秒杀活动 以及 秒杀活动需要的商品

1、Controller 层接口编写

package com.atguigu.gulimall.coupon.controller;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import com.atguigu.gulimall.coupon.entity.SeckillSessionEntity;
import com.atguigu.gulimall.coupon.service.SeckillSessionService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.R;



/**
 * 秒杀活动场次
 *
 * @author leifengyang
 * @email [email protected]
 * @date 2019-10-08 09:36:40
 */
@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
    @Autowired
    private SeckillSessionService seckillSessionService;

    /**
     * 查询三天内需要上架的服务
     * @return
     */
    @GetMapping("/lates3DaySession")
    public R getLates3DaySession(){
        List<SeckillSessionEntity> sessions =  seckillSessionService.getLates3DaySession();
        return R.ok().setData(sessions);
    }

2、Service 层实现类编写

package com.atguigu.gulimall.coupon.service.impl;

@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {


    @Autowired
    SeckillSkuRelationService seckillSkuRelationService;

    @Override
    public List<SeckillSessionEntity> getLates3DaySession() {
        // 计算最近3天
        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));

        if (list!=null && list.size()>0) {
            List<SeckillSessionEntity> collect = list.stream().map(session -> {
                Long id = session.getId();
                List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                session.setRelationSkus(relationEntities);
                return session;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

第二步、在Redis中保存秒杀场次信息

package com.atguigu.gulimall.seckill.service.impl;

@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";

    /**
     * 缓存活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
        sessions.stream().forEach(session ->{
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            System.out.println(key);
            List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
            // 缓存活动信息
            redisTemplate.opsForList().leftPushAll(key,collect);
        });
    }

第三步、在Redis中保存秒杀活动关联的商品信息


  1. Sku的基本信息
  2. Sku的秒杀信息
/**
 * 缓存活动的关联商品信息
 * @param sessions
 */
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
    sessions.stream().forEach(session->{
        // 准备Hash操作
        BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        session.getRelationSkus().stream().forEach(seckillSkuVo -> {
            // 缓存商品
            SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
            // 1、Sku的基本数据
            R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
            if (skuInfo.getCode() == 0) {
                SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                redisTo.setSkuInfo(info);
            }

            // 2、Sku的秒杀信息
            BeanUtils.copyProperties(seckillSkuVo,  redisTo);

            // 3、设置上当前商品的秒杀时间信息
            redisTo.setStartTime(session.getStartTime().getTime());
            redisTo.setEndTime(session.getEndTime().getTime());

            // 4、商品的随机码
            String token = UUID.randomUUID().toString().replace("_", "");
            redisTo.setRandomCode(token);

            // 5、引入分布式的信号量 限流
            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
            semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());

            String jsonString = JSON.toJSONString(redisTo);
            ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
        });
    });
}

1)、封装秒杀商品的详细信息 To

package com.atguigu.gulimall.seckill.to;

import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;

import java.math.BigDecimal;

/**
 * Data time:2022/4/16 22:20
 * StudentID:2019112118
 * Author:hgw
 * Description: 秒杀商品的详细信息
 */
@Data
public class SecKillSkuRedisTo {

    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品秒杀的随机码
     */
    private String randomCode;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private BigDecimal seckillCount;
    /**
     * 每人限购数量
     */
    private BigDecimal seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    /**
     * sku的详细信息
     */
    private SkuInfoVo skuInfo;
    /**
     * 当前商品秒杀活动的开始时间
     */
    private Long startTime;
    /**
     * 当前商品秒杀活动的结束时间
     */
    private Long endTime;
}
package com.atguigu.gulimall.seckill.vo;

@Data
public class SkuInfoVo {
    /**
     * skuId
     */
    private Long skuId;
    /**
     * spuId
     */
    private Long spuId;
    /**
     * sku名称
     */
    private String skuName;
    /**
     * sku介绍描述
     */
    private String skuDesc;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 默认图片
     */
    private String skuDefaultImg;
    /**
     * 标题
     */
    private String skuTitle;
    /**
     * 副标题
     */
    private String skuSubtitle;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 销量
     */
    private Long saleCount;
}

2)、编写远程查询 Sku基本信息 的接口

  1. 在 gulimall-seckill 服务中编写 远程调用 gulimall-product 服务中的 查询sku基本信息的方法
package com.atguigu.gulimall.seckill.feign;

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Data time:2022/4/16 22:36
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);

}

第四步、幂等性保证


谷粒商城-分布式高级篇[商城业务-秒杀服务]_第5张图片

  1. 加上分布式锁
    • 保证在分布式的情况下,锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态
  2. 代码逻辑编写
    • 当查询Redis中已经上架的秒杀场次和秒杀关联的商品,则不进行上架

第一步、加锁

package com.atguigu.gulimall.seckill.scheduled;

@Slf4j
@Service
public class SeckillSkuScheduled {

    @Autowired
    SeckillService seckillService;

    @Autowired
    RedissonClient redissonClient;

    private final String upload_lock = "seckill:upload:lock";

    // TODO 幂等性处理
    @Scheduled(cron = "* * 3 * * ?")
    public void uploadSeckillSkuLatest3Days() {
        // 1、重复上架无需处理
        log.info("上架秒杀商品的信息");
        // 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            seckillService.uploadSeckillSkuLatest3Days();
        } finally {
            lock.unlock();
        }
    }

}

第二步、判断Redis中是否已上架

package com.atguigu.gulimall.seckill.service.impl;

@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redissonClient;

    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    // + 商品随机码

    /**
     * 远程查询最近 3 天内秒杀的活动 以及 秒杀活动的关联的商品信息
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1、扫描最近三天数据库需要参与秒杀的活动
        R session = couponFeignService.getLates3DaySession();
        if (session.getCode() == 0) {
            // 上架商品
            List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            // 缓存到Redis
            // 1)、缓存活动信息
            saveSessionInfos(sessionData);
            // 2)、缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }
    }

    /**
     * 缓存活动信息
     *
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = redisTemplate.hasKey(key);
            if (!hasKey) {
                // 缓存活动信息
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId().toString()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }

    /**
     * 缓存活动的关联商品信息
     *
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            // 准备Hash操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                // 生成随机码
                String token = UUID.randomUUID().toString().replace("_", "");

                // 1)、缓存商品
                if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString())) {
                    SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                    // 1、Sku的基本数据
                    R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (skuInfo.getCode() == 0) {
                        SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        redisTo.setSkuInfo(info);
                    }

                    // 2、Sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo, redisTo);

                    // 3、设置上当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());

                    // 4、商品的随机码
                    redisTo.setRandomCode(token);

                    String jsonString = JSON.toJSONString(redisTo);
                    ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString);

                    // 如果当前这个场次的商品的库存信息已经上架就不需要上架
                    // 5、引入分布式的信号量 限流
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                }

            });
        });
    }

}


四、获取当前的秒杀商品 并 展示

4.1、获取当前的秒杀商品


  1. Controller层接口
package com.atguigu.gulimall.seckill.controller;

@RestController
public class SeckillController {

    @Autowired
    SeckillService seckillService;

    /**
     * 返回当前时间可以参与秒杀的商品信息
     * @return
     */
    @GetMapping("/currentSeckillSkus")
    public R getCurrentSeckillSkus(){
        List<SecKillSkuRedisTo> vos =  seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }
}
  1. Service 层实现类方法编写

gulimall-seckill 服务的 com/atguigu/gulimall/seckill/service/impl 路径下的 SeckillServiceImpl.java

/**
 * 获取当前参与秒杀的商品
 * @return
 */
@Override
public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
    // 1、确定当前时间属于哪个秒杀场次
    long time = new Date().getTime();
    Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
    for (String key : keys) {
        // seckill:sessions:1650153600000_1650160800000
        String replace = key.replace(SESSION_CACHE_PREFIX, "");
        String[] s = replace.split("_");
        long start = Long.parseLong(s[0]);
        long end = Long.parseLong(s[1]);
        if (time>= start && time<=end) {
            // 2、获取指定秒杀场次需要的所有商品信息
            List<String> range = redisTemplate.opsForList().range(key, -100, 100);
            BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            List<String> list = hashOps.multiGet(range);
            if (list!=null) {
                List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
                    SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
                    redis.setRandomCode(null);  // 当前秒杀开始了需要随机码
                    return redis;
                }).collect(Collectors.toList());
                return collect;
            }
            break;
        }
    }
    return null;
}

4.2、首页获取并拼装数据


第一步、环境配置

1、配置网关

- id: gulimall_seckill_route
  uri: lb://gulimall-seckill
  predicates:
    - Host=seckill.gulimall.cn

2、配置域名 vim /etc/hosts

# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
127.0.0.1 order.gulimall.cn
127.0.0.1 member.gulimall.cn
127.0.0.1 seckill.gulimall.cn
# Gulimall Host End

第二步、页面修改


修改 gulimall-product 服务的 index.html :

<div class="section_second_list">
  <div class="swiper-container swiper_section_second_list_left">
    <div class="swiper-wrapper">
      <div class="swiper-slide">
        <ul id="seckillSkuContent">
        ul>
function to_href(skuId) {
  location.href = "http://item.gulimall.cn/"+skuId+".html";
}
$.get("http://seckill.gulimall.cn/currentSeckillSkus",function (resp) {
  if (resp.data.length > 0) {
    resp.data.forEach(function (item) {
      $("
  • "
    ) .append($("")) .append($("

    "+ item.skuInfo.skuTitle +"

    "
    )) .append($(""+ item.seckillPrice +"")) .append($(""+ item.skuInfo.price +"")) .appendTo("#seckillSkuContent"); }); }





    五、商品详情页获取当前商品的秒杀信息

    5.1、编写 获取某个商品的秒杀预告信息


    主体:修改 gulimall-product 服务的SkuInfoServiceImpl 类的 item 方法

    gulimall-product 服务的 com.atguigu.gulimall.product.service.impl 路径下的 SkuInfoServiceImpl类:

    @Override
    public SkuItemVo item(Long skuId) {
        SkuItemVo skuItemVo = new SkuItemVo();
    
        // 1、sku基本信息    pms_sku_info
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);
    
        // 2、获取 spu 的销售属性组合
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync(res -> {
            List<SkuItemSaleAttrsVo> saleAttrVos = saleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);
    
        // 3、获取 spu 的介绍 pms_spu_info_desc
        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);
        }, executor);
    
        // 4、获取 spu 的规格参数信息 pms_spu_info_desc
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);
    
        // 5、sku的图片信息   pms_sku_images
        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);
    
        // 6、查询当前sku是否参与秒杀优惠
        CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
            R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
            if (seckillInfo.getCode() == 0) {
                SeckillInfoVo seckillInfoVo = seckillInfo.getData(new TypeReference<SeckillInfoVo>() {
                });
                skuItemVo.setSeckillInfo(seckillInfoVo);
            }
        }, executor);
    
        // 等待所有任务都完成
        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).join();
    
        return skuItemVo;
    }
    

    第一步、在gulimall-product 服务中编写 远程调用gulimall-seckill 服务的feign接口

    package com.atguigu.gulimall.product.feign;
    
    @FeignClient("gulimall-seckill")
    public interface SeckillFeignService {
        @GetMapping("/sku/seckill/{skuId}")
        R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
    }
    

    封装接收VO:

    package com.atguigu.gulimall.product.vo;
    
    /**
     * Data time:2022/4/5 10:34
     * StudentID:2019112118
     * Author:hgw
     * Description: 商品详情
     */
    
    @Data
    public class SkuItemVo {
        // 1、sku基本信息    pms_sku_info
        SkuInfoEntity info;
    
        // 是否有货
        boolean hasStock = true;
    
        // 2、sku的图片信息   pms_sku_images
        List<SkuImagesEntity> images;
    
        // 3、获取 spu 的销售属性组合
        List<SkuItemSaleAttrsVo> saleAttr;
    
        // 4、获取 spu 的介绍 pms_spu_info_desc
        SpuInfoDescEntity desp;
    
        // 5、获取 spu 的规格参数信息
        List<SpuItemAttrGroupVo> groupAttrs;
    
        // 6、当前商品的秒杀优惠信息
        SeckillInfoVo seckillInfo;
    }
    
    package com.atguigu.gulimall.product.vo;
    
    @Data
    public class SeckillInfoVo {
        /**
         * 活动id
         */
        private Long promotionId;
        /**
         * 活动场次id
         */
        private Long promotionSessionId;
        /**
         * 商品id
         */
        private Long skuId;
        /**
         * 商品秒杀的随机码
         */
        private String randomCode;
        /**
         * 秒杀价格
         */
        private BigDecimal seckillPrice;
        /**
         * 秒杀总量
         */
        private BigDecimal seckillCount;
        /**
         * 每人限购数量
         */
        private BigDecimal seckillLimit;
        /**
         * 排序
         */
        private Integer seckillSort;
        /**
         * 当前商品秒杀活动的开始时间
         */
        private Long startTime;
        /**
         * 当前商品秒杀活动的结束时间
         */
        private Long endTime;
    }
    

    第二步、在gulimall-seckill 服务中编写 获取某个商品的秒杀预告信息 接口

    1、gulimall-seckill 服务 com.atguigu.gulimall.seckill.controller 路径下的 SeckillController 类,代码如下:

    package com.atguigu.gulimall.seckill.controller;
    
    @RestController
    public class SeckillController {
    
        @Autowired
        SeckillService seckillService;
        /**
         * 获取某个商品的秒杀预告信息
         * @param skuId
         * @return
         */
        @GetMapping("/sku/seckill/{skuId}")
        public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
    
            SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
            return R.ok().setData(to);
        }
    }
    

    2、gulimall-seckill 服务 com.atguigu.gulimall.seckill.service.impl 路径下的 SeckillServiceImpl 类,代码如下:

    /**
     * 获取某个商品的秒杀预告信息
     * @param skuId
     * @return
     */
    @Override
    public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        // 1、找到所有需要参与秒杀的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size()>0) {
            String regx = "\\d_"+skuId;
            for (String key : keys) {
                if (Pattern.matches(regx,key)) {
                    String json = hashOps.get(key);
                    SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
    
                    long current = new Date().getTime();
                    Long startTime = skuRedisTo.getStartTime();
                    Long endTime = skuRedisTo.getEndTime();
                    if (current>=startTime && current<=endTime){
                        // 在秒杀活动时
                    } else {
                        // 不在秒杀活动时不应该传递随机码
                        skuRedisTo.setRandomCode("");
                    }
                    return skuRedisTo;
                }
            }
        }
        return null;
    }
    

    5.2、商品详情页前端渲染


    修改 item.html 页面

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

    谷粒商城-分布式高级篇[商城业务-秒杀服务]_第6张图片



    六、登录检查

    6.1、商品详情页修改

    • 在秒杀活动时,商品显示:立刻抢购
      • 登录才跳转至 秒杀服务
      • 未登录不跳转
    • 在秒杀活动外,商品显示:加入购物车

    1、修改 item.html 页面

    <div class="box-btns-two" th:if="${item.seckillInfo != null && (item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime)}">
        <a href="#" 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="${item.seckillInfo == null || (item.seckillInfo.startTime > #dates.createNow().getTime() || #dates.createNow().getTime() > item.seckillInfo.endTime)}">
        <a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">
            加入购物车
        a>
    div>
    
    • 前端要考虑秒杀系统设计的限流思想
    • 在进行立即抢购之前,前端先进行判断是否登录
    $("#secKillA").click(function () {
        var islogin = [[${session.loginUser!=null}]];
        if (islogin) {
            var killId = $(this).attr("sessionid")+"_"+$(this).attr("skuid");
            var key = $(this).attr("code");
            var num = $("#numInput").val();
            location.href = "http://seckill.gulimall.cn/kill?killId="+killId+"&key="+key+"&num="+num;
        } else {
            alert("秒杀请先登录!");
        }
        return false;
    });
    

    6.2、秒杀服务登录检查


    1、引入SpringSession依赖的Redis

    
    <dependency>
        <groupId>org.springframework.sessiongroupId>
        <artifactId>spring-session-data-redisartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettucegroupId>
                <artifactId>lettuce-coreartifactId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>redis.clientsgroupId>
        <artifactId>jedisartifactId>
    dependency>
    

    2、在配置文件中添加SpringSession的保存方式

    #SpringSession的保存方式
    spring.session.store-type=redis
    

    3、主启动类开启RedisHttpSession这个功能

    package com.atguigu.gulimall.seckill;
    
    @EnableRedisHttpSession
    @EnableFeignClients
    @EnableDiscoveryClient
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class GulimallSeckillApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GulimallSeckillApplication.class, args);
        }
    
    }
    

    4、编写SpringSession的配置

    package com.atguigu.gulimall.seckill.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.session.web.http.CookieSerializer;
    import org.springframework.session.web.http.DefaultCookieSerializer;
    
    /**
     * Data time:2022/4/9 10:19
     * StudentID:2019112118
     * Author:hgw
     * Description: 自定义Session 配置
     */
    @Configuration
    public class GulimallSessionConfig {
    
        @Bean
        public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setDomainName("gulimall.cn");
            cookieSerializer.setCookieName("GULISESSION");
    
            return cookieSerializer;
        }
    
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
            return new GenericJackson2JsonRedisSerializer();
        }
    }
    

    5、编写用户登录拦截器 并 配置到Spring容器中

    package com.atguigu.gulimall.seckill.interceptoe;
    
    @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 matcher = new AntPathMatcher();
            boolean match = matcher.match("/kill", uri);
            if (match){
                MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
                if (attribute!=null){
                    loginUser.set(attribute);
                    return true;
                } else {
                    // 没登录就去登录
                    request.getSession().setAttribute("msg", "请先进行登录");
                    response.sendRedirect("http://auth.gulimall.cn/login.html");
                    return false;
                }
            }
            return true;
        }
    }
    
    • 把拦截器配置到spring中,否则拦截器不生效。
    • 添加addInterceptors表示当前项目的所有请求都要经过这个拦截请求

    添加“com.atguigu.gulimall.seckill.config.SeckillWebConfig”类,代码如下:

    package com.atguigu.gulimall.seckill.config;
    
    @Configuration
    public class SeckillWebConfiguration implements WebMvcConfigurer {
    
        @Autowired
        LoginUserInterceptor interceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(interceptor).addPathPatterns("/**");
        }
    }
    


    七、秒杀

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GJrfVDgy-1650200866286)(谷粒商城-分布式高级篇[商城业务-秒杀服务].assets/image-20220417170203168.png)]

    7.1、秒杀请求处理


    1、Controller层接口的编写

    package com.atguigu.gulimall.seckill.controller;
    
    @RestController
    public class SeckillController {
    
        @Autowired
        SeckillService seckillService;
    
        /**
         * 秒杀请求
         * @return
         */
        @GetMapping("/kill")
        public R secKill(@RequestParam("killId") String killId,
                         @RequestParam("key") String key,
                         @RequestParam("num") Integer num) {
            String orderSn = seckillService.kill(killId,key,num);
            return R.ok().setData(orderSn);
        }
    }
    

    7.2、引入rabbitMQ依赖


    使用队列进行削峰

    在这里插入图片描述

    1、引入依赖

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-amqpartifactId>
    dependency>
    

    2、编写配置

    #RabbitMq的配置
    spring.rabbitmq.host=124.222.223.222
    spring.rabbitmq.virtual-host=/
    

    3、编写配置类

    package com.atguigu.gulimall.seckill.config;
    
    @Configuration
    public class MyRabbitConfig {
    
        @Autowired
        RabbitTemplate rabbitTemplate;
    
        /**
         * 使用JSON序列化机制,进行消息转换
         * @return
         */
        @Bean
        public MessageConverter messageConverter(){
            return new Jackson2JsonMessageConverter();
        }
    }
    

    4、编写 创建消息队列、以及消息队列和交换器的绑定

    在 gulimall-order 服务的 com.atguigu.gulimall.order.config 路径 MyMQConfig 类中,加入以下代码:

    @Bean
    public Queue orderSeckillOrderQueue() {
        return new Queue("order.seckill.order.queue",true,false,false);
    }
    
    @Bean
    public Binding orderSeckillOrderQueueBinding() {
        return new Binding("order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
    }
    

    7.3、创建订单


    Service层实现类的方法编写

    gulimall-seckill 服务的 com.atguigu.gulimall.seckill.service.impl 路径下的 SeckillServiceImpl实现类

    /**
     * 秒杀处理,发送消息给MQ
     * @param killId 存放的key
     * @param key 随机码
     * @param num 购买数量
     * @return  生成的订单号
     */
    @Override
    public String kill(String killId, String key, Integer num) {
    
        MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
    
        // 1、获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(json)) {
            return null;
        } else {
            SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
            // 2、校验合法性
            long time = new Date().getTime();
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
    
            long ttl = endTime - time;
            // 2.1、校验时间的合法性
            if (time >= startTime && time <= endTime) {
                // 2.2、校验随机码 和 商品id 是否正确
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                if (randomCode.equals(key) && killId.equals(skuId)) {
                    // 2.3、验证购物车数量是否合理
                    if (num <= redis.getSeckillLimit().intValue()) {
                        // 2.4、验证这个人是否购买过。幂等性:如果只要秒杀成功,就去占位。 userId_SessionId_skuId
                        String redisKey = respVo.getId() + "_" + skuId;
                        // 自动过期
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            // 占位成功说明从来没有买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            try {
                                boolean tryAcquire = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                // 秒杀成功
                                // 3、快速下单,给MQ发送消息
                                String timeId = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(respVo.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(redis.getSeckillPrice());
                                
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo);
    
                                return timeId;
                            } catch (InterruptedException e) {
                                return null;
                            }
                        } else {
                            // 说明已经买过了
                            return null;
                        }
                    }
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }
    
        return null;
    }
    

    消息传递的TO

    package com.atguigu.common.to.mq;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    
    /**
     * Data time:2022/4/17 17:50
     * StudentID:2019112118
     * Author:hgw
     * Description: 秒杀订单
     */
    @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;
    }
    

    7.4、监听队列,进行订单处理


    package com.atguigu.gulimall.order.listener;
    
    @Slf4j
    @RabbitListener(queues = "order.seckill.order.queue")
    @Component
    public class OrderSeckillListener {
    
        @Autowired
        OrderService orderService;
    
        @RabbitHandler
        public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
            try {
                log.info("准备创建秒杀单的详细信息:"+seckillOrder);
                orderService.createSeckillOrder(seckillOrder);
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e){
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            }
        }
    
    }
    

    2、gulimall-order 服务的 com.atguigu.gulimall.order.service.impl 路径下 OrderServiceImpl,方法:

    /**
     * 秒杀单的详细信息创建
     * @param seckillOrder
     */
    @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 orderItemEntity = new OrderItemEntity();
        orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
        orderItemEntity.setRealAmount(multiply);
        orderItemEntity.setSkuQuantity(seckillOrder.getNum());
        // TODO 获取当前SKU的详细信息进行设置
    
        orderItemService.save(orderItemEntity);
    }
    

    7.5、秒杀页面


    1、引入thymeleaf

    1. 导入依赖

      
      <dependency>
          <groupId>org.springframework.bootgroupId>
          <artifactId>spring-boot-starter-thymeleafartifactId>
      dependency>
      
    2. 在配置里关闭thymeleaf缓存

      #关闭缓存
      spring.thymeleaf.cache=false
      

    2、修改Controller层代码进行页面跳转

    package com.atguigu.gulimall.seckill.controller;
    
    @Controller
    public class SeckillController {
    
        @Autowired
        SeckillService seckillService;
    
        /**
         * 返回当前时间可以参与秒杀的商品信息
         * @return
         */
        @ResponseBody
        @GetMapping("/currentSeckillSkus")
        public R getCurrentSeckillSkus(){
            List<SecKillSkuRedisTo> vos =  seckillService.getCurrentSeckillSkus();
            return R.ok().setData(vos);
        }
    
        /**
         * 获取某个商品的秒杀预告信息
         * @param skuId
         * @return
         */
        @ResponseBody
        @GetMapping("/sku/seckill/{skuId}")
        public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
    
            SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
            return R.ok().setData(to);
        }
    
    
        /**
         * 秒杀请求
         * @return
         */
        @GetMapping("/kill")
        public String secKill(@RequestParam("killId") String killId,
                         @RequestParam("key") String key,
                         @RequestParam("num") Integer num,
                              Model model) {
            String orderSn = seckillService.kill(killId,key,num);
            model.addAttribute("orderSn",orderSn);
            return "success";
        }
    }
    

    3、前端页面修改

    <div class="main">
    
        <div class="success-wrap">
            <div class="w" id="result">
                <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.cn/payOrder?orderSn='+orderSn}">去支付a>h2>
                    div>
                div>
                <div th:if="${orderSn==null}">
                    <h1>手气不好,秒杀失败!h1>
                div>
            div>
        div>
    
    div>
    

    你可能感兴趣的:(谷粒商城,教育电商,rabbitmq,redis)