秒杀服务

在这里插入图片描述
去到我们的后台管理系统查询上架秒杀商品的管理页面:
然后查看它所发送的请求,我们就知道应该编写哪个服务的数据
在这里插入图片描述
配置网关路由:
在这里插入图片描述

注意:url 和id 一定要写在同一列,因为他们是同级别的。gateway 也会识别出这块的格式。
在这里插入图片描述

点击新增,就能增加秒杀活动的场次
在这里插入图片描述
在这里插入图片描述

添加了两个场次:

在这里插入图片描述

每个场次都能关联对应的商品:
在这里插入图片描述

根据点击这个"关联商品" 出现的request 请求,我们找到后台的Controller。然后修改里面的参数。我们要达到的效果是:点击不同场次,显示出不同场次中包含的商品,所以查询时应该带上这个场次的id
在这里插入图片描述

seckill_session: 秒杀场次。 seckill_sku_relation: 场次关联的商品
在这里插入图片描述

秒杀服务要单独成一个服务,因为就算这里因为高并发崩溃了,也不会影响其他服务。
秒杀流程:
在这里插入图片描述

定时任务:
定时任务的框架:Quartz
在这里插入图片描述
在这里插入图片描述

网上有很多生成cron 表达式的网站。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在如果日,月是通配,周不是,那么日,月其中一个得是 ?
如果日,月,周都是通配,则他们其中一个得是 ?

SpringBoot 整合cron 表达式定时任务与异步任务:
在原生cron 表达式中:1 = 星期天,6 = 星期5。
在Spring 的cron 表达式中:1 = 星期一,7 = 星期日

/**
 * 定时任务:
 * 1. @EnableScheduling开启定时任务
 * 2. @Component 放入容器中
 * 3. @Scheduled 开启定时任务
 * 4、自动配置类 TaskSchedulingAutoConfiguration
 *
 * 异步任务:
 * 1. @EnableAsync 开启异步任务功能
 * 2. @Async 给希望异步执行的方法上标注
 * 3. 自动配置类 TaskExecutionAutoConfiguration
 */
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {

    /**
     * 1. 在Spring 只允许6 位组成,不允许第7 位的年
     * 2. 在周几的位置,1-7 代表: 星期一到日: MON-SUN
     * 3. 定时任务不应该阻塞。默认是阻塞的(当前定时任务阻塞住,下一个定时任务也无法执行)
     *   1). 可以让业务运行以异步的方式,自己提交到到线程池(要自己配置线程池,麻烦)
     *   2). 支持定时任务线程池:设置 TaskSchedulingProperties (有些版本的spring 不能使用该功能,不用)
     *      spring.task.scheduling.pool.size=5
     *   3). 让定时任务异步执行(简单方便,使用)
     *          异步任务
     *  解决:使用异步+定时任务来完成定时任务不阻塞的功能
     */
    @Async
    @Scheduled(cron = "*/5 * * ? * 7")
    public void hello(){
        log.info("hello...");
    }
}

application.properties
在这里插入图片描述

查询最近3 天的秒杀活动场次:
是在coupon 服务中进行查询
在这里插入图片描述
数据库查询语句:
在这里插入图片描述

SeckillSessionServiceImpl:

   @Override
    public List<SeckillSessionEntity> getLatest3DaySession() {
        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));

        return list;
    }

    /**
     * 得出当前日期的最小时间:2020/11/22 00:00:00
     * @return
     */
    private String startTime(){
        LocalDate now  = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now,min);
        String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }


    /**
     * 得到最近三天的场次
     * @return
     */
    private String endTime(){
        LocalDate now  = LocalDate.now();
        //以现在是时间开始算,往后两天就是最近三天的时间
        LocalDate localDate = now.plusDays(2);
        LocalDateTime of = LocalDateTime.of(localDate,LocalTime.MAX);
        String format = of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

关于java8 中时间类的简单使用:
在这里插入图片描述
在这里插入图片描述

查出活动以后,从活动中查询其中包含的商品
为了方便,在秒杀活动的实体类中添加一个秒杀活动的商品的集合字段
在这里插入图片描述

将秒杀场次和秒杀商品都放到Redis 中的处理类的相关字段.
在这里插入图片描述

在这里插入图片描述

从coupon 服务中获取到秒杀场次和秒杀商品的数据并封装成属于coupon 服务的vo 数据:
SekillSessionsWithSkus(秒杀场次对象):
在这里插入图片描述
SeckillSkuVo(秒杀场次所包含的秒杀商品):

@Data
public class SeckillSkuVo {

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

在这里插入图片描述

因为缓存的商品对象需要用到商品的详细信息,所以又构建了一个to 对象(秒杀商品信息+商品详细信息+秒杀开始/结束信息)
SecKillSkuRedisTo:

@Data
public class SecKillSkuRedisTo {

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

    //sku 的详细信息
    private SkuInfoVo skuInfo;

    //当前商品秒杀的开始时间
    private Long startTime;

    //当前商品秒杀的结束时间
    private Long endTime;

    //当前商品的秒杀随机码
    private String randomCode;

}

将秒杀场次对象缓存到redis 中
在这里插入图片描述

将秒杀商品也缓存到redis 中

  private void saveSessionSkuInfos(List<SekillSessionsWithSkus> 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.设置redis 信息号量
                //随机码和信号量产生关联,只有知道随机码才能进行信号量的减少,进而才能抢到秒杀商品
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                //使用库存作为信号量的值
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

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

因为这里用到了redis 的操作,所以就要引入redis
在这里插入图片描述

幂等性处理:
上架过的就不要再上传了。
在这里插入图片描述

在这里插入图片描述

解决:要为定时任务加一把分布式锁。这里就解决了定时任务的幂等性。
让优先获取到锁的机器先执行定时任务
在这里插入图片描述

解决业务代码的幂等性:
为了方便对比数据,对Redis 的存储的数据修改一下结构。
当优先进来的机器进来执行完业务代码后,后面的机器进来看到redis 中已经存在了这些数据了,就不用重复上架相同的秒杀场次和秒杀商品了。
在这里插入图片描述

给定时任务加上分布式锁:
在这里插入图片描述

业务代码:


@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    RedissonClient redissonClient;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

    private final String SKUKILL_CACHE_PREFIX = "seckill:skus";

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

    @Override
    public void  uploadSeckillSkuLatest3Days() {
        //1. 扫描最近3天需要参与秒杀的活动
        R session = couponFeignService.getLatest3DaySession();
        if (session.getCode() == 0){
            //上架商品
            List<SekillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SekillSessionsWithSkus>>() {
            });
            //缓存到redis 中
            //1. 缓存活动信息
            saveSessionInfos(sessionData);

            //2. 缓存活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }
    }

    private void saveSessionInfos(List<SekillSessionsWithSkus> sessions){
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            //存到redis 的key 值
            String key = SESSIONS_CACHE_PREFIX + startTime+ "_"+endTime;

            //如果当前Redis 中已经有了这个key,那就不需要再往里面加这个数据了
            Boolean hasKey = redisTemplate.hasKey(key);
            //获取当前活动场次的所有商品id

            if (!hasKey){
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
                //缓存活动信息
                redisTemplate.opsForList().leftPushAll(key,collect);
            }
        });
    }

    private void saveSessionSkuInfos(List<SekillSessionsWithSkus> sessions){
        sessions.stream().forEach(session -> {
            //准备hash 操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);

            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //4. 商品的随机码
                //这个随机码的作用:当秒杀商品的请求暴露以后,有人可能会恶意多次发送这个请求来强商品
                //或者后台人员都知道发送这个请求来抢这个商品的话,客户就会很亏,所以抢商品时就要携带一个随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                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);

                    //将数据以JSON 字符串形式序列化,方便在Redis 中阅读
                    String jsonString = JSON.toJSONString(redisTo);
                    ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(),jsonString);

                    //如果当前这个场次的商品的库存信息已经上架就不需要上架
                    //5.设置redis 信息号量
                    //随机码和信号量产生关联,只有知道随机码才能进行信号量的减少,进而才能抢到秒杀商品
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    //使用库存作为信号量的值
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }

            });
        });
    }
}

完成功能:
1.将参与秒杀的商品放到秒杀模块部分显示
在这里插入图片描述
2.还有就是在商品详情页中显示当前当前商品如果参与秒杀活动的话,就会有显示。
在这里插入图片描述
功能1:当前页面得判断当前的时间,将当前时间具有的秒杀商品显示出来
在这里插入图片描述
SeckillServiceImpl:

    @Override
    public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
        //1. 确定当前时间属于哪个秒杀场次
        //1970 - 现在的差值数据
        long time = new Date().getTime();

        //获取到redis 中key 中带有SESSIONS_CACHE_PREFIX 的所有key.
        Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        for (String key : keys) {

            //"seckill:sessions:1606003200000_1606010400000"
            String replace = key.replace(SESSIONS_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);

                //因为我们存入秒杀商品时的数据结构就是Hash,所以取出来也要绑定Hash 结构
                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);
                        return redis;
                    }).collect(Collectors.toList());
                    return  collect;
                }
                break;
            }

        }
        return null;
    }

在商城首页:一刷新商品页面就立刻发送ajax 请求,请求秒杀商品
在这里插入图片描述
以上就完成了首页的秒杀商品信息。

完成功能2:
在商品详情页面有当前时间的秒杀价格信息,并且在页面的秒杀活动中能跳转到对应秒杀的商品详情页
因为涉及到商品详情页的数据,所以就去到商品详情页的Controller 中获取数据就行了
itemController:
在这里插入图片描述
SkuInfoServiceImpl:

    @Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo = new SkuItemVo();

        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1. sku 基本信息获取 pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);

        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //3. 获取spu 的销售属性组合
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        },executor);

        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
            //4. 获取spu 的介绍
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);
        }, executor);

        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //5. 获取spu 的规格参数信息
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySkuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);

        CompletableFuture<Void> imagesFuture = CompletableFuture.runAsync(() -> {
            //2. sku 图片信息获取 pms_sku_images
            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);


        //要等待所有的异步线程完成以后,才能return skuItemVo;
        CompletableFuture.allOf(saleAttrFuture,descFuture,imagesFuture,secKillFuture).get();

        return skuItemVo;
    }

从秒杀服务中,利用当前skuId 查询到当前商品有没有秒杀活动
在这里插入图片描述
而且为了接受从秒杀活动查询得到的数据,也创建了对应的vo 对象

@Data
public class SeckillInfoVo {

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

    //当前商品秒杀的开始时间
    private Long startTime;

    //当前商品秒杀的结束时间
    private Long endTime;

    //当前商品的秒杀随机码
    private String randomCode;
}

远程查询的秒杀服务:
SeckillController:
在这里插入图片描述

SeckillServiceImpl:

    @Override
    public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        //1. 找到所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SESSIONS_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0){
            String regx = "\\d_"+skuId;
            for (String key : keys) {
                //6_4
                if(Pattern.matches(regx,key)){
                    String json = hashOps.get(key);
                    SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);

                    //随机码:如果当前时间刚好是这件商品的秒杀时间,那么就返回它的随机码,不然就不返回
                    long current = new Date().getTime();
                    if(current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()){

                    }else{
                        skuRedisTo.setRandomCode(null);
                    }

                    return skuRedisTo;
                }
            }
        }

        return null;
    }

以上就是后端查询得到的商品秒杀数据,把这些数据封装打包给前端:
商品详情页(item.html)
效果:查询当前时间,如果时间在还没到秒杀活动,则提示秒杀活动的时间。如果时间在秒杀活动期间,则提示商品的秒杀价格。
在这里插入图片描述

在这里插入图片描述

从商品首页跳转到秒杀商品详情页:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

秒杀系统的设计:
秒杀与不秒杀的商品都是在点击购买以后进入到购物车的。所以秒杀业务不是重点,而是考虑如何设计一个高并发的系统,能承载住大量的峰值流量。
在这里插入图片描述
在这里插入图片描述
07:限流& 熔断 & 降级,这个在高并发系统中一定要考虑。

01:服务单一职责,独立部署:我们项目已经将秒杀服务独立成一个项目服务
02:秒杀链接加密:为每一个秒杀商品都加上一个随机码,用于加密。只有拿着随机码才能进行秒杀。项目中有了
03:库存预热,快速扣减:使用redis 的信号量来对秒杀数据请求进行控制。项目中有了。
04:动静分离:将静态资源直接部署到nginx上,我们项目已经有了
06:流量错峰:我们项目使用了购物车逻辑,这使得流量得以错峰。
我们开始关注05,07,08 问题。

引入spring-session,用于进行登录校验:
在这里插入图片描述
另外,因为redis 默认的客户端总是有问题,所以要更换redis 客户端
在这里插入图片描述

在配置文件中声明session 的存储方式:redis
在这里插入图片描述

配置Cookie 的作用域以及session 的序列化机制
在这里插入图片描述
开启Redis 中的session 功能:
在这里插入图片描述

以上配置好spring-session 以后才能写上拦截器,它的作用就是每次有请求进入秒杀服务后,都得先通过这个拦截器判断有没有登录,再按照拦截器的指示进行操作:
在秒杀服务中,只有/kill 请求需要判断有没有登录,其他都不需要判断登录状态,所以放行其他请求

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    //使用ThreadLocal 来进行数据共享
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    /**
     * 在目标请求到达之前,执行该方法
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match =antPathMatcher.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.store/login.html");
                return false;
            }
        }

        return true;
    }
}

要想这个拦截器生效,就得注入到Spring 的web 容器中:
在这里插入图片描述

后端之所以要拦截/kill 请求(抢购秒杀商品请求),是因为这也是一个错峰限流的操作。包括前端的检查登录状态,拦截抢购商品操作也是:
在这里插入图片描述

抢购秒杀商品第一套流程:秒杀商品其实就是优惠商品,也是直接从购物车到支付。
优点:因为有加入购物车环节,所以流量就能分散开来。它会把流量带到购物车服务中,
在这里插入图片描述
在这里插入图片描述

第二套流程:这套能应对超高并发的流量。
优点:没有操作数据库,没有做过远程调用,这是非常快的流程,非常快的创建好订单号等订单服务消费即可。
缺点:如果订单服务炸了,那么订单消息没有订单服务消费,所以秒杀商品就一直支付不成功。
在这里插入图片描述

完成合法校验+快速创建订单号 ,然后返回订单号流程:
在这里插入图片描述

接收请求:
在这里插入图片描述

SeckillServiceImpl

    @Override
    public String kill(String killId, String key, Integer num) {

        //从拦截器中得到当前用户的id
        MemberRespVo respVo = LoginUserInterceptor.loginUser.get();

        //1. 获取当前秒杀商品的详细信息
        //从Redis 中获取,因为商品详细信息在redis 中是以Hash 形式保存的,所以就要绑定一个hash 操作
        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);

            //校验合法性
            //1. 秒杀时间是否已经过了(检验时间合法性)
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            
            //当前时间
            long time = new Date().getTime();

            //当前秒杀场次剩下的时间
            long ttl = endTime - time;

            if (time >= startTime && time <= endTime){
                //能进入这里,证明时间合法
                //2. 校验随机码和商品id 是否正确
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() +"_"+redis.getSkuId();
                if (randomCode.equals(key) && killId.equals(skuId)){
                    //3. 验证购物数量是否合理
                    if(num <= redis.getSeckillLimit()){
                        //4. 验证这个人是否已经购买过。
                        //幂等性:如果秒杀成功就去Redis 中占位,userId_SessionId_skuId
                        String redisKey = respVo.getId()+"_"+skuId;
                        //设置自动过期时间: MICROSECONDS: 微秒; MILLISECONDS : 毫秒
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean){
                            //如果占位成功,说明这个人从来没买过这个秒杀商品

                            //购买商品
                            //1. 获取特定的分布式信号量(对应秒杀商品的数量)
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);

                            //MILLISECONDS : 毫秒
                            try {
                                //不用acquire() 原因: 它是阻塞方法,必须等到有信号量才会去减信号量
                                //tryAcquire() : 有则减,没有则过
                                boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);

                                //秒杀成功
                                //快速下单: 发送MQ 消息, 耗费10ms
                                //利用mybatis-plus 的工具类:IdWorker 来生成订单号
                                String timeId = IdWorker.getTimeId();
                                return timeId;

                            } catch (InterruptedException e) {
                                return null;
                            }
                        }else{
                            //如果失败,说明已经买过了
                            return null;
                        }
                    }
                }else{
                    return null;
                }
            }else{
                return null;
            }

        }
        return null;
    }

引入RabbitMQ:
在这里插入图片描述

配置RabbitMQ 的信息:
在这里插入图片描述

配置发送给RabbitMQ 的数据序列化:
在这里插入图片描述

可以不用在配置类中写@EnableRabbit。因为这个配置是用来监听RabbitMQ 的消息用的。我们现在是发消息给RabbitMQ
SeckillServiceImpl:

    @Override
    public String kill(String killId, String key, Integer num) {


        //从拦截器中得到当前用户的id
        MemberRespVo respVo = LoginUserInterceptor.loginUser.get();

        //1. 获取当前秒杀商品的详细信息
        //从Redis 中获取,因为商品详细信息在redis 中是以Hash 形式保存的,所以就要绑定一个hash 操作
        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);

            //校验合法性
            //1. 秒杀时间是否已经过了(检验时间合法性)
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            
            //当前时间
            long time = new Date().getTime();

            //当前秒杀场次剩下的时间
            long ttl = endTime - time;

            if (time >= startTime && time <= endTime){
                //能进入这里,证明时间合法
                //2. 校验随机码和商品id 是否正确
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() +"_"+redis.getSkuId();
                if (randomCode.equals(key) && killId.equals(skuId)){
                    //3. 验证购物数量是否合理
                    if(num <= redis.getSeckillLimit()){
                        //4. 验证这个人是否已经购买过。
                        //幂等性:如果秒杀成功就去Redis 中占位,userId_SessionId_skuId
                        String redisKey = respVo.getId()+"_"+skuId;
                        //设置自动过期时间: MICROSECONDS: 微秒; MILLISECONDS : 毫秒
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean){
                            //如果占位成功,说明这个人从来没买过这个秒杀商品

                            //购买商品
                            //1. 获取特定的分布式信号量(对应秒杀商品的数量)
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);

                            //MILLISECONDS : 毫秒

                            //不用acquire() 原因: 它是阻塞方法,必须等到有信号量才会去减信号量
                            //tryAcquire() : 有则减,没有则过
                            boolean b = semaphore.tryAcquire(num);

                            //只有当信号量减成功了,才能说明抢到了秒杀商品,此时才能创建订单
                            if (b){
                                //秒杀成功
                                //快速下单: 发送MQ 消息, 耗费10ms
                                //利用mybatis-plus 的工具类:IdWorker 来生成订单号
                                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;
                            }

                            return null;

                        }else{
                            //如果失败,说明已经买过了
                            return null;
                        }
                    }
                }else{
                    return null;
                }
            }else{
                return null;
            }

        }
        return null;
    }

MQ 消息队列模型:
在这里插入图片描述

按照MQ 模型来创建消息队列:
在这里插入图片描述

然后通过order 服务中,监听RabbitMQ 的队列: order.seckill.order.queue 队列,来触发保存订单和订单项数据业务:
在这里插入图片描述
OrderServiceImpl:
在这里插入图片描述

引入RabbitMQ 和Redis 就能做到削峰操作,因为Redis 的信号量(秒杀商品总数量)是有限的,只要Redis 的信号量减完了,那么之后发过来的请求都不会进行接收,然后所有秒杀成功的请求都进入消息队列,再发消息给order 服务慢慢创建订单,这就不会造成有一大批的流量一时间涌进Order 服务导致其崩溃了。

秒杀页面的完成:
前端的商品详情页:加入购物车和立即抢购按钮的逻辑判断
在这里插入图片描述

要实现的效果:
当成功抢到商品(点击立即抢购成功),来到购物车页面,提示:“恭喜你抢购成功”.
所以这是给秒杀服务单独写一个购物车页面,就把cart 服务的seccuess.html 页面拿过来就行

引入thymeleaf 依赖:
在这里插入图片描述

关闭thymeleaf 缓存:
在这里插入图片描述
因为success.html 的静态资源都是从cart 服务中的,这些资源都在Nginx 中,所以就要给所有的src, href 连接都加上cart 服务的nginx 入口路径
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
效果图:
抢购成功:
在这里插入图片描述
支付:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值