结合电商模式打造校园交易平台之秒杀服务篇(全文总共13万字,超详细)

秒杀服务

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

  • 限流方式
    • 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
    • Nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
    • 网关限流,限流的过滤器
    • 代码中使用分布式信号量
    • RabbitMq限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
  • 秒杀架构思路
    • ①项目独立部署,独立秒杀模块gulimall-seckill,以免因为秒杀服务的服务器崩溃宕机导致其他服务也不可用
    • ②使用定时任务每天三点上架最新秒杀商品,削减高峰期压力
    • ③秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口
      • 在 Redis 中保存秒杀商品信息时,为redisTo 保存一个随机码并保存在Redis中;加载商品页(Item)的时候,如果在秒杀时间内,就可携带随机码;否则不显示随机码。
    • ④ 库存预热,先从数据库中扣除一部分库存以redisson信号量的形式存储在redis中
      • 在 Redis 中保存秒杀商品信息时,商品可以秒杀的数量作为分布式信号量
    • ⑤队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单
      • 只要通过校验并信号量获取成功,就发送消息给 kedamall-order 服务
    • ⑥Nginx 做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
  • 秒杀系统设计
    在这里插入图片描述
  • 在这里插入图片描述

一、搭建秒杀服务环境

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

1、配置网关

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

2、新增场次,关联商品

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

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

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

2、搭建秒杀服务环境

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

1、创建微服务模块

在这里插入图片描述

2、导入依赖

<dependency>
  <groupId>org.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>

3、添加配置

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

4、主启动类添加注解

package com.atguigu.gulimall.seckill;

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

二、定时任务

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

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

秒杀商品定时上架流程:

在这里插入图片描述

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

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. @EnableScheduling 开启定时任务
      2. @Scheduled 开启一个定时任务
      3. 定时任务的自动配置类 TaskSchedulingAutoConfiguration
    • 异步任务:
      1. @EnableAsync 开启异步任务功能
      2. @Async :给希望异步执行的方法上标注
      3. 异步任务的自动配置类 TaskExecutionAutoConfiguration 其属性绑定在 TaskExecutionProperties
package com.atguigu.gulimall.seckill.scheduled;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
 * Data time:2022/4/16 20:20
 * StudentID:2019112118
 * Author:hgw
 * Description: 定时调度测试
 * 定时任务:
 *  1、@EnableScheduling 开启定时任务
 *  2、@Scheduled 开启一个定时任务
 *  3、自动配置类 TaskSchedulingAutoConfiguration
 * 异步任务:
 *  1、@EnableAsync 开启异步任务功能
 *  2、@Async :给希望异步执行的方法上标注
 *  3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
 */
@Slf4j
@Component
@EnableAsync //开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling //开启对定时任务的支持
public class HelloSchedule {
    /**
     * 1、spring中corn 表达式由6为组成,不允许第7位的年  Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
     * 2、在周几的位置,1-7分别代表:周一到周日(MON-SUN)
     * 3、定时任务默认是阻塞的。如何让它不阻塞?
     *      1)、可以让业务运行以异步的方式,自己提交到线程池
     *      2)、Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
     *              spring.task.scheduling.pool.size=5
     *      3)、让定时任务异步执行
     *          异步任务
     *   解决:使用异步+定时任务来完成定时任务不阻塞的功能
     */
    @Async
    @Scheduled(cron = "* * * * * 6")
    public void hello() throws InterruptedException {
        log.info("hello.....");
        Thread.sleep(3000);
    }
}

配置定时任务参数

三、商品上架

在这里插入图片描述

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

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

1、gulimall-seckill服务中编写 gulimall-coupon服务的远程调用接口,gulimall-seckill服务 的 com.atguigu.gulimall.seckill.feign 路径下的 CouponFeignService类,远程调用优惠服务是用来查询秒杀活动信息的。

package com.atguigu.gulimall.seckill.feign;

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
 * Data time:2022/4/16 21:05
 * StudentID:2019112118
 * Author:hgw
 * Description: 远程调用优惠服务接口
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
    @GetMapping("/coupon/seckillsession/lates3DaySession")
    R getLates3DaySession();
}

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

package com.atguigu.gulimall.seckill.vo;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
import java.util.List;

@Data
public class SeckillSessionsWithSkus {
    /**
     * id
     */
    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;
    private List<SeckillSkuVo> relationSkus;
}
package com.atguigu.gulimall.seckill.vo;

import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;

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

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

1、Controller 层接口编写

package com.atguigu.gulimall.coupon.controller;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.atguigu.gulimall.coupon.entity.SeckillSessionEntity;
import com.atguigu.gulimall.coupon.service.SeckillSessionService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.R;

/**
 * 秒杀活动场次
 *
 * @author leifengyang
 * @email 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);
    }

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

这里的startTime和endTime都是使用LocalDate类来加工获取的,时间就是第一天的零点到第三天的23点59分59秒。

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

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

@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
    /**
     * 缓存活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
        sessions.stream().forEach(session ->{
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            System.out.println(key);
            List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
            // 缓存活动信息
            redisTemplate.opsForList().leftPushAll(key,collect);
        });
    }

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

  1. Sku的基本信息
  2. Sku的秒杀信息

这里我们引入了Redisson分布式锁中的信号量,这样可以避免秒杀时大量请求访问数据库进行扣库存处理。因为秒杀商品数量很少,可能100万请求中只有100个能成功处理,所以我们在redis中设置一个信号量,当秒杀大并发请求进来时都先去redis中验证信号量,只有信号量(=秒杀商品数量)>0时才会去数据库执行减少库存处理,而没有获取到信号量的请求则直接进行返回,不需要执行下面的业务逻辑

/**
 * 缓存活动的关联商品信息
 * @param sessions
 */
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
    sessions.stream().forEach(session->{
        // 准备Hash操作
        BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        session.getRelationSkus().stream().forEach(seckillSkuVo -> {
            // 缓存商品
            SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
            // 1、Sku的基本数据
            R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
            if (skuInfo.getCode() == 0) {
                SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                redisTo.setSkuInfo(info);
            }
            // 2、Sku的秒杀信息
            BeanUtils.copyProperties(seckillSkuVo,  redisTo);
            // 3、设置上当前商品的秒杀时间信息
            redisTo.setStartTime(session.getStartTime().getTime());
            redisTo.setEndTime(session.getEndTime().getTime());
            // 4、商品的随机码
            String token = UUID.randomUUID().toString().replace("_", "");
            redisTo.setRandomCode(token);
            // 5、引入分布式的信号量 限流
            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
            semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
            String jsonString = JSON.toJSONString(redisTo);
            ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
        });
    });
}

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

package com.atguigu.gulimall.seckill.to;

import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;
import java.math.BigDecimal;

/**
 * Data time:2022/4/16 22:20
 * StudentID:2019112118
 * Author:hgw
 * Description: 秒杀商品的详细信息
 */
@Data
public class SecKillSkuRedisTo {
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品秒杀的随机码
     */
    private String randomCode;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private BigDecimal seckillCount;
    /**
     * 每人限购数量
     */
    private BigDecimal seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    /**
     * sku的详细信息
     */
    private SkuInfoVo skuInfo;
    /**
     * 当前商品秒杀活动的开始时间
     */
    private Long startTime;
    /**
     * 当前商品秒杀活动的结束时间
     */
    private Long endTime;
}
package com.atguigu.gulimall.seckill.vo;
//sku的基本信息
@Data
public class SkuInfoVo {
    /**
     * skuId
     */
    private Long skuId;
    /**
     * spuId
     */
    private Long spuId;
    /**
     * sku名称
     */
    private String skuName;
    /**
     * sku介绍描述
     */
    private String skuDesc;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 默认图片
     */
    private String skuDefaultImg;
    /**
     * 标题
     */
    private String skuTitle;
    /**
     * 副标题
     */
    private String skuSubtitle;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 销量
     */
    private Long saleCount;
}

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

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

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
 * Data time:2022/4/16 22:36
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {
    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);
}

第四步、幂等性保证

在这里插入图片描述

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

第一步、加锁

package com.atguigu.gulimall.seckill.scheduled;

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

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

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

@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    RedissonClient redissonClient;
    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_ CACHE_PREFIX = "seckill:skus:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    // + 商品随机码
    /**
     * 远程查询最近 3 天内秒杀的活动 以及 秒杀活动的关联的商品信息
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1、扫描最近三天数据库需要参与秒杀的活动
        R session = couponFeignService.getLates3DaySession();
        if (session.getCode() == 0) {
            // 上架商品
            List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            // 缓存到Redis
            // 1)、缓存活动信息
            saveSessionInfos(sessionData);
            // 2)、缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }
    }
    /**
     * 缓存活动信息
     *
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = redisTemplate.hasKey(key);
            if (!hasKey) {
                // 缓存活动信息
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId().toString()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }
    /**
     * 缓存活动的关联商品信息
     *
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            // 准备Hash操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                // 生成随机码
                String token = UUID.randomUUID().toString().replace("_", "");
                // 1)、缓存商品
                if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString())) {
                    SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                    // 1、Sku的基本数据
                    R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (skuInfo.getCode() == 0) {
                        SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        redisTo.setSkuInfo(info);
                    }
                    // 2、Sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo, redisTo);
                    // 3、设置上当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());
                    // 4、商品的随机码
                    redisTo.setRandomCode(token);
                    String jsonString = JSON.toJSONString(redisTo);
             ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString);
                    // 如果当前这个场次的商品的库存信息已经上架就不需要上架
                    // 5、引入分布式的信号量 限流
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                }
            });
        });
    }
}

每个秒杀商品在redis中存储时都与其场次信息拼串后进行存储,这样就可以防止如果多个秒杀场次有相同商品时不会再次上架的问题(不是幂等性需要处理的,因为场次不同,相同场次多次上架才需要幂等性处理)。

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

获取当前的秒杀商品

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

@RestController
public class SeckillController {
    @Autowired
    SeckillService seckillService;
    /**
     * 返回当前时间可以参与秒杀的商品信息
     * @return
     */
    @GetMapping("/currentSeckillSkus")
    public R getCurrentSeckillSkus(){
        List<SecKillSkuRedisTo> vos =  seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }
}
  1. Service 层实现类方法编写gulimall-seckill 服务的 com/atguigu/gulimall/seckill/service/impl 路径下的 SeckillServiceImpl.java
/**
 * 获取当前参与秒杀的商品
 * @return
 */
@Override
public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
    // 1、确定当前时间属于哪个秒杀场次
    long time = new Date().getTime();
    Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
    for (String key : keys) {
        // seckill:sessions:1650153600000_1650160800000
        String replace = key.replace(SESSION_CACHE_PREFIX, "");
        String[] s = replace.split("_");
        long start = Long.parseLong(s[0]);
        long end = Long.parseLong(s[1]);
        if (time>= start && time<=end) {
            // 2、获取指定秒杀场次需要的所有商品信息
            List<String> range = redisTemplate.opsForList().range(key, -100, 100);
            BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            List<String> list = hashOps.multiGet(range);
            if (list!=null) {
                List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
                    SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
                    redis.setRandomCode(null);  // 当前秒杀开始了需要随机码
                    return redis;
                }).collect(Collectors.toList());
                return collect;
            }
            break;
        }
    }
    return null;
}

首页获取并拼装数据

第一步、环境配置

1、配置网关

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

2、配置域名 vim /etc/hosts

# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
127.0.0.1 order.gulimall.cn
127.0.0.1 member.gulimall.cn
127.0.0.1 seckill.gulimall.cn
# Gulimall Host End
第二步、页面修改

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

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

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

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

查询某个商品时,把这个商品id发到后端查一下这个商品是否是秒杀商品(skuid和redis中存的秒杀商品id进行正则匹配,因为redis中存的id还有场次信息),如果是秒杀商品则需要多返回一些数据并且展示当前商品正在秒杀。

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

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

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

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

package com.atguigu.gulimall.product.feign;

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}

封装接收VO:

package com.atguigu.gulimall.product.vo;
/**
 * Data time:2022/4/5 10:34
 * StudentID:2019112118
 * Author:hgw
 * Description: 商品详情
 */
@Data
public class SkuItemVo {
    // 1、sku基本信息    pms_sku_info
    SkuInfoEntity info;

    // 是否有货
    boolean hasStock = true;

    // 2、sku的图片信息   pms_sku_images
    List<SkuImagesEntity> images;

    // 3、获取 spu 的销售属性组合
    List<SkuItemSaleAttrsVo> saleAttr;

    // 4、获取 spu 的介绍 pms_spu_info_desc
    SpuInfoDescEntity desp;

    // 5、获取 spu 的规格参数信息
    List<SpuItemAttrGroupVo> groupAttrs;

    // 6、当前商品的秒杀优惠信息
    SeckillInfoVo seckillInfo;
}
package com.atguigu.gulimall.product.vo;

@Data
public class SeckillInfoVo {
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品秒杀的随机码
     */
    private String randomCode;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private BigDecimal seckillCount;
    /**
     * 每人限购数量
     */
    private BigDecimal seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    /**
     * 当前商品秒杀活动的开始时间
     */
    private Long startTime;
    /**
     * 当前商品秒杀活动的结束时间
     */
    private Long endTime;
}

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

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

package com.atguigu.gulimall.seckill.controller;

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

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

/**
 * 获取某个商品的秒杀预告信息
 * @param skuId
 * @return
 */
@Override
public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
    // 1、找到所有需要参与秒杀的key
    BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);

    Set<String> keys = hashOps.keys();
    if (keys != null && keys.size()>0) {
        String regx = "\\d_"+skuId;
        for (String key : keys) {
            if (Pattern.matches(regx,key)) {
                String json = hashOps.get(key);
                SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
                long current = new Date().getTime();
                Long startTime = skuRedisTo.getStartTime();
                Long endTime = skuRedisTo.getEndTime();
                if (current>=startTime && current<=endTime){
                    // 在秒杀活动时
                } else {
                    // 不在秒杀活动时不应该传递随机码
                    skuRedisTo.setRandomCode("");
                }
                return skuRedisTo;
            }
        }
    }
    return null;
}

商品详情页前端渲染

修改 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、修改 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>
  • 前端要考虑秒杀系统设计的限流思想
  • 在进行立即抢购之前,前端页面先进行判断是否登录,因为之前我们登录之后会在session中存取用户信息,所以前端只需要判断session中是否有用户信息即可知道登没登录。
$("#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、引入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>

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

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

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

package com.atguigu.gulimall.seckill;

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

4、编写SpringSession的配置

package com.atguigu.gulimall.seckill.config;

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

5、编写用户登录拦截器 并 配置到Spring容器中,因为这个是秒杀服务的拦截器,这里我们只想让秒杀服务需要登录,所以只对kill路径下的请求进行拦截,让其登录后才能秒杀,实现后端限流。

package com.atguigu.gulimall.seckill.interceptoe;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean match = matcher.match("/kill", uri);
        if (match){
            MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
            if (attribute!=null){
                loginUser.set(attribute);
                return true;
            } else {
                // 没登录就去登录
                request.getSession().setAttribute("msg", "请先进行登录");
                response.sendRedirect("http://auth.gulimall.cn/login.html");
                return false;
            }
        }
        return true;
    }
}
  • 拦截器配置到spring中,否则拦截器不生效。
  • 添加addInterceptors表示当前项目的所有请求都要经过这个拦截请求

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

package com.atguigu.gulimall.seckill.config;

@Configuration
public class SeckillWebConfiguration implements WebMvcConfigurer {
    @Autowired
    LoginUserInterceptor interceptor;

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

七、秒杀

秒杀流程:

在这里插入图片描述

点击参与秒杀的商品的商品详情页的立即抢购时,会发送秒杀请求,秒杀请求会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单。这里秒杀处理是很快的,因为我们只需要去redis中获取一次数据,然后进行如上判断即可,下单锁库存等操作可以让MQ去慢慢处理。

秒杀过程中,需要保证幂等性,每个用户只能秒杀一次,其他的返回结构都一致。

在这里插入图片描述

秒杀请求处理

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);
        //只要处理秒杀请求之后能获取到订单号就证明秒杀成功了即orderSn不为null
        return R.ok().setData(orderSn);
    }
}
	@Override
    public String kill(String killId, String key, Integer num) {
        //绑定哈希操作
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
		//1.获取秒杀商品的详细信息
        String s = hashOps.get(killId);
        if(StringUtils.isEmpty(s)){
        	//非空判断
            return null;
        }else {
            SecKillSkuRedisTo redisTo = JSON.parseObject(s, SecKillSkuRedisTo.class);
            //2.合法性校验
            //2.1秒杀时间校验
            Long startTime = redisTo.getStartTime();
            Long endTime = redisTo.getEndTime();
            long time = new Date().getTime();
            long ttl = endTime - startTime;
            if(time<startTime && time>endTime){
                return null;
            }
            //2.2随机码校验和商品ID
            String randomCode = redisTo.getRandomCode();
            String id = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
            if(!key.equals(randomCode) || !killId.equals(id)){
                return null;
            }
            //2.3购买数量是否超过限购数量
            if(num>redisTo.getSeckillLimit().intValue())return null;
            //2.4验证这个人是否购买过了(幂等性)==》只要秒杀成功就去redis占位;数据格式 userId_sessionId_skuId
            MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
            String occupyKey = memberResponseVo.getId() + "_" + id;
            Boolean absent = redisTemplate.opsForValue().setIfAbsent(occupyKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
            if(!absent){
                //占位失败:已买过
                return null;
            }
            //TODO 3.开始秒杀!!!!
            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
            boolean acquire = semaphore.tryAcquire(num);
            //只要信号量获取成功
            if(acquire){
                // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                String timeId = IdWorker.getTimeId();
                SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
                seckillOrderTo.setOrderSn(timeId);
                seckillOrderTo.setMemberId(memberResponseVo.getId());
                seckillOrderTo.setNum(num);
                seckillOrderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                seckillOrderTo.setSkuId(redisTo.getSkuId());
                seckillOrderTo.setSeckillPrice(redisTo.getSeckillPrice());
                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", seckillOrderTo);
                return timeId;
            }
        }
        return null;
    }

引入rabbitMQ消息队列

使用消息队列进行流量削峰

在这里插入图片描述

1、引入依赖

<!--RabbitMq-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、编写配置

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

3、编写配置类

package com.atguigu.gulimall.seckill.config;

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

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

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

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

创建订单

Service层实现类的方法编写

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

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

消息传递的TO

package com.atguigu.common.to.mq;

import lombok.Data;
import java.math.BigDecimal;
/**
 * Data time:2022/4/17 17:50
 * StudentID:2019112118
 * Author:hgw
 * Description: 秒杀订单
 */
@Data
public class SeckillOrderTo {
    /**
     * 订单号
     */
    private String orderSn;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀件数
     */
    private Integer num;
    /**
     * 会员id
     */
    private Long memberId;
}

监听队列,进行订单处理

package com.atguigu.gulimall.order.listener;

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
        try {
            log.info("准备创建秒杀单的详细信息:"+seckillOrder);
            orderService.createSeckillOrder(seckillOrder);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

}

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

/**
 * 秒杀单的详细信息创建
 * @param seckillOrder
 */
@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
    //TODO 保存订单信息
    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setOrderSn(seckillOrder.getOrderSn());
    orderEntity.setMemberId(seckillOrder.getMemberId());

    orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
    orderEntity.setPayAmount(multiply);
    this.save(orderEntity);
    // TODO 保存订单项信息
    OrderItemEntity orderItemEntity = new OrderItemEntity();
    orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
    orderItemEntity.setRealAmount(multiply);
    orderItemEntity.setSkuQuantity(seckillOrder.getNum());
    // TODO 获取当前SKU的详细信息进行设置
    orderItemService.save(orderItemEntity);
}

秒杀页面

1、引入thymeleaf

  1. 导入依赖

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

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

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

package com.atguigu.gulimall.seckill.controller;

@Controller
public class SeckillController {
    @Autowired
    SeckillService seckillService;
    /**
     * 返回当前时间可以参与秒杀的商品信息
     * @return
     */
    @ResponseBody
    @GetMapping("/currentSeckillSkus")
    public R getCurrentSeckillSkus(){
        List<SecKillSkuRedisTo> vos =  seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }
    /**
     * 获取某个商品的秒杀预告信息
     * @param skuId
     * @return
     */
    @ResponseBody
    @GetMapping("/sku/seckill/{skuId}")
    public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {

        SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
        return R.ok().setData(to);
    }
    /**
     * 秒杀请求
     * @return
     */
    @GetMapping("/kill")
    public String secKill(@RequestParam("killId") String killId,
                     @RequestParam("key") String key,
                     @RequestParam("num") Integer num,
                          Model model) {
        String orderSn = seckillService.kill(killId,key,num);
        model.addAttribute("orderSn",orderSn);
        return "success";
    }
}

3、前端页面修改

<div class="main">
    <div class="success-wrap">
        <div class="w" id="result">
            <div class="m succeed-box">
                <div th:if="${orderSn!=null}" class="mc success-cont">
                    <h1>恭喜,秒杀成功!订单号: [[${orderSn}]]</h1>
                    <h2>正在准备订单数据,10s以后自动跳转支付 <a style="color: red" th:href="${'http://order.gulimall.cn/payOrder?orderSn='+orderSn}">去支付</a></h2>
                </div>
            </div>
            <div th:if="${orderSn==null}">
                <h1>手气不好,秒杀失败!</h1>
            </div>
        </div>
    </div>
</div>


感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值