秒杀服务流程

本文介绍了如何在Gulimall秒杀系统中实现后台添加秒杀商品,包括SeckillSkuRelationController的控制层操作、SeckillSkuRelationService的实现、秒杀服务的创建、定时任务调度、数据库配置、分布式锁应用和秒杀流程的业务逻辑。
摘要由CSDN通过智能技术生成

一、后台添加秒杀商品

1、SeckillSkuRelationController 控制层

/**
 * 秒杀活动商品关联
 *
 * @author lianxiaoyu
 * @email 376536577@qq.com
 * @date 2021-06-04 16:44:55
 */
@RestController
@RequestMapping("coupon/seckillskurelation")
public class SeckillSkuRelationController {

    @Autowired
    private SeckillSkuRelationService seckillSkuRelationService;

    /**
     * 列表
     * params  代表请求参数
     */
    @RequestMapping("/list")
    public R list(@RequestParam Map<String, Object> params){
        PageUtils page = seckillSkuRelationService.queryPage(params);
        return R.ok().put("page", page);
    }
}

2、实现类

根据活动场次id获取SeckillSkuRelationEntity信息

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

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        //封装条件构造器
        QueryWrapper<SeckillSkuRelationEntity> wrapper = new QueryWrapper<>();
        //请求参数获取场次id
        String promotionSessionId = (String) params.get("promotionSessionId");
        if (!StringUtils.isEmpty(promotionSessionId)){
            wrapper.eq("promotion_session_id",promotionSessionId);
        }

        IPage<SeckillSkuRelationEntity> page = this.page(
                new Query<SeckillSkuRelationEntity>().getPage(params),
                wrapper
        );

        return new PageUtils(page);
    }
}

二、创建秒杀服务

第1步:pom

	<dependencies>
        <dependency>
            <groupId>com.lian.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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--redis的 redisson 实现分布式锁-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

        <!--引入分布式session-->
        <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>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <!--引入rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

    </dependencies>

第2步:application配置

application.yaml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.56.10:3306/gulimall_seckill
    username: root
    password: root

  application:
    name: gulimall-seckill
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    #配置oss对象存储

  #配置日期格式
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss

  #关闭thymeleaf缓存
  thymeleaf:
    cache: false

  #配置redis
  redis:
    host: 192.168.56.10
    port: 6379

  #配置redis缓存类型
  cache:
    type: redis
    redis:
      time-to-live: 3600000      # 指定redis中的过期时间为1h
      use-key-prefix: true   #使用key前缀
      cache-null-values: true #缓存空值,解决缓存穿透问题,缓存穿透是 数据库和缓存中不存在的数据

#    alicloud:
#      access-key: LTAI5t6PMA6dybm1iuVL6RXQ
#      secret-key: EkkMyqaTwC3DgYWnSTuJOCPoR1kzJr
#      oss:
#        endpoint: oss-cn-beijing.aliyuncs.com

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      #配置逻辑删除,mybatisplus官网
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

server:
  port: 25000

#配置日志打印
logging:
  level:
    com.lian.gulimall: debug

application.properties

#spring.task.scheduling.pool.size=10

#session中存储的类型
spring.session.store-type=redis
#redis主机
spring.redis.host=192.168.56.10

#配置rabbitmq的虚拟主机
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.simple.acknowledge-mode=manual

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

第3步:主启动类加注解

@EnableRedisHttpSession //开启分布式事务
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

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

}

第4步: 定时任务和cron表达式

在这里插入图片描述

cron表达式

corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
在这里插入图片描述
cron表达式生成器
https://cron.qqe2.com/

第5步:SpringBoot整合定时任务和异步任务

定时任务启动,就会查到最近3天的秒杀商品

package com.lian.gulimall.seckill.schedule;

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;

/**
 * 定时任务
 * 1、Scheduling 开启定时任务
 * 2、Scheduled  开启一个定时任务
 * 3、自动配置类 TaskSchedulingAutoConfiguration
 *
 * 异步任务
 * 1、@EnableAsync 开启异步任务
 * 2、@Async 给执行异步的方法加上
 * 3、自动配置类 TaskExecutionAutoConfiguration
 *
 * 使用异步+定时任务 完成定时任务不阻塞的功能
 */
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {

    /**
     * cron = "* * * * * ?"
     * 秒 分 时 日 月 周
     * 日和周的位置,随便出现一个问号 ?
     */
    @Async
    @Scheduled(cron = "* * * * * *")
    public void hello() throws Exception {
        log.info("hello...");
        Thread.sleep(1000);
    }

}

第6步:时间日期处理

定时任务和异步任务配置类

/**
 * 定时任务和异步任务的配置类
 * @EnableScheduling 定时任务
 * @EnableAsync 异步任务
 */
@EnableScheduling
@EnableAsync
@Configuration
public class ScheduledConfig {
    
}

秒杀商品定时上架功能控制层

/**
 * 秒杀商品的定时上架
 * 每天晚上3点,上架最近3天需要秒杀的商品
 */
@Slf4j
@Service
public class SeckillSkuScheduled {

    @Autowired
    SeckillService seckillService;

    @Autowired
    RedissonClient redissonClient;

    //秒杀商品上架的锁
    private final String upload_lock = "seckill:upload:lock";

    @Async
    @Scheduled(cron = "0 * 3 * * ?")
    public void uploadSeckillLatest3Days(){
        //上架参与秒杀的商品
        log.info("上架秒杀的商品信息");
        //上锁,锁10秒钟
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        //无论成功与失败,最后都要解锁
        try {
        	//执行业务
            seckillService.uploadSeckillLatest3Days();
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

优惠服务控制层

@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {

	@Autowired
    private SeckillSessionService seckillSessionService;
    
    @GetMapping("/latest3DaySession")
    public R getLatest3DaySession(){
    	//获取最近3天参与秒杀的活动及每个活动场次的秒杀商品列表
        List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
        return R.ok().setData(sessions);
    }

}

优惠服务业务层

获取最近3天参与秒杀的活动及每个活动场次的秒杀商品列表
两个表:sms_seckill_session(活动表)、sms_seckill_sku_relation(活动场次对应秒杀商品表)

@Override
    public List<SeckillSessionEntity> getLatest3DaySession() {
        //查询出最近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) -> {
                //获取活动场次id
                Long id = session.getId();
                //根据活动场次id获取到对应的 秒杀商品列表
                List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                //关联活动场次和秒杀商品列表
                session.setRelationSkus(relationEntities);
                return session;
            }).collect(Collectors.toList());
            //返回活动场次类列表,及每场活动id对应的秒杀商品列表
            return collect;
        }
        return null;
    }

    //开始时间
    private String startTime(){
        //获取此时日期 年月日
        LocalDate now = LocalDate.now();
        //获取此时最小时间 时分秒
        LocalTime localTime = LocalTime.MIN;
        //获取此时 年月日 时分秒
        LocalDateTime startTime = LocalDateTime.of(now, localTime);
        //格式化日期模式
        String formatStartTime = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return formatStartTime;
    }

    //结束时间
    private String endTime(){
        LocalDate now = LocalDate.now();
        LocalDate localDate = now.plusDays(2);
        LocalTime localTime = LocalTime.MAX;
        LocalDateTime endTime = LocalDateTime.of(localDate, localTime);
        String formatEndTime = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return formatEndTime;
    }

}

秒杀服务远程调用优惠服务

@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    /**
     * 获取最近3天参与的秒杀活动及每场活动场次id对应的所有商品
     * sms_seckill_session 活动场次表
     * sms_seckill_sku_relation 活动场次对应秒杀商品列表
     */
    @GetMapping("/coupon/seckillsession/latest3DaySession")
    R getLatest3DaySession();

}

秒杀商品定时上架功能业务层

@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    CouponFeignService couponFeignService;  //优惠服务远程调用

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    ProductFeignService productFeignService; //商品服务远程调用

    @Autowired
    RedissonClient redissonClient;

    @Autowired
    RabbitTemplate rabbitTemplate;

    //秒杀场次
    private final String SESSIONS_CACHE_PREFIX ="seckill:sessions:";
    //秒杀商品
    private final String SKUKILL_CACHE_PREDIX ="seckill:skus";
    //信号量
    private final String SKU_STOCK_SEMPHORE ="seckill:stock:";      //后缀加商品随机码

    @Override
    public void uploadSeckillLatest3Days() {
        //扫描最近3天需要参与秒杀的活动
        //远程调用优惠服务,获取最近3天的所有活动场次及所有秒杀商品
        R session = couponFeignService.getLatest3DaySession();
        if (session.getCode() == 0){
            //得到最近3天所有活动场次及对应的所有秒杀商品   SeckillSessionsWithSkus 等同于 SeckillSessionEntity(里面包含了sms_seckill_sku_relation表)
            List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {});
            //上架秒杀商品,将秒杀商品缓存到redis里
            //1、缓存活动信息 map(key=seckill:sessions:startTime_endTime,value=List<活动场次id_商品id>)
            saveSessionInfos(sessionData);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }
    }


    //sessions:代表最近3天的所有活动场次 及 每场次对应的所有秒杀商品
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
        //stream流处理每一个活动场次 sms_seckill_session
        sessions.stream().forEach((session)->{
            //获取每个活动场次的开始和结束时间
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            //redis中保存的key=seckill:sessions:startTime_endTime
            String key = SESSIONS_CACHE_PREFIX+startTime+"_"+endTime;
            //判断redis中是否保存了此 key
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if (!hasKey){
                //获取所有参与秒杀商品的 活动场次id_商品id
                //stream流处理 sms_seckill_sku_relation 秒杀商品表
                List<String> collect = session.getRelationSkus().stream().map((item) -> {
                    // 每个秒杀商品的  活动场次id_商品id   例如:2_3
                    return item.getPromotionSessionId()+"_"+item.getSkuId().toString();
                }).collect(Collectors.toList());
                //缓存活动信息 map(key,list<活动场次id_商品id>),将商品id的集合保存到redis中
                stringRedisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }   

    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions) {
        //遍历活动场次类(包含了sms_seckill_sku_relation)
        sessions.stream().forEach((session)->{
            //准备hash操作
            BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
            //遍历秒杀商品类 sms_seckill_sku_relation = seckillSkuVo
            session.getRelationSkus().stream().forEach((seckillSkuVo -> {
                //4、生成商品的秒杀随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                //redis保存的key: 活动场次id_商品id
                Boolean hasKey = ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString());
                if (!hasKey) {
                    //缓存商品
                    //此to 封装了 秒杀商品表和商品属性信息表,SeckillSkuVo SkuInfoEntity
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    //1、sku的基本数据 pms_sku_info
                    R r = productFeignService.info(seckillSkuVo.getSkuId());
                    if (r.getCode() == 0) {
                        SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                        redisTo.setSkuInfoVo(skuInfo);
                    }
                    //2、sku的秒杀信息 sms_seckill_sku_relation
                    BeanUtils.copyProperties(seckillSkuVo, redisTo);
                    //3、设置当前商品的秒杀开始和结束时间
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());
                    //秒杀商品设置随机码
                    redisTo.setRandomCode(token);
                    //转为json字符串保存到redis中
                    String jsonString = JSON.toJSONString(redisTo);
                    //redis保存商品 map(场次id_商品id,redisTo)
                    ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString);

                    //5、引入分布式的信号量,使用库存作为分布式的信号量 seckill:stock:token
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMPHORE + token);
                    //商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            }));
        });
    }

分布式锁做信号量需要引入pom

<!--redis的 redisson 实现分布式锁-->
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.12.0</version>
</dependency>

redisson配置返回RedissonClient

redisson是专门负责做分布式锁的

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对redisson的使用都是通过 redissonClient对象
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient(){
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        //2、根据config创建出redissonClient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

redisson信号量

		<!--redis的 redisson 实现分布式锁-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

配置类

package com.lian.gulimall.seckill.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对redisson的使用都是通过 redissonClient对象
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient(){
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        //2、根据config创建出redissonClient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

redis保存结果

只要开启定时任务,redis就会查询出最近3天的活动场次及所有参与秒杀的商品
在这里插入图片描述

第7步:查询秒杀商品

控制层

@Controller
public class SeckillController {

    @Autowired
    SeckillService seckillService;

    /**
     * 返回当前时间可以参与的秒杀商品信息
     */
    @ResponseBody
    @GetMapping("/currentSeckillSkus")
    public R getCurrentSeckillSkus(){
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }
}

实现层

    //返回当前时间可以参与的秒杀商品信息
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        //1、确定当前时间属于哪个秒杀场次
        //获取当前时间
        long time = new Date().getTime();
        //获取redis中保存的所有秒杀商品场次  key:seckill:sessions:开始时间_结束时间
        Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        for (String key : keys) {
            //用空串替代前缀,留下 开始时间_结束时间
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            long startTime = Long.parseLong(s[0]);
            long endTime = Long.parseLong(s[1]);
            if (time >= startTime && time <= endTime){
                //2、获取这个秒杀场次需要的所有商品信息
                //获取在活动期间的所有key的值 1_1 、 2_1   key:seckill:sessions:1629245700000_1629302399000
                //range:取出此key中的所有值
                List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                //绑定值操作
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
                //批量获取 key 1_1 、 2_1 的值
                List<String> list = hashOps.multiGet(range);
                if (list != null){
                    List<SeckillSkuRedisTo> collect = list.stream().map((item) -> {
                        SeckillSkuRedisTo redisTo = JSON.parseObject((String) item, SeckillSkuRedisTo.class);
                        //redisTo.setRandomCode(null); //当前秒杀开始才需要随机码
                        return redisTo;
                    }).collect(Collectors.toList());
                    return collect;
                }
                break;
            }
        }
        return null;
    }

查询结果

在这里插入图片描述

第8步:秒杀页面渲染

控制层

    /**
     * 获取当前sku的秒杀信息
     * 判读当前sku的商品是否参与秒杀活动
     */
    @ResponseBody
    @GetMapping("/sku/seckill/{skuId}")
    public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId){
        SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
        return R.ok().setData(to);
    }

业务层

@Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        //找到所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
        Set<String> keys = hashOps.keys();
        if (keys.size()>0 && keys!=null){
            //场次id + 商品skuId 6_4
            String regex = "\\d_"+skuId;
            for (String key : keys) {
                //只要活动场次中有此skuId的商品,就都匹配
                if (Pattern.matches(regex, 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(null);
                    }
                    return skuRedisTo;
                }
            }
        }
        return null;
    }

商品服务调用远程服务

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {

    /**
     * 获取当前sku的秒杀信息
     */
    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);

}

商品服务业务类

商品服务的 itemController层查询skuId商品是否参与秒杀,并将其封装起来,供前台详情页调用

@Override
    public SkuItemVo item(Long skuId) throws Exception{
        //商品详情页返回数据都封装到 SkuItemVo
        SkuItemVo skuItemVo = new SkuItemVo();

        /**
         * 使用异步编排,节省时间提升效率,一起执行不阻塞等待
         * supplyAsync 有返回值,其他任务可以用
         * 开启一个异步任务,创建异步对象
         * infoFuture 任务完成后,saleAttrFuture、descFuture、baseAttrFuture 才开始执行,因为他们都需要依赖任务1的数据结果
         */
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1、sku基本信息获取,标题、副标题、价格等 pms_sku_info
            SkuInfoEntity info = baseMapper.selectById(skuId);
            skuItemVo.setInfo(info);
            //因为其他任务要用基本信息,所以我们返回基本信息
            return info;
            //executor代表要放到自己的线程池里面
        }, executor);

        //接下来接收任务的返回结果,accept只是接收上一个任务的结果,自己不返回结果
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //执行第二个任务
            //3、获取spu的销售属性组合
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        });

        //继续执行任务
        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
            //4、获取spu的介绍
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesc(spuInfoDescEntity);
        }, executor);

        //继续执行任务,任务3、4、5都依赖任务1的结果 获取spuId
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //5、获取spu的规格参数信息
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);

        /**
         * 任务2 不需要依赖任务1提供的结果数据,所以不需要等待任务1完成,直接和任务1同步执行,所以自己也开启一个异步任务
         * runAsync 代表不需要返回结果,因为也没有其他任务需要依赖任务2的数据
         */
        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            //2、sku图片信息 pms_sku_images
            List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);

        /**
         * 查询当前sku是否参加秒杀优惠
         */
        CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
            R r = seckillFeignService.getSkuSeckillInfo(skuId);
            if (r.getCode() == 0) {
                SeckillInfoVo data = r.getData(new TypeReference<SeckillInfoVo>() {
                });
                skuItemVo.setSeckillInfoVo(data);
            }
        }, executor);


        /**
         * 等待所有任务都完成,因为每一个任务都是在给 vo 中封装数据
         * get()方法就是阻塞等待所有任务都执行完
         * infoFuture 也可以不写,因为别人是依赖她的,如果别人都执行完了,那么她肯定也执行完了
         */
        CompletableFuture.allOf(infoFuture, saleAttrFuture, descFuture, baseAttrFuture, imageFuture,secKillFuture).get();


        return skuItemVo;
    }

第9步:秒杀系统设计

在这里插入图片描述

在这里插入图片描述

第10步:登录检查

整合SpringSession

pom依赖

		<!--引入分布式session-->
        <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>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

application.properties

#session中存储的类型
spring.session.store-type=redis
#redis主机
spring.redis.host=192.168.56.10

主启动类配置注解

/**
 * 定时任务和异步任务的配置类
 * @EnableScheduling 定时任务
 * @EnableAsync 异步任务
 */
@EnableScheduling
@EnableAsync
@EnableRedisHttpSession //开启分布式事务
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

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

}

springSession配置类

/**
 * 自定义cookie的序列化
 * 自定义redis的序列化
 */
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setDomainName("gulimall.com");
        defaultCookieSerializer.setCookieName("GULISESSION");
        defaultCookieSerializer.setCookiePath("/");
        return defaultCookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }

}

登录拦截器配置

@Component
public class LoginUserIntereptor implements HandlerInterceptor {

    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //对指定路径放行。秒杀请求: localhost:8080/kill
        //获取请求路径 uri:代表localhost:8080/ 斜杠后面的请求   url:代表是localhost:8080/ 全部的请求路径
        String uri = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean match = matcher.match("/kill", uri);
        //如果是秒杀请求,才需要判断是否登录,只有登录才可以进行秒杀活动
        if (match){
            MemberRespVo respVo = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
            if (respVo != null){
                loginUser.set(respVo);
                return true;
            }else {
                request.getSession().setAttribute("msg","请先进行登录");
                response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }else {
            //如果不是秒杀请求,其他的请求都放行
            return true;
        }
    }
}

拦截器配置类

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Autowired
    LoginUserIntereptor loginUserIntereptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserIntereptor).addPathPatterns("/**");
    }
}

第11步:秒杀流程业务类

在这里插入图片描述

秒杀控制层

	/**
     * 秒杀方法
     * @param killId 场次id_商品id
     * @param key    随机码
     * @param num    秒杀数量
     * @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);
        System.out.println("orderSn是:"+orderSn);
        model.addAttribute("orderSn", orderSn);
        return "success";
    }

秒杀业务层

    @Override
    public String kill(String killId, String key, Integer num) {
        //获取拦截器中登录的用户
        MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get();

        //1、获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
        //根据秒杀id获取商品信息  2_1
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(json)){
            return null;
        }else {
            SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            //todo 校验合法性
            //1、校验时间的合法性
            //获取开始和结束时间
            Long startTime = redisTo.getStartTime();
            Long endTime = redisTo.getEndTime();
            //获取活动持续时间
            Long ttl = endTime - startTime;
            //获取当前时间
            long time = new Date().getTime();
            //如果秒杀时间正确,就进行秒杀,否则返回为空
            if (time >= startTime || time <= endTime){
                //2、校验随机码和商品id
                //获取随机码
                String randomCode = redisTo.getRandomCode();
                //获取秒杀id = 场次id+商品skuid
                String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                //匹配传入的随机码和秒杀id是否正确
                if (randomCode.equals(key) && skuId.equals(killId)){
                    //3、验证购物数量是否合理
                    Integer seckillLimit = redisTo.getSeckillLimit();
                    //如果购买数量小于每人限购数量,才是合法的
                    if (num <= seckillLimit){
                        //4、验证这个人是否已经购买过此秒杀商品(防止一人多次重复秒杀商品)
                        String redisKey = memberRespVo.getId()+"_"+skuId;
                        //自动过期 等同于 setnx 设置如果不存在
                        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean){
                            //如果此 redisKey 设置key-v成功,代表从来没有买过秒杀商品
                            //获取信号量 + 后缀随机码
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMPHORE+randomCode);
                            //从信号量中取出一个

                            //请求一个信号量,减少num个
                            boolean b = semaphore.tryAcquire(num);
                            if (b) {
                                //秒杀成功
                                //快速下单,发送mq消息,10ms
                                //获取商品订单 ID
                                String timeId = IdWorker.getTimeId();
                                //封装
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                                orderTo.setSkuId(redisTo.getSkuId());
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                orderTo.setNum(num);
                                //用户id,说明是哪个用户秒杀的商品
                                orderTo.setMemberId(memberRespVo.getId());
                                //利用rabbitmq发送消息到mq(订单服务接收此消息)
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                                return timeId;
                            }
                                return null;
                        }

                    }else {
                        //说明此人已经买过秒杀商品,就不能买了
                        return null;
                    }
                }

            }else {
                return null;
            }
        }
        return null;
    }

第12步:rabbitmq

秒杀服务秒杀成功后,发送消息到mq中,订单服务监听消息,负责下单处理

导入amqp的pom

<!--引入rabbitmq-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

rabbitmq配置

#配置rabbitmq的虚拟主机
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.simple.acknowledge-mode=manual

为了保证消息是json格式,所以对rabbitmq做了消息转换配置

@Configuration
public class MyRabbitmqConfig {

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

}

订单服务里mq秒杀队列和交换机绑定配置

//声明秒杀队列
    @Bean
    public Queue orderSeckillOrderQueue(){
        Queue queue = new Queue("order.seckill.order.queue", true, false, false, null);
        return queue;
    }

    //声明秒杀队列与交换机绑定
    @Bean
    public Binding orderSeckillOrderQueueBinding(){
        return new Binding("order.seckill.order.queue",
                            Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null
                );
    }

监听秒杀控制层

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Service
public class OrderSeckillListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(SeckillOrderTo seckillOrderTo, Channel channel, Message message){
        try {
            log.info("准备创建秒杀单的详细信息");
            orderService.createSeckillOrder(seckillOrderTo);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

监听后创建秒杀订单

/**
     * 创建秒杀单的详细信息,就是保存订单 orderEntity 和 orderItemEntity
     * @param seckillOrderTo
     */
    @Override
    public void createSeckillOrder(SeckillOrderTo seckillOrderTo) {
        //1、保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(seckillOrderTo.getOrderSn());
        orderEntity.setMemberId(seckillOrderTo.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        BigDecimal price = seckillOrderTo.getSeckillPrice().multiply(new BigDecimal("" + seckillOrderTo.getNum()));
        orderEntity.setPayAmount(price);
        this.save(orderEntity);
        //2、保存订单项信息
        OrderItemEntity itemEntity = new OrderItemEntity();
        //设置sku的详细设置
        R r = productFeignService.getSpuInfoBySkuId(seckillOrderTo.getSkuId());
        itemEntity.setSkuQuantity(seckillOrderTo.getNum());
        SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {});
        itemEntity.setSpuBrand(spuInfo.getBrandId().toString());
        itemEntity.setSpuName(spuInfo.getSpuName());
        itemEntity.setCategoryId(spuInfo.getCatalogId());
        orderItemService.save(itemEntity);
    }

秒杀测试

http://localhost:25000/kill?killId=1_1&key=a7397e32a3794ac3acaa7639a2e51917&num=1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值