【谷粒商城之秒杀服务】

news2024/11/24 19:42:12

本笔记内容为尚硅谷谷粒商城秒杀服务部分

目录

一、秒杀业务的介绍

秒杀设计 

秒杀流程

二、搭建秒杀服务环境 

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

2、搭建秒杀服务环境

二、定时任务

1、cron 表达式

2、测试

三、商品上架

1、远程查询秒杀的活动以及关联的商品信息

2、在Redis中保存秒杀场次信息 

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

4、幂等性保证

加锁 

判断Redis中是否已上架

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

1、获取当前的秒杀商品

2、首页获取并拼装数据

五、获取处于秒杀的商品信息

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

2、商品详情页前端渲染

六、登录检查

1、商品详情页修改

2、秒杀服务登录检查

七、秒杀

1、模式一

2、模式二

1.秒杀请求的处理

2.引入rabbitMQ

3.创建订单

4.监听队列,秒杀消息消费

5.秒杀页面

八、秒杀设计问题的解决方法


一、秒杀业务的介绍


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

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

秒杀设计 

秒杀流程

 

二、搭建秒杀服务环境 


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

1.网关配置

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

2.新增场次,关联商品

SeckillSkuRelationServiceImpl.java

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.添加配置

application.properties

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

4.主启动类添加注解

package com.atguigu.gulimall.seckill;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

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

}

二、定时任务


1、cron 表达式

在线Cron表达式生成器 (qqe2.com)

1.cron表达式语法

秒 分 时 日 月 周 年(Spring不支持)

2.cron 表达式特殊字符

,:枚举;
(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.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点,4点 

 0 0 12 ? * WED 表示每个星期三中午12点
 0 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2: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 * * * ?" 每小时的10分30秒触发任务
"30 10 1 * * ?" 每天1点10分30秒触发任务
"30 10 1 20 * ?" 每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *" 每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011" 2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011" 2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011" 2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?" 每15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 15到45秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?" 每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10点15分0秒触发任务

2、测试

问题:定时任务默认是阻塞的。如何让它不阻塞?
解决:使用异步+定时任务来完成定时任务不阻塞的功能
定时任务:

  • @EnableScheduling 开启定时任务
  • @Scheduled 开启一个定时任务
  • 自动配置类 TaskSchedulingAutoConfiguration

异步任务:

  • @EnableAsync 开启异步任务功能
  • @Async :给我希望异步执行的方法上标注
  • 自动配置类 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;

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

配置异步任务线程池:

spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50

定时任务开启后其实也是有线程池的,通过更改配置修改线程池大小,这样也可以解决阻塞问题

#默认为1,就会阻塞
spring.task.scheduling.pool.size: 2  

三、商品上架


1、远程查询秒杀的活动以及关联的商品信息

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

1.秒杀服务中编写优惠服务的远程调用接口

CouponFeignService.java

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;

/**
 * Description: 远程调用优惠服务接口
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @GetMapping("/coupon/seckillsession/lates3DaySession")
    R getLates3DaySession();
}

2.秒杀服务中编写优惠服务获取的数据的Vo

SeckillSessionsWithSkus.java

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

SeckillSkuVo.java 

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

优惠服务编写扫描数据库最近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;

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

2、在Redis中保存秒杀场次信息 

SeckillServiceImpl.java

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

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

saveSessionSkuInfo.java

/**
 * 缓存活动的关联商品信息
 * @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);
        });
    });
}

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

SecKillSkuRedisTo.java

package com.atguigu.gulimall.seckill.to;

import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;

import java.math.BigDecimal;

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

SkuInfoVo.java 

package com.atguigu.gulimall.seckill.vo;

@Data
public class SkuInfoVo {
    /**
     * skuId
     */
    private Long skuId;
    /**
     * spuId
     */
    private Long spuId;
    /**
     * sku名称
     */
    private String skuName;
    /**
     * sku介绍描述
     */
    private String skuDesc;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 默认图片
     */
    private String skuDefaultImg;
    /**
     * 标题
     */
    private String skuTitle;
    /**
     * 副标题
     */
    private String skuSubtitle;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 销量
     */
    private Long saleCount;
}

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

1.在秒杀服务中编写远程调用产品服务中的 查询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;

@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);

}

4、幂等性保证

加上分布式锁

  • 保证在分布式的情况下,锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态

代码逻辑编写

  • 当查询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());
                }

            });
        });
    }

}

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


1、获取当前的秒杀商品

1.Controller层接口

package com.atguigu.gulimall.seckill.controller;

@RestController
public class SeckillController {

    @Autowired
    SeckillService seckillService;

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

2.Service 层实现类方法编写

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

2、首页获取并拼装数据

1、环境配置

1.配置网关

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

2.配置域名 vim /etc/hosts

127.0.0.1 seckill.gulimall.cn

2、页面修改 

修改 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");
    });
  }

五、获取处于秒杀的商品信息


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

修改商品服务的SkuInfoServiceImpl类的 item 方法

SkuInfoServiceImpl.java

@Override
public SkuItemVo item(Long skuId) {
    SkuItemVo skuItemVo = new SkuItemVo();

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

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

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

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

    // 5、sku的图片信息   pms_sku_images
    CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
        List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);
    }, executor);

    // 6、查询当前sku是否参与秒杀优惠
    CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
        R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
        if (seckillInfo.getCode() == 0) {
            SeckillInfoVo seckillInfoVo = seckillInfo.getData(new TypeReference<SeckillInfoVo>() {
            });
            skuItemVo.setSeckillInfo(seckillInfoVo);
        }
    }, executor);

    // 等待所有任务都完成
    CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).join();

    return skuItemVo;
}

1.在商品服务中编写远程调用秒杀服务的feign接口

package com.atguigu.gulimall.product.feign;

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

封装接收VO

SkuItemVo.java

package com.atguigu.gulimall.product.vo;

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

SeckillInfoVo.java 

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

2.在秒杀服务中编写获取某个商品的秒杀预告信息接口

SeckillController.java

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

SeckillServiceImpl.java

/**
 * 获取某个商品的秒杀预告信息
 * @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;
}

2、商品详情页前端渲染

修改 item.html 页面

<div class="box-summary clear">
    <ul>
        <li>京东价</li>
        <li>
            <span>¥</span>
            <span th:text="${#numbers.formatDecimal(item.info.price,0,2)}">4499.00</span>
        </li>
        <li style="color: red" th:if="${item.seckillInfo!=null}">
            <span th:if="${#dates.createNow().getTime() < item.seckillInfo.startTime}">
                商品将会在 [[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]] 进行秒杀
            </span>
            <span th:if="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
                秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
            </span>
        </li>
        <li>
            <a href="/static/item/">
                预约说明
            </a>
        </li>
    </ul>
</div>

六、登录检查


1、商品详情页修改

  • 在秒杀活动时,商品显示:立刻抢购
    • 登录才跳转至 秒杀服务
    • 未登录不跳转
  • 在秒杀活动外,商品显示:加入购物车

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

2、秒杀服务登录检查

1.引入SpringSession依赖的Redis

<!-- 整合SpringSession完成Session共享问题-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

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;

/**
 * Description: 自定义Session 配置
 */
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.cn");
        cookieSerializer.setCookieName("GULISESSION");

        return cookieSerializer;
    }

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

5.编写用户登录拦截器并配置到Spring容器中

package com.atguigu.gulimall.seckill.interceptoe;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

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

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

        String uri = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean match = matcher.match("/kill", uri);
        if (match){
            MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
            if (attribute!=null){
                loginUser.set(attribute);
                return true;
            } else {
                // 没登录就去登录
                request.getSession().setAttribute("msg", "请先进行登录");
                response.sendRedirect("http://auth.gulimall.cn/login.html");
                return false;
            }
        }
        return true;
    }
}

注意:把拦截器配置到spring中,否则拦截器不生效。添加addInterceptors表示当前项目的所有请求都要经过这个拦截请求。

添加SeckillWebConfig

package com.atguigu.gulimall.seckill.config;

@Configuration
public class SeckillWebConfiguration implements WebMvcConfigurer {

    @Autowired
    LoginUserInterceptor interceptor;

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

七、秒杀


1、模式一

加入购物车秒杀-----不推荐用

优点: 加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好

缺点: 秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单

2、模式二

独立秒杀业务来处理----我们使用此秒杀模式

优点: 从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息

缺点: 如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款

解决方案: 不使用订单服务处理秒杀消息,需要一套独立的业务来处理

 

1.秒杀请求的处理

Controller层接口的编写

package com.atguigu.gulimall.seckill.controller;

@RestController
public class SeckillController {

    @Autowired
    SeckillService seckillService;

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

2.引入rabbitMQ

①引入依赖

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

②编写配置

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

③编写配置类

MyRabbitConfig.java

package com.atguigu.gulimall.seckill.config;

@Configuration
public class MyRabbitConfig {

    @Autowired
    RabbitTemplate rabbitTemplate;

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

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

/**
 * 商品秒杀队列
 * 作用:削峰,创建订单
 */
@Bean
public Queue orderSecKillOrderQueue() {
    Queue queue = new Queue("order.seckill.order.queue", true, false, false);
    return queue;
}

@Bean
public Binding orderSecKillOrderQueueBinding() {
    //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;
}

3.创建订单

秒杀代码SeckillServiceImpl.java

/**
 * 秒杀处理,发送消息给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;

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

4.监听队列,秒杀消息消费

package com.atguigu.gulimall.order.listener;

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

    @Autowired
    OrderService orderService;

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

}

创建秒杀订单createSeckillOrder.java

/**
 * 创建秒杀单
 * @param orderTo
 */
@Override
public void createSeckillOrder(SeckillOrderTo orderTo) {

    //TODO 保存订单信息
    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setOrderSn(orderTo.getOrderSn());
    orderEntity.setMemberId(orderTo.getMemberId());
    orderEntity.setCreateTime(new Date());
    BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
    orderEntity.setPayAmount(totalPrice);
    orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());

    //保存订单
    this.save(orderEntity);

    //保存订单项信息
    OrderItemEntity orderItem = new OrderItemEntity();
    orderItem.setOrderSn(orderTo.getOrderSn());
    orderItem.setRealAmount(totalPrice);

    orderItem.setSkuQuantity(orderTo.getNum());

    //保存商品的spu信息
    R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
    SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
    });
    orderItem.setSpuId(spuInfoData.getId());
    orderItem.setSpuName(spuInfoData.getSpuName());
    orderItem.setSpuBrand(spuInfoData.getBrandName());
    orderItem.setCategoryId(spuInfoData.getCatalogId());

    //保存订单项数据
    orderItemService.save(orderItem);
}

5.秒杀页面

①导入thymeleaf依赖

<!--模板引擎 thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

②关闭thymeleaf缓存

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

③修改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";
    }
}

④前端页面修改

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

八、秒杀设计问题的解决方法


服务单一职责+独立部署:新增秒杀服务

秒杀链接加密:请求需要随机码,在秒杀开始时随机码才会放在商品信息中

库存预热+快速扣减:库存放入redis中,使用分布式信号量扣减+限流

动静分离:Nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。

恶意请求拦截:使用网关拦截,一些不带令牌的请求循环发送,本系统做了登录拦截器

流量错峰:

  • 1、输入验证码需要时间,将流量错开了【速度有快有慢】
  • 2、加入购物车,然后再结算【速度有快有慢】--当前使用

限流&熔断&降级:spring alibaba sentinel

队列削峰:秒杀服务将创建订单的请求存入mq,订单服务监听mq。

最后提一句:高并发有三宝,缓存异步队排好 

结束!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559882.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

「一本通 1.2 练习 3」灯泡

题目描述 相比 w i l d l e o p a r d wildleopard wildleopard 的家&#xff0c;他的弟弟 m i l d l e o p a r d mildleopard mildleopard 比较穷。他的房子是狭窄的而且在他的房间里面仅有一个灯泡。每天晚上&#xff0c;他徘徊在自己狭小的房子里&#xff0c;思考如何赚…

探索Kotlin 1.8.20新特性

探索Kotlin 1.8.20新特性 Kotlin 1.8.20已经发布&#xff0c;我们将探索一些新功能和改进。 我们将仅涵盖新的语言功能和标准库更新。请参考参考资料部分以了解此版本的完整详情。 语言更新 Enum 类entries函数 为什么需要这个函数&#xff1f; values() - 返回数组&#x…

电商数仓建模案例

目录 一、数据仓库分层规划二、数据仓库构建流程2.1 数据调研2.2 明确数据域2.3 构建业务总线矩阵2.4 明确统计指标2.4 维度模型设计2.5 汇总模型设计 三、数仓建模之ODS层3.1 日志表3.1.1 日志表表结构 3.2 业务表3.2.1 业务表表结构 四、数仓开发之DIM层4.1 商品维度表4.2 优…

The Sandbox 与 Biconomy 合作,达成交易里程碑

双方将继续合作利用 Polygon Labs 来降低用户的成本和 The Sandbox 的碳足迹。 我们团队继续推动更好的整体项目生态系统的可持续性&#xff0c;同时沿途的里程碑向我们展示这些进展产生的影响。我们很高兴地宣布&#xff0c;自从在 Biconomy.io 的帮助下首次在 Polygon 上部署…

网络地址转换:DNAT和SNAT有啥区别?分别用于什么场景?

什么是网络地址转换&#xff08;NAT&#xff09;&#xff1f; 网络地址转换&#xff08;Network Address Translation&#xff0c;简称NAT&#xff09;是一种常见的网络技术&#xff0c;用于在私有网络&#xff08;如家庭或办公室网络&#xff09;与公共网络&#xff08;如互联…

POI Excel 基础(一)

POI 5.2.3 官网 github POI-HSSF and POI-XSSF/SXSSF 用于访问Microsoft Excel格式文件的Java API HSSF&#xff1a;是Horrible SpreadSheet Format的缩写&#xff0c;也即“可怕的电子表格格式” 是操作Excel97-2003版本&#xff0c;扩展名为.xls。XSSF&#xff1a; 是操作…

【JavaSE】Java基础语法(三)

文章目录 1. &#x1f37c;流程控制语句1.1 流程控制语句分类1.2 分支结构之if语句1.2.1 if语句格式3案例 2. &#x1f37c;switch语句2.1 分支语句switch语句2.2 switch案例-减肥计划2.3 switch语句case穿透 3. &#x1f37c;for 循环3.1 循环语句-for循环3.2 for循环案例-求1…

项目集管理—项目集相关方参与

项目集相关方参与是识别和分析相关方需求、管理期望和沟通&#xff0c;以促进相关方支持的绩效领域。 本章内容包括&#xff1a; 1 项目集相关方识别2 项目集相关方分析3 项目集相关方参与规划4 项目集相关方参与5 项目集相关方沟通 相关方是指能够影响项目、项目集或项目组合…

Java 实现实时监听MySQL数据库变更MySQLBinListener

目录 1、导出需要的类和接口 2、 定义 MySQLBinlogListener类 3、私有方法&#xff0c;启动重连定时器 4、完整代码 编写一个MySQL数据库实时变更的监听器。 为什么要编写这个一个监听器&#xff1a;为了实时监测和响应MySQL数据库中的变更事件 实时数据同步&#xff1a;通…

运营-12.转化

用户注册、登录、浏览、发布、购买等行为都可以视为转化&#xff1b;而对于电商产品而言&#xff0c;狭义的转化即为获取 收入&#xff0c;就是要用户买单、消费&#xff0c;把免费用户转化为付费用户&#xff1b; 转化的重要性 1.转化意味着用户有投入&#xff0c;投入即沉没成…

Serverless冷扩机器在压测中被击穿问题 | 京东云技术团队

一、现象回顾 在今天ForceBot全链路压测中&#xff0c;有位同事负责的服务做Serverless扩容&#xff08;负载达到50%之后自动扩容并上线接入流量&#xff09;中&#xff0c;发现新扩容的机器被击穿&#xff0c;监控如下&#xff08;关注2:40-3:15时间段的数据&#xff09;&…

人工智能AI 计算平台介绍

人工智能AI计算平台介绍 产品及服务&#xff1a; 标准模块开源 核心模块及服务收费 资源齐全 服务支持 产品使用者&#xff1a; 自行扩充组件 快速二次开发 轻松搭建企业级 数据挖掘应用 自…

linux0.12-10-1-总体功能

第10章 字符设备驱动程序 [466页] 10-1 总体功能 本章的程序可分成三部分: 第一部分是是关于RS-232串行线路驱动程序&#xff0c;包括程序rs_io.s和serial.c&#xff1b; 第二部分是涉及控制台的驱动程序&#xff0c;包括键盘中断驱动程序keyboard.S和控制台显示驱动程序con…

人工智能AI会话+文字转为markdown格式/思维导图的工具的介绍和使用

AI会话工具和文字转换为markdown格式 1、这里首先要有一个xmind思维导图软件&#xff0c;下载链接在下方。XMind: 2、本章中列举的AI工具是近期国内交火的prompt&#xff08;文心一言 作者还没排上队&#xff09; https://chat.givemeprompt.com/3、首先运用此工具生成你想要的…

leetcode刷题之数组问题总结,二分法,移除元素,

目录 一、二分查找相关应用704.二分查找35.搜索插入位置方法一:二分法暴力解法 34.在排序数组中查找元素的开始位置和最后一个位置方法一&#xff1a;暴力解法方法二&#xff1a;二分法&#xff0c;确定左右两侧的边界 69.x的平方根方法一:二分法方法二&#xff1a;暴力解法错解…

k8s1.26.1部署 ingress-nginx-controller(1.6.4)

文章目录 参考ingress-nginx git地址ingress-nginx 的 deployment的地址&#xff1a; 参考 1.24版本k8s集群配置Nginx-Ingressrancher配置https域名访问图文教程 ingress-nginx git地址 https://github.com/kubernetes/ingress-nginx ingress-nginx 的 deployment的地址&a…

cdn配置(超详细+图解+原理)

具体的详细配置在右侧目录翻到“三”&#xff0c;前面的一二是将原理 以腾讯云的cdn为例&#xff0c;其它家的大同小异 一、cdn作用和配置思路 &#xff08;一&#xff09;cdn作用 1.加速访问 cdn服务通常有多个节点缓存&#xff0c;用户可以就近获取&#xff0c;延迟较低 …

得物App万米高空WiFi拦截记

0.前情摘要 在一次飞行途中&#xff0c;我司客户遭遇到了得物App在飞机上的WiFi网络访问异常的问题。这让我们意识到在特定场景下&#xff0c;用户可能面临无法使用得物App的困扰。经过SRE团队与无线团队、网络团队联合全力排查与优化&#xff0c;最终成功解决了这一问题&…

PAN(Pyramid Attention Network for semantic segmentation)paper解读

Pyramid Attention Network for Semantic Segmentation讲PAN用于语义分割&#xff0c;网络结构类似encoder-decode, u-shape。 背景 encoder-decoder结构&#xff0c; 在encoding到高维度特征的过程中&#xff0c;原始的纹理信息会遭遇空间分辨率损失&#xff0c;例如FCN。 P…

Spring Boot进阶(39):SpringBoot之Jackson配置全局时间日期格式 | 超级详细,建议收藏

1. 前言&#x1f525; 不知道小伙伴对于日期字段&#xff0c;在项目中都是如何处理的&#xff0c;是单独给每个字段都自定义日期格式还是做全局格式设置&#xff1f;这个我之前啊&#xff0c;是对日期都是有做单独配置&#xff0c;给每个Entity带有日期字段的都配JsonFormat注解…