当前位置:   article > 正文

谷粒商城-分布式高级篇[商城业务-秒杀服务]_谷粒商城中信号量在哪里使用

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


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

  • 限流方式

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

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

一、搭建秒杀服务环境

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


1、配置网关

在这里插入图片描述

        - id: coupon_route
          uri: lb://gulimall-coupon
          predicates:
            - Path=/api/coupon/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在这里插入图片描述


2、搭建秒杀服务环境


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

1、创建微服务模块

在这里插入图片描述

2、导入依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.12.0</version>
</dependency>
<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <exclusions>
        <exclusion>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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
  • 1
  • 2
  • 3
  • 4

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

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13


二、定时任务

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

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

在这里插入图片描述

使用 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秒触发任务
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

配置定时任务参数



三、商品上架


在这里插入图片描述

第一步、远程查询最近 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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

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 leifengyang@gmail.com
 * @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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

第二步、在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);
        });
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

第三步、在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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

第四步、幂等性保证


在这里插入图片描述

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

第二步、判断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());
                }

            });
        });
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107


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

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

4.2、首页获取并拼装数据


第一步、环境配置

1、配置网关

- id: gulimall_seckill_route
  uri: lb://gulimall-seckill
  predicates:
    - Host=seckill.gulimall.cn
  • 1
  • 2
  • 3
  • 4

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
第二步、页面修改

修改 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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
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) {
      $("<li οnclick='to_href("+ item.skuId +")'></li>")
              .append($("<img style='width: 130px; height: 130px;' src='"+ item.skuInfo.skuDefaultImg+"'/>"))
              .append($("<p>"+ item.skuInfo.skuTitle +"</p>"))
              .append($("<span>"+ item.seckillPrice +"</span>"))
              .append($("<s>"+ item.skuInfo.price +"</s>"))
              .appendTo("#seckillSkuContent");
    });
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述



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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

第一步、在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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

封装接收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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

第二步、在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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

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.00</span>
        </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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述



六、登录检查

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 前端要考虑秒杀系统设计的限流思想
  • 在进行立即抢购之前,前端先进行判断是否登录
$("#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;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

6.2、秒杀服务登录检查


1、引入SpringSession依赖的Redis

<!-- 整合SpringSession完成Session共享问题-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

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

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 把拦截器配置到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("/**");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13


七、秒杀

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

7.2、引入rabbitMQ依赖


使用队列进行削峰

在这里插入图片描述

1、引入依赖

<!--RabbitMq-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

2、编写配置

#RabbitMq的配置
spring.rabbitmq.host=124.222.223.222
spring.rabbitmq.virtual-host=/
  • 1
  • 2
  • 3

3、编写配置类

package com.atguigu.gulimall.seckill.config;

@Configuration
public class MyRabbitConfig {

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

消息传递的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

7.5、秒杀页面


1、引入thymeleaf

  1. 导入依赖

    <!--模板引擎 thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
  2. 在配置里关闭thymeleaf缓存

    #关闭缓存
    spring.thymeleaf.cache=false
    
    • 1
    • 2

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";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/872868
推荐阅读
相关标签
  

闽ICP备14008679号