zl程序教程

您现在的位置是:首页 > 

当前栏目

谷粒商城-高级篇(秒杀功能)

功能 高级 秒杀 商城 谷粒
2023-06-13 09:11:39 时间

一、秒杀(高并发)系统关注的问题

秒杀业务:

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

限流方式:

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

秒杀流程:

​ 1、先新增秒杀场次到 DB【后台系统新增】 ​ 2、再关联商品【后台系统关联】 ​ 3、定时任务将最近三天的场次+关联商品上传到 redis 中【定时 上架 3 天内的秒杀场次+商品】

二、创建秒杀服务

添加 gateway 路由转发

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

登录后台管理界面,添加秒杀场次

**例如: **添加 8 点场,对应表 sms_seckill_session【秒杀场次表】

秒杀场次关联商品

sms_seckill_sku_relation【关联表】 字段: promotion_id【活动 id】、promotion_session_id【活动场次 id】、sku_id、排序、价格、总量、每人限购数量

  • SeckillSkuRelationServiceImpl.java
public PageUtils queryPage(Map<String, Object> params) {

    QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<>();

    String promotionSessionId = (String) params.get("promotionSessionId");

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

创建秒杀 gulimall-seckill 微服务

  • redis、openFeign、spring boot devtools、spring web、lombok
<dependencies>
    <dependency>
        <groupId>com.oy.gulimall</groupId>
        <artifactId>gulimall-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </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>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</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>
</dependencies>

三、定时任务-QUARTZ

多种实现方式:

​ Timer、线程池、mq 的延迟队列、QUARTZ【搭配 cron 表达式使用】、spring 框架的定时任务,可以整合 QUARTZ(springboot 默认定时任务框架不是 QUARTZ,如果需要使用引入即可)

最终解决方案:使用异步任务 + 定时任务来完成定时任务不阻塞的功能

1、减轻 DB 压力,定时任务查询需要上架的秒杀商品上架到 redis 中,库存信息等

2、语法:秒 分 时 日 月 周 年 (spring 不支持年,所以可以不写)

http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

Format
A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:

Field Name	Mandatory	Allowed Values		Allowed Special Characters
Seconds			YES		0-59				, - * /
Minutes			YES		0-59				, - * /
Hours			YES		0-23				, - * /
Day of month	YES		1-31				, - * ? / L W
Month			YES		1-12 or JAN-DEC		, - * /
Day of week		YES		1-7 or SUN-SAT		, - * ? / L #
Year			NO		empty, 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号,而且必须是周二然后启动这个任务;

L:(出现在日和周的位置)”,
last:最后一个
(cron="***?*3L"):每月的最后一个周二

W:Work Day:工作日
(cron="***W*?"):每个月的工作日触发
(cron="***LW*?"):每个月的最后一个工作日触发
#:第几个
(cron="***?*5#2"):每个月的 第2个周4

3、在线定时器 https://cron.qqe2.com/

4、Example

Examples
Here are some full examples:

**Expression**		**Meaning**
0 0 12 * * ?		Fire at 12pm (noon) every day
0 15 10 ? * *		Fire at 10:15am every day
0 15 10 * * ?		Fire at 10:15am every day
0 15 10 * * ? *		Fire at 10:15am every day
0 15 10 * * ? 2005	Fire at 10:15am every day during the year 2005
0 * 14 * * ?		Fire every minute starting at 2pm and ending at 2:59pm, every day
0 0/5 14 * * ?		Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day
0 0/5 14,18 * * ?	Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5 minutes starting at 6pm and ending at 6:55pm, every day
0 0-5 14 * * ?		Fire every minute starting at 2pm and ending at 2:05pm, every day
0 10,44 14 ? 3 WED	Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.
0 15 10 ? * MON-FRI	Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday
0 15 10 15 * ?		Fire at 10:15am on the 15th day of every month
0 15 10 L * ?		Fire at 10:15am on the last day of every month
0 15 10 L-2 * ?		Fire at 10:15am on the 2nd-to-last last day of every month
0 15 10 ? * 6L		Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L		Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L 2002-2005	Fire at 10:15am on every last friday of every month during the years 2002, 2003, 2004 and 2005
0 15 10 ? * 6#3		Fire at 10:15am on the third Friday of every month
0 0 12 1/5 * ?		Fire at 12pm (noon) every 5 days every month, starting on the first day of the month.
0 11 11 11 11 ?		Fire every November 11th at 11:11am.

springboot 开启定时任务 Demo

​ 解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能

1、加在类上
	@Component
	@EnableScheduling开启定时任务【spring 默认是使用自己的定时任务】
	@EnableAsync:开启异步任务【定时任务不应该阻塞,需要异步执行(不加该注解是同步的,例如方法内部sleep会阻塞)】
		解决办法:1、自己异步执行【CompletableFuture.runAsync】
				2、使用spring的 定时任务线程池scheduling.pool.size: 5
				3、使用springboot的异步定时任务@EnableAsync
					然后配置异步任务的属性
					spring:
					  task:
						execution:
							pool:
								core-size: 5
								max-size: 50
					然后给定时任务方法加上@Async【这个注解就是异步执行,不一定是定时任务】

2、加载异步定时任务方法上

@Async 异步执行的方法标注
@Scheduled(cron = "*/5 * * ? * 4")

3、编写任务

每周4的任意秒启动,5S一次执行

4、spring 注意


	1)spring周一都周天就是1-7
	2)没有年,只有6个
/**
 * 定时任务
 *      1、@EnableScheduling 开启定时任务
 *      2、@Scheduled开启一个定时任务
 *		3、自动配置类TaskSchedulingAutoConfiguration
 * 异步任务
 *      1、@EnableAsync:开启异步任务
 *      2、@Async:给希望异步执行的方法标注
 *      3、自动配置类TaskExecutionAutoConfiguration
 */

@Slf4j
@Component
// @EnableAsync
// @EnableScheduling
public class HelloScheduled {

    /**
     * 1、在Spring中表达式是6位组成,不允许第七位的年份
     * 2、在周几的的位置,1-7代表周一到周日
     * 3、定时任务不该阻塞。默认是阻塞的
     *      1)、可以让业务以异步的方式,自己提交到线程池
     *              CompletableFuture.runAsync(() -> {
     *         },execute);
     *
     *      2)、支持定时任务线程池;设置 TaskSchedulingProperties
     *        spring.task.scheduling.pool.size: 5
     *
     *      3)、让定时任务异步执行
     *          异步任务
     *
     *      解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
     *
     */
     @Async
     @Scheduled(cron = "*/5 * * ? * 4")
     public void hello() {
         log.info("hello...");
         try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

     }

}

四、秒杀架构设计

4.1 秒杀架构图

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

4.2 存储模型设计

  • 秒杀场次存储的List可以当做hash keySECKILL_CHARE_PREFIX 中获得对应的商品数据
//存储的秒杀场次对应数据
//K: SESSION_CACHE_PREFIX + startTime + "_" + endTime
//V: sessionId+"-"+skuId的List
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";

//存储的秒杀商品数据
//K: 固定值SECKILL_CHARE_PREFIX
//V: hash,k为sessionId+"-"+skuId,v为对应的商品信息SeckillSkuRedisTo
private final String SECKILL_CHARE_PREFIX = "seckill:skus";

//K: SKU_STOCK_SEMAPHORE+商品随机码
//V: 秒杀的库存件数
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码
  • 存储后的效果
  • 用来存储的 to
@Data
public class SeckillSkuRedisTo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    //以上都为SeckillSkuRelationEntity的属性

    //skuInfo
    private SkuInfoVo skuInfoVo;

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

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

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

4.3 商品上架

4.3.1 定时上架

  • 开启对定时任务的支持
@EnableAsync //开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling //开启对定时任务的支持
@Configuration
public class ScheduledConfig {
}
  • 每天凌晨三点远程调用coupon服务上架最近三天的秒杀商品
  • 由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法
//秒杀商品上架功能的锁
private final String upload_lock = "seckill:upload:lock";

/**
     * 定时任务
     * 每天三点上架最近三天的秒杀商品
     */
@Async
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
    //为避免分布式情况下多服务同时上架的情况,使用分布式锁
    RLock lock = redissonClient.getLock(upload_lock);
    try {
        lock.lock(10, TimeUnit.SECONDS);
        secKillService.uploadSeckillSkuLatest3Days();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        lock.unlock();
    }
}

@Override
public void uploadSeckillSkuLatest3Days() {
    R r = couponFeignService.getSeckillSessionsIn3Days();
    if (r.getCode() == 0) {
        List<SeckillSessionWithSkusVo> sessions = r.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
        });
        //在redis中分别保存秒杀场次信息和场次对应的秒杀商品信息
        saveSecKillSession(sessions);
        saveSecKillSku(sessions);
    }
}

4.3.2 获取最近三天的秒杀信息

  • 获取最近三天的秒杀场次信息,再通过秒杀场次 id 查询对应的商品信息
@Override
public List<SeckillSessionEntity> getSeckillSessionsIn3Days() {
    QueryWrapper<SeckillSessionEntity> queryWrapper = new QueryWrapper<SeckillSessionEntity>()
            .between("start_time", getStartTime(), getEndTime());
    List<SeckillSessionEntity> seckillSessionEntities = this.list(queryWrapper);
    List<SeckillSessionEntity> list = seckillSessionEntities.stream().map(session -> {
        List<SeckillSkuRelationEntity> skuRelationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", session.getId()));
        session.setRelations(skuRelationEntities);
        return session;
    }).collect(Collectors.toList());

    return list;
}

//当前天数的 00:00:00
private String getStartTime() {
    LocalDate now = LocalDate.now();
    LocalDateTime time = now.atTime(LocalTime.MIN);
    String format = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}

//当前天数+2 23:59:59..
private String getEndTime() {
    LocalDate now = LocalDate.now();
    LocalDateTime time = now.plusDays(2).atTime(LocalTime.MAX);
    String format = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}

4.3.3 在 redis 中保存秒杀场次信息

private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {

    if(sessions != null && sessions.size() > 0){
        sessions.stream().forEach(session -> {

            //获取当前活动的开始和结束时间的时间戳
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();

            //存入到Redis中的key
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;

            //判断Redis中是否有该信息,如果没有才进行添加
            Boolean hasKey = redisTemplate.hasKey(key);
            //缓存活动信息
            if (!hasKey) {
                //获取到活动中所有商品的skuId
                List<String> skuIds = session.getRelationSkus().stream()
                        .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key,skuIds);
            }
        });
    }
}

4.3.4 在 redis 中保存秒杀商品信息

private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {

        if(sessions != null && sessions.size() > 0){
            sessions.stream().forEach(session -> {
                //准备hash操作,绑定hash
                BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                    //生成随机码
                    String token = UUID.randomUUID().toString().replace("-", "");
                    String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                    if (!operations.hasKey(redisKey)) {

                        //缓存我们商品信息
                        SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                        Long skuId = seckillSkuVo.getSkuId();
                        //1、先查询sku的基本信息,调用远程服务
                        R info = productFeignService.info(skuId);
                        if (info.getCode() == 0) {
                            SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
                            redisTo.setSkuInfoVo(skuInfo);
                        }

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

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

                        //4、设置商品的随机码(防止恶意攻击)
                        redisTo.setRandomCode(token);

                        //序列化json格式存入Redis中
                        String seckillValue = JSON.toJSONString(redisTo);
                        operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

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

4.4 首页展示秒杀活动

  • 业务说明和逻辑分析 业务说明:展示符合当前页面时间的秒杀活动,把关联的商品都显示出来 逻辑分析
    1. 判断当前时间是否落在了活动信息 startend 之间 long time = new Date().getTime(); 然后判断这个 time 在哪个活动的 start_end 之间,因为 start_end 也是 long 类型,与 1970 的差值 查询所有场次的 key 信息:keys seckill:sessions: 【匹配所有】 java 代码:redisTemplate.keys(“seckill:sessions:_“),然后遍历 key 获得 start、end
    2. 返回商品信息的时候,要屏蔽掉随机码信息【这个业务还是需要的,在商品页,如果当前商品参与了秒杀,不返回随机码信息】
@Controller
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    @GetMapping(value = "/getCurrentSeckillSkus")
    @ResponseBody
    public R getCurrentSeckillSkus(){

        // 获取到当前可以参加秒杀商品的信息
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();

        return R.ok().setData(vos);
    }
}
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
    if(keys != null && keys.size() > 0){
        long currentTime = System.currentTimeMillis();
        for (String key : keys) {
            String replace = key.replace(SESSION_CACHE_PREFIX, "");
            String[] split = replace.split("_");
            long startTime = Long.parseLong(split[0]);
            long endTime = Long.parseLong(split[1]);
            // 当前秒杀活动处于有效期内
            if(currentTime > startTime && currentTime < endTime){
                // 取出当前秒杀活动对应商品存储的hash key
                List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                // 取出存储的商品信息并返回
                List<SeckillSkuRedisTo> collect = range.stream().map(s -> {
                    String json = ops.get(s);
                    SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
                    return redisTo;
                }).collect(Collectors.toList());
                return collect;
            }
        }
    }
    return null;
}

首页获取并拼装数据

<div class="swiper-slide">
  <!-- 动态拼装秒杀商品信息 -->
  <ul id="seckillSkuContent"></ul>
</div>

<script type="text/javascript">
  $.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {
    if (res.data.length > 0) {
      res.data.forEach(function (item) {
        $("<li onclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfoVo.skuDefaultImg + "' />"))
                .append($("<p>"+item.skuInfoVo.skuTitle+"</p>"))
                .append($("<span>" + item.seckillPrice + "</span>"))
                .append($("<s>" + item.skuInfoVo.price + "</s>"))
                .appendTo("#seckillSkuContent");
      })
    }
  })

  function toDetail(skuId) {
    location.href = "http://item.gulimall.com/" + skuId + ".html";
  }

</script>

首页展示效果

4.5 获取当前商品的秒杀信息

@ResponseBody
@GetMapping(value = "/getSeckillSkuInfo/{skuId}")
public R getSeckillSkuInfo(@PathVariable("skuId") Long skuId) {
    SeckillSkuRedisTo to = secKillService.getSeckillSkuInfo(skuId);
    return R.ok().setData(to);
}

@Override
public SeckillSkuRedisTo getSeckillSkuInfo(Long skuId) {
    BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
    //获取所有商品的hash key
    Set<String> keys = ops.keys();
    for (String key : keys) {
        //通过正则表达式匹配 数字-当前skuid的商品
        if (Pattern.matches("\\d-" + skuId,key)) {
            String v = ops.get(key);
            SeckillSkuRedisTo redisTo = JSON.parseObject(v, SeckillSkuRedisTo.class);
            //当前商品参与秒杀活动
            if (redisTo!=null){
                long current = System.currentTimeMillis();
                //当前活动在有效期,暴露商品随机码返回
                if (redisTo.getStartTime() < current && redisTo.getEndTime() > current) {
                    return redisTo;
                }
                //当前商品不再秒杀有效期,则隐藏秒杀所需的商品随机码
                redisTo.setRandomCode(null);
                return redisTo;
            }
        }
    }
    return null;
}

在查询商品详情页的接口中查询秒杀对应信息

更改商品详情页的显示效果

<li style="color: red" th:if="${item.seckillSkuVo != null}">
  <span th:if="${#dates.createNow().getTime() < item.seckillSkuVo.startTime}">
    商品将会在[[${#dates.format(new
    java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd
    HH:mm:ss")}]]进行秒杀
  </span>

  <span
    th:if="${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}"
  >
    秒杀价 [[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]
  </span>
</li>

<div class="box-btns-two" th:if="${item.seckillSkuVo == null }">
  <a
    class="addToCart"
    href="http://cart.gulimall.com/addToCart"
    th:attr="skuId=${item.info.skuId}"
  >
    加入购物车
  </a>
</div>

<div
  class="box-btns-two"
  th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}"
>
  <a
    class="seckill"
    href="#"
    th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}"
  >
    立即抢购
  </a>
</div>

五、秒杀

5.1 秒杀接口

  • 点击立即抢购,会发送请求
  • 秒杀请求会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单
@GetMapping("/kill")
public String kill(@RequestParam("killId") String killId,
                   @RequestParam("key")String key,
                   @RequestParam("num")Integer num,
                   Model model) {
    String orderSn= null;
    try {
        orderSn = secKillService.kill(killId, key, num);
        model.addAttribute("orderSn", orderSn);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "success";
}

 @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String json = ops.get(killId);
        String orderSn = null;
        if (!StringUtils.isEmpty(json)){
            SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            //1. 验证时效
            long current = System.currentTimeMillis();
            if (current >= redisTo.getStartTime() && current <= redisTo.getEndTime()) {
                //2. 验证商品和商品随机码是否对应
                String redisKey = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
                if (redisKey.equals(killId) && redisTo.getRandomCode().equals(key)) {
                    //3. 验证当前用户是否购买过
                    MemberResponseVo memberResponseVo = LoginInterceptor.loginUser.get();
                    long ttl = redisTo.getEndTime() - System.currentTimeMillis();
                    //3.1 通过在redis中使用 用户id-skuId 来占位看是否买过
                    Boolean occupy = redisTemplate.opsForValue().setIfAbsent(memberResponseVo.getId()+"-"+redisTo.getSkuId(), num.toString(), ttl, TimeUnit.MILLISECONDS);
                    //3.2 占位成功,说明该用户未秒杀过该商品,则继续
                    if (occupy){
                        //4. 校验库存和购买量是否符合要求
                        if (num <= redisTo.getSeckillLimit()) {
                            //4.1 尝试获取库存信号量
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTo.getRandomCode());
                            boolean acquire = semaphore.tryAcquire(num,100,TimeUnit.MILLISECONDS);
                            //4.2 获取库存成功
                            if (acquire) {
                                //5. 发送消息创建订单
                                //5.1 创建订单号
                                orderSn = IdWorker.getTimeId();
                                //5.2 创建秒杀订单to
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setMemberId(memberResponseVo.getId());
                                orderTo.setNum(num);
                                orderTo.setOrderSn(orderSn);
                                orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                                orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                orderTo.setSkuId(redisTo.getSkuId());
                                //5.3 发送创建订单的消息
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                            }
                        }
                    }
                }
            }
            return orderSn;
        }

5.2 创建订单

发送消息

//发送创建订单的消息
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);

创建秒杀所需队列

 /**
     * 商品秒杀队列
     * @return
     */
@Bean
public Queue orderSecKillOrrderQueue() {
    Queue queue = new Queue("order.seckill.order.queue", true, false, false);
    return queue;
}

@Bean
public Binding orderSecKillOrrderQueueBinding() {
    //String destination, DestinationType destinationType, String exchange, String routingKey,
    // 			Map<String, Object> arguments
    Binding binding = new Binding(
            "order.seckill.order.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.seckill.order",
            null);

    return binding;
}

监听队列

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

    @RabbitHandler
    public void createOrder(SeckillOrderTo orderTo, Message message, Channel channel) throws IOException {
        System.out.println("***********接收到秒杀消息");
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            orderService.createSeckillOrder(orderTo);
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            channel.basicReject(deliveryTag,true);
        }
    }
}

创建订单

@Transactional
@Override
public void createSeckillOrder(SeckillOrderTo orderTo) {
    MemberResponseVo memberResponseVo = LoginInterceptor.loginUser.get();
    //1. 创建订单
    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setOrderSn(orderTo.getOrderSn());
    orderEntity.setMemberId(orderTo.getMemberId());
    orderEntity.setMemberUsername(memberResponseVo.getUsername());
    orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    orderEntity.setCreateTime(new Date());
    orderEntity.setPayAmount(orderTo.getSeckillPrice().multiply(new BigDecimal(orderTo.getNum())));
    this.save(orderEntity);
    //2. 创建订单项
    R r = productFeignService.info(orderTo.getSkuId());
    if (r.getCode() == 0) {
        SeckillSkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SeckillSkuInfoVo>() {
        });
        OrderItemEntity orderItemEntity = new OrderItemEntity();
        orderItemEntity.setOrderSn(orderTo.getOrderSn());
        orderItemEntity.setSpuId(skuInfo.getSpuId());
        orderItemEntity.setCategoryId(skuInfo.getCatalogId());
        orderItemEntity.setSkuId(skuInfo.getSkuId());
        orderItemEntity.setSkuName(skuInfo.getSkuName());
        orderItemEntity.setSkuPic(skuInfo.getSkuDefaultImg());
        orderItemEntity.setSkuPrice(skuInfo.getPrice());
        orderItemEntity.setSkuQuantity(orderTo.getNum());
        orderItemService.save(orderItemEntity);
    }
}

页面跳转效果