第十六章 秒杀

1.秒杀业务分析

1.1 需求分析

所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。

秒杀商品通常有三种限制:库存限制、时间限制、购买量限制。

1)库存限制:商家只拿出限量的商品来秒杀。比如某商品实际库存是200件,但只拿出50件来参与秒杀。我们可以将其称为“秒杀库存”。商家赔本赚吆喝,图啥?人气!

2)时间限制:通常秒杀都是有特定的时间段,只能在设定时间段进行秒杀活动;

3)购买量限制:同一个商品只允许用户最多购买几件。比如某手机限购1件。张某第一次买个1件,那么在该次秒杀活动中就不能再次抢购

 

1.2 秒杀功能分析

列表页

 详情页

 排队页

 下单页

支付页

1.3 数据库表

秒杀商品表seckill_goods

 

2.搭建秒杀模块

我们先把秒杀模块搭建好,秒杀一共有三个模块,秒杀微服务模块service-activity,负责封装秒杀全部服务端业务;秒杀前端模块web-all,负责前端显示业务;service-activity-client api接口模块

2.1 搭建service-activity模块

2.1.1 搭建service-activity

搭建方式如service-order

2.1.2 修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>com.atguigu.gmall</groupId>
      <artifactId>service</artifactId>
      <version>1.0</version>
   </parent>

   <version>1.0</version>
   <artifactId>service-activity</artifactId>
   <packaging>jar</packaging>
   <name>service-activity</name>
   <description>service-activity</description>

   <dependencies>
      <dependency>
         <groupId>com.atguigu.gmall</groupId>
         <artifactId>service-user-client</artifactId>
         <version>1.0</version>
      </dependency>
      <dependency>
         <groupId>com.atguigu.gmall</groupId>
         <artifactId>service-product-client</artifactId>
         <version>1.0</version>
      </dependency>

      <dependency>
         <groupId>com.atguigu.gmall</groupId>
         <artifactId>service-order-client</artifactId>
         <version>1.0</version>
      </dependency>
      <dependency>
         <groupId>com.atguigu.gmall</groupId>
         <artifactId>rabbit-util</artifactId>
         <version>1.0</version>
      </dependency>
   </dependencies>

   <build>
      <finalName>service-activity</finalName>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

  </project>

2.1.3 添加配置

bootstrap.properties

spring.application.name=service-activity
  spring.profiles.active=dev
  spring.cloud.nacos.discovery.server-addr=192.168.200.129:8848
  spring.cloud.nacos.config.server-addr=192.168.200.129:8848
  spring.cloud.nacos.config.prefix=${spring.application.name}
  spring.cloud.nacos.config.file-extension=yaml
  spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml

2.1.4 启动类

package com.atguigu.gmall.activity;

@SpringBootApplication
@ComponentScan({"com.atguigu.gmall"})
  @EnableDiscoveryClient
@EnableFeignClients(basePackages= {"com.atguigu.gmall"})
  public class ServiceActivityApplication {
   public static void main(String[] args) {
      SpringApplication.run(ServiceActivityApplication.class, args);
   }
}

2.2 搭建service-activity-client模块 

2.2.1 搭建service-activity-client

搭建方式如service-order-client

2.2.2 修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <parent>
      <groupId>com.atguigu.gmall</groupId>
      <artifactId>service-client</artifactId>
      <version>1.0</version>
   </parent>

   <artifactId>service-activity-client</artifactId>
   <version>1.0</version>

   <packaging>jar</packaging>
   <name>service-activity-client</name>
   <description>service-activity-client</description>

  </project>

2.3 添加依赖,配置网关        

2.3.1 在web-all中引入依赖

<dependency>
         <groupId>com.atguigu.gmall</groupId>
         <artifactId>service-activity-client</artifactId>
         <version>1.0</version>
      </dependency>

2.3.2 在网关项目中配置秒杀服务,域名

- id: web-activity
 
uri: lb://web-all
 
predicates:
  - Host=activity.gmall.com

- id: service-activity
 
uri: lb://service-activity
 
predicates:
  - Path=/*/activity/**
# 路径匹配

3.秒杀商品导入缓存

缓存数据实现思路:service-task模块统一管理我们的定时任务,为了减少service-task模块的耦合度,我们可以在定时任务模块只发送mq消息,需要执行定时任务的模块监听该消息即可,这样有利于我们后期动态控制,例如:每天凌晨一点我们发送定时任务信息到mq交换机,如果秒杀业务凌晨一点需要导入数据到缓存,那么秒杀业务绑定队列到交换机就可以了,其他业务也是一样,这样就做到了很好的扩展。

上面提到我们要控制库存数量,不能超卖,那么如何控制呢?在这里我们提供一种解决方案,那就我们在导入商品缓存数据时,同时将商品库存信息导入队列{list},利用redis队列的原子性,保证库存不超卖

库存加入队列实施方案

  1. 如果秒杀商品有N个库存,那么我就循环往队列放入N个队列数据
  2. 秒杀开始时,用户进入,然后就从队列里面出队,只有队列里面有数据,说明就一点有库存(redis队列保证了原子性),队列为空了说明商品售罄

3.1 编写定时任务

在service-task模块发送消息

3.1.1  搭建service-task服务

搭建方式如service-mq

3.1.2  修改配置pom.xml

<?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>com.atguigu.gmall</groupId>
      <artifactId>service</artifactId>
      <version>1.0</version>
   </parent>

   <artifactId>service-task</artifactId>
   <version>1.0</version>

   <packaging>jar</packaging>
   <name>service-task</name>
   <description>service-task</description>

   <dependencies>
      <!--rabbitmq消息队列-->
      <dependency>
         <groupId>com.atguigu.gmall</groupId>
         <artifactId>rabbit-util</artifactId>
         <version>1.0</version>
      </dependency>
   </dependencies>
  
   <build>
      <finalName>service-task</finalName>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

  </project>

说明:引入依赖

3.1.3  添加配置文件以及启动类

bootstrap.properties

spring.application.name=service-task
spring.profiles.active=dev
spring.cloud.nacos.discovery.server-addr=192.168.200.129:8848
spring.cloud.nacos.config.server-addr=192.168.200.129:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml

启动类

package com.atguigu.gmall.task;
 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消据源自配置
  @ComponentScan({"com.atguigu.gmall"})
  @EnableDiscoveryClient
  public class ServiceTaskApplication {
   public static void main(String[] args) {
      SpringApplication.run(ServiceTaskApplication.class, args);
   }
}

3.1.4  添加定时任务

定义凌晨一点mq相关常量

/**
 * 务
 */
  public static final String EXCHANGE_DIRECT_TASK = "exchange.direct.task";
  public static final String ROUTING_TASK_1 = "seckill.task.1";
  //public static final String QUEUE_TASK_1  = "queue.task.1";
package com.atguigu.gmall.task.scheduled;
  @Autowired
  private RabbitService rabbitService;

  @Component
@EnableScheduling
@Slf4j
  public class ScheduledTask {
   /**
 * 每天凌晨1行
 */
    //@Scheduled(cron = "0/30 * * * * ?")
    @Scheduled(cron = "0 0 1 * * ?")
    public void task1() {
    rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK,  MqConst.ROUTING_TASK_1, "");
   }
}

3.2 监听定时任务信息

在service-activity模块绑定与监听消息,处理缓存逻辑,更新状态位

3.2.1 数据导入缓存

3.2.1.1 在service-util的RedisConst类中定义常量

//商品前public static final String SECKILL_GOODS = "seckill:goods";
  public static final String SECKILL_ORDERS = "seckill:orders";
  public static final String SECKILL_ORDERS_USERS = "seckill:orders:users";
  public static final String SECKILL_STOCK_PREFIX = "seckill:stock:";
  public static final String SECKILL_USER = "seckill:user:";
  //户锁时间 单位:秒
  public static final int SECKILL__TIMEOUT = 60 * 60;

3.2.1.2 创建秒杀商品实体与Mapper

package com.atguigu.gmall.model.activity;
@Data
@ApiModel(description = "SeckillGoods")
  @TableName("seckill_goods")
  public class SeckillGoods extends BaseEntity {
   private static final long serialVersionUID = 1L;

   @ApiModelProperty(value = "spu ID")
   @TableField("spu_id")
   private Long spuId;

   @ApiModelProperty(value = "sku ID")
   @TableField("sku_id")
   private Long skuId;  

   @ApiModelProperty(value = "标题")
   @TableField("sku_name")
   private String skuName;  

   @ApiModelProperty(value = "商品图片")
   @TableField("sku_default_img")
   private String skuDefaultImg;  

   @ApiModelProperty(value = "原价格")
   @TableField("price")
   private BigDecimal price;

   @ApiModelProperty(value = "秒杀价格")
   @TableField("cost_price")
   private BigDecimal costPrice;

   @ApiModelProperty(value = "添加日期")
   @TableField("create_time")
   private Date createTime;

   @ApiModelProperty(value = "审核日期")
   @TableField("check_time")
   private Date checkTime;

   @ApiModelProperty(value = "审核状态")
   @TableField("status")
   private String status;
 

   @ApiModelProperty(value = "开始时间")
   @TableField("start_time")
   private Date startTime; 

   @ApiModelProperty(value = "结束时间")
   @TableField("end_time")
   private Date endTime; 

   @ApiModelProperty(value = "秒杀商品数")
   @TableField("num")
   private Integer num;

   @ApiModelProperty(value = "剩余库存数")
   @TableField("stock_count")
   private Integer stockCount; 

   @ApiModelProperty(value = "描述")
   @TableField("sku_desc")
   private String skuDesc;
}
package com.atguigu.gmall.activity.mapper;
  @Mapper
  public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {
}

3.2.1.3 监听消息

导入工具包{redis,util}到service-activity 项目中

package com.atguigu.gmall.activity.receiver;

  @Component
  public class SeckillReceiver {
    @Autowired
    private RedisTemplate redisTemplate;  

    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;
  
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MqConst.QUEUE_TASK_1,durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK),
        key = {MqConst.ROUTING_TASK_1}
))
public void importToRedis(Message message, Channel channel){
    try {
        //  将当天的秒杀商品放入缓存!通过mapper 执行sql 语句!
        //  条件当天 ,剩余库存>0 , 审核状态 = 1
        QueryWrapper<SeckillGoods> seckillGoodsQueryWrapper = new QueryWrapper<>();
        seckillGoodsQueryWrapper.eq("status","1").gt("stock_count",0);
        // select  DATE_FORMAT(start_time,'%Y-%m-%d') from seckill_goods; yyyy-mm-dd
        seckillGoodsQueryWrapper.eq("DATE_FORMAT(start_time,'%Y-%m-%d')", DateUtil.formatDate(new Date()));
        //  获取到当天秒杀的商品列表!
        List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(seckillGoodsQueryWrapper);

        //  将seckillGoodsList 这个集合数据放入缓存!
        for (SeckillGoods seckillGoods : seckillGoodsList) {
            //  考虑使用哪种数据类型,以及缓存的key!使用hash! hset key field value hget key field
            //  定义key = SECKILL_GOODS field = skuId value = seckillGoods
            //  判断当前缓存key 中是否有 秒杀商品的skuId
            Boolean flag = redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).hasKey(seckillGoods.getSkuId().toString());
            //  判断
            if (flag){
                //  表示缓存中已经当前的商品了。
                continue;
            }
            //  没有就放入缓存!
            redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(),seckillGoods);
            //  将每个商品对应的库存剩余数,放入redis-list 集合中!
            for (Integer i = 0; i < seckillGoods.getStockCount(); i++) {
                //  放入list  key = seckill:stock:skuId;
                String key = RedisConst.SECKILL_STOCK_PREFIX+seckillGoods.getSkuId();
                redisTemplate.opsForList().leftPush(key,seckillGoods.getSkuId().toString());
                //  redisTemplate.boundListOps(key).leftPush(seckillGoods.getSkuId());
            }

            //  秒杀商品在初始化的时候:状态位初始化 1
            //  publish seckillpush 46:1  | 后续业务如果说商品被秒杀完了! publish seckillpush 46:0
            redisTemplate.convertAndSend("seckillpush",seckillGoods.getSkuId()+":1");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    //  手动确认消息
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}



}

3.2.2 更新状态位

由于我们的秒杀服务时集群部署service-activity的,我们面临一个问题?RabbitMQ 如何实现对同一个应用的多个节点进行广播呢?

RabbitMQ 只能对绑定到交换机上面的不同队列实现广播,对于同一队列的消费者他们存在竞争关系,同一个消息只会被同一个队列下其中一个消费者接收,达不到广播效果;

我们目前的需求是定时任务发送消息,我们将秒杀商品导入缓存,同事更新集群的状态位,既然RabbitMQ 达不到广播的效果,我们就放弃吗?当然不是,我们很容易就想到一种解决方案,通过redis的发布订阅模式来通知其他兄弟节点,这不问题就解决了吗?

过程大致如下

    应用启动,多个节点监听同一个队列(此时多个节点是竞争关系,一条消息只会发到其中一个节点上)

    消息生产者发送消息,同一条消息只被其中一个节点收到

收到消息的节点通过redis的发布订阅模式来通知其他兄弟节点

接下来配置redis发布与订阅

3.2.2.1  redis发布与订阅实现

package com.atguigu.gmall.activity.redis;
  @Configuration
  public class RedisChannelConfig {
    /*
         docker exec -it  bc92 redis-cli
         subscribe seckillpush // 订阅 接收消息
         publish seckillpush admin // 发布消息
     */
    /**
     * 注入订阅主题
     * @param connectionFactory redis 链接工厂
     * @param listenerAdapter 消息监听适配器
     * @return 订阅主题对象
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //订阅主题

        container.addMessageListener(listenerAdapter, new PatternTopic("seckillpush"));

        //这个container 可以添加多个 messageListener

        return container;

    }

    /**
     * 返回消息监听器
     * @param receiver 创建接收消息对象
     * @return
     */
    @Bean
    MessageListenerAdapter listenerAdapter(MessageReceive receiver) {
        //这个地方 是给 messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“receiveMessage”
        //也有好几个重载方法,这边默认调用处理器的方法 叫handleMessage 可以自己到源码里面看
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }

    @Bean //注入操作数据的template
    StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
        return new StringRedisTemplate(connectionFactory);
    }
}
package com.atguigu.gmall.activity.redis;

  @Component
  public class MessageReceive {  

    /**接收消息的方法*/
    public void receiveMessage(String message){
        System.out.println("----------收到消息了message:"+message);
        if(!StringUtils.isEmpty(message)) {
            /*
             消息格式
                skuId:0 表示没有商品
                skuId:1 表示有商品
             */
             // 因为传递过来的数据为 ""6:1""
message = message.replaceAll("\"","");
            String[] split = StringUtils.split(message, ":");
            if (split == null || split.length == 2) {
                CacheHelper.put(split[0], split[1]);
            }
        }
    }

}

CacheHelper类本地缓存类

package com.atguigu.gmall.activity.util;
/**
 * 统缓类
 */
  public class CacheHelper {
/**
     * 存容器
     */
    private final static Map<String, Object> cacheMap = new ConcurrentHashMap<String, Object>();
/**
     * 加入存
     */
    public static void put(String key, Object cacheObject) {
        cacheMap.put(key, cacheObject);
    }
 /**

     * 
     */
    public static Object get(String key) {
        return cacheMap.get(key);
    }
 /**
     * 
     */
    public static void remove(String key) {
        cacheMap.remove(key);
    }

    public static synchronized void removeAll() {
        cacheMap.clear();
    }
}

说明:

  1. RedisChannelConfig 类配置redis监听的主题和消息处理器
  2. MessageReceive 类为消息处理器,消息message为:商品id与状态位,如:1:1 表示商品id1,状态位为1

3.2.2.2  redis发布消息

监听已经配置好,接下来我就发布消息,更改秒杀监听器{ SeckillReceiver },如下

 完整代码如下

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MqConst.QUEUE_TASK_1, durable = "true"),
        exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK, type = ExchangeTypes.DIRECT, durable = "true"),
        key = {MqConst.ROUTING_TASK_1}
))

  public void importItemToRedis(Message message, Channel channel) throws IOException {

    //Log.info("importItemToRedis:");
  

    QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("status", 1);
    queryWrapper.gt("stock_count", 0);

    //当天的秒杀商品导入缓存
    queryWrapper.eq("DATE_FORMAT(start_time,'%Y-%m-%d')", DateUtil.formatDate(new Date()));

    List<SeckillGoods> list = seckillGoodsMapper.selectList(queryWrapper);

    //把数据放在redis中
    for (SeckillGoods seckillGoods : list) {
        if (redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).hasKey(seckillGoods.getSkuId().toString()))
            continue;
        redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(), seckillGoods);

        //根据每一个商品的数量把商品按队列的形式放进redis中
        for (int i = 0; i < seckillGoods.getStockCount(); i++) {
            redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + seckillGoods.getSkuId()).leftPush(seckillGoods.getSkuId().toString());

        }
        //通知添加与更新状态位,更新为开启
        redisTemplate.convertAndSend("seckillpush", seckillGoods.getSkuId()+":1");
    }
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

说明:到目前我们就实现了商品信息导入缓存,同时更新状态位的工作

4.秒杀列表与详情

4.1 封装秒杀列表与详情接口

4.1.1 封装接口

package com.atguigu.gmall.activity.service;
public interface SeckillGoodsService {
/**
    * 返回全部列表
    * @return
    */
   List<SeckillGoods> findAll();
/**
    * 根据ID体
    * @param id
    * @return
    */
   SeckillGoods getSeckillGoods(Long id);
}

4.1.2 完成实现类

package com.atguigu.gmall.activity.service.impl;
 @Service
  public class SeckillGoodsServiceImpl implements SeckillGoodsService {
    @Autowired
    private RedisTemplate redisTemplate;
 /**
     * 查询全部
     */
    @Override
    public List<SeckillGoods> findAll() {
        List<SeckillGoods> seckillGoodsList = redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).values();
        return seckillGoodsList;
    }
 /**
     * 根据ID
     * @param id
     * @return
     */
    @Override
    public SeckillGoods getSeckillGoods(Long id) {
        return (SeckillGoods) redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).get(id.toString());
    }}

4.1.3 完成控制器

package com.atguigu.gmall.activity.controller;
  @RestController
@RequestMapping("/api/activity/seckill")
  public class SeckillGoodsApiController {
 @Autowired
    private SeckillGoodsService seckillGoodsService;
 @Autowired
    private UserFeignClient userFeignClient;
 @Autowired
    private ProductFeignClient productFeignClient;

    /**
     * 返回全部列表
     */
    @GetMapping("/findAll")
    public Result findAll() {
        return Result.ok(seckillGoodsService.findAll());
    }
    /**
     * 
     */
    @GetMapping("/getSeckillGoods/{skuId}")
    public Result getSeckillGoods(@PathVariable("skuId") Long skuId) {
        return Result.ok(seckillGoodsService.getSeckillGoods(skuId));
    }}

4.2 在service-activity-client模块添加接口

package com.atguigu.gmall.activity.client;
@FeignClient(value = "service-activity", fallback = ActivityDegradeFeignClient.class)
  public interface ActivityFeignClient {
/*
     * 返回全部列表
     * @return
     */
    @GetMapping("/api/activity/seckill/findAll")
    Result findAll();

    /**
     * 
     * @param skuId
     * @return
     */
    @GetMapping("/api/activity/seckill/getSeckillGoods/{skuId}")
    Result getSeckillGoods(@PathVariable("skuId") Long skuId);

}

package com.atguigu.gmall.cart.client.impl;
  @Component
  public class ActivityDegradeFeignClient implements ActivityFeignClient {
    @Override
    public Result findAll() {
        return Result.fail();
    }
  
    @Override
    public Result getSeckillGoods(Long skuId) {
        return Result.fail();
    }}

4.3 页面渲染

4.3.1 在web-all 中编写控制器

在 web-all 项目中添加控制器

package com.atguigu.gmall.all.controller;
  @Controller
  public class SeckillController {

    @Autowired
    private ActivityFeignClient activityFeignClient;

    /**
     * 列表
     * @param model
     * @return
     */
    @GetMapping("seckill.html")
    public String index(Model model) {
        Result result = activityFeignClient.findAll();
        model.addAttribute("list", result.getData());
        return "seckill/index";
    }

}

列表

页面资源: \templates\seckill\index.html

<div class="goods-list" id="item">
   <ul class="seckill" id="seckill">
      <li class="seckill-item" th:each="item: ${list}">
         <div class="pic" th:@click="|detail(${item.skuId})|">
            <img th:src="${item.skuDefaultImg}" alt=''>
         </div>
         <div class="intro">
            <span th:text="${item.skuName}">手机</span>
         </div>
         <div class='price'>
            <b class='sec-price' th:text="'¥'+${item.costPrice}">¥0</b>
            <b class='ever-price' th:text="'¥'+${item.price}">¥0</b>
         </div>
         <div class='num'>
            <div th:text="'已售'+${item.num}">已售1</div>
            <div class='progress'>
               <div class='sui-progress progress-danger'>
                  <span style='width: 70%;' class='bar'></span>
               </div>
            </div>
            <div>剩余
               <b class='owned' th:text="${item.stockCount}">0</b>件</div>
         </div>
         <a class='sui-btn btn-block btn-buy' th:href="'/seckill/'+${item.skuId}+'.html'" target='_blank'>立即抢购</a>
      </li>
   </ul>
  </div>

4.3.2 秒杀详情页面功能介绍

说明:

  1. 立即购买,该按钮我们要加以控制,该按钮就是一个链接,页面只是控制能不能点击,一般用户可以绕过去,直接点击秒杀下单,所以我们要加以控制,在秒杀没有开始前,不能进入秒杀页面

4.3.2.1 web-all添加商品详情控制器

SeckillController

@GetMapping("seckill/{skuId}.html")
  public String getItem(@PathVariable Long skuId, Model model){
    // skuId 查询skuInfo
    Result result = activityFeignClient.getSeckillGoods(skuId);
    model.addAttribute("item", result.getData());
    return "seckill/item";
}

4.3.2.2 详情页面介绍

4.3.2.2.1 基本信息渲染

<div class="product-info">
   <div class="fl preview-wrap">
      <!--放大镜效果-->
      <div class="zoom">
         <!--默认第一个预览-->
         <div id="preview" class="spec-preview">
            <span class="jqzoom"><img th:jqimg="${item.skuDefaultImg}" th:src="${item.skuDefaultImg}" width="400" height="400"/></span>
         </div>
      </div>

   </div>
   <div class="fr itemInfo-wrap">
      <div class="sku-name">
         <h4 th:text="${item.skuName}">三星</h4>
      </div>
      <div class="news">
         <span><img src="/img/_/clock.png"/>品优秒杀</span>
         <span class="overtime">{{timeTitle}}:{{timeString}}</span>
      </div>
      <div class="summary">
         <div class="summary-wrap">
            <div class="fl title">
               <i>秒杀价</i>
            </div>
            <div class="fl price">
               <i>¥</i>
               <em th:text="${item.costPrice}">0</em>
               <span th:text="'原价:'+${item.price}">原价:0</span>
            </div>
            <div class="fr remark">
               剩余库存:<span th:text="${item.stockCount}">0</span>
            </div>
         </div>
         <div class="summary-wrap">
            <div class="fl title">
               <i>促  销</i>
            </div>
            <div class="fl fix-width">
               <i class="red-bg">加价购</i>
               <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
            </div>
         </div>
      </div>
      <div class="support">
         <div class="summary-wrap">
            <div class="fl title">
               <i>支  持</i>
            </div>
            <div class="fl fix-width">
               <em class="t-gray">以旧换新,闲置手机回收 4G套餐超值抢 礼品购</em>
            </div>
         </div>
         <div class="summary-wrap">
            <div class="fl title">
               <i>配 送 至</i>
            </div>
            <div class="fl fix-width">
               <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
            </div>
         </div>
      </div>
      <div class="clearfix choose">

         <div class="summary-wrap">
            <div class="fl title">
            </div>
            <div class="fl">
               <ul class="btn-choose unstyled">
                  <li>
                     <a href="javascript:" v-if="isBuy" @click="queue()" class="sui-btn  btn-danger addshopcar">立即抢购</a>
                     <a href="javascript:" v-if="!isBuy" class="sui-btn  btn-danger addshopcar" disabled="disabled">立即抢购</a>
                  </li>
               </ul>
            </div>
         </div>
      </div>
   </div>
  </div>

4.3.2.2.2 倒计时处理

思路:页面初始化时,拿到商品秒杀开始时间和结束时间等信息,实现距离开始时间和活动倒计时。

活动未开始时,显示距离开始时间倒计时;

活动开始后,显示活动结束时间倒计时。

倒计时代码片段

init() {

  // debugger
// 计算出剩余时间
  var startTime = new Date(this.data.startTime).getTime();
  var endTime = new Date(this.data.endTime).getTime();
  var nowTime = new Date().getTime();
  

  var secondes = 0;
  // 还未开始抢购
  if(startTime > nowTime) {
   this.timeTitle = '距离开始'
   secondes = Math.floor((startTime - nowTime) / 1000);
}
  if(nowTime > startTime && nowTime < endTime) {
   this.isBuy = true
   this.timeTitle = '距离结束'
   secondes = Math.floor((endTime - nowTime) / 1000);
}
  if(nowTime > endTime) {
   this.timeTitle = '抢购结束'
   secondes = 0;
}

  const timer = setInterval(() => {
   secondes = secondes - 1
   this.timeString = this.convertTimeString(secondes)
}, 1000);

  // 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
  this.$once('hook:beforeDestroy', () => {
   clearInterval(timer);
})
},

时间转换方法

convertTimeString(allseconds) {
    if(allseconds <= 0) return '00:00:00'
    // 计算天数
    var days = Math.floor(allseconds / (60 * 60 * 24));

    // 小时
    var hours = Math.floor((allseconds - (days * 60 * 60 * 24)) / (60 * 60));

    // 分钟
    var minutes = Math.floor((allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60)) / 60);

    // 秒
    var seconds = allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60) - (minutes * 60);

    //拼接时间
    var timString = "";
    if (days > 0) {
        timString = days + "天:";
    }
    return timString += hours + ":" + minutes + ":" + seconds;
}

4.3.2.3 秒杀按钮控制

在进入秒杀功能前,我们加一个下单码,只有你获取到该下单码,才能够进入秒杀方法进行秒杀

4.3.2.3.1 获取下单码

SeckillGoodsApiController
@GetMapping("auth/getSeckillSkuIdStr/{skuId}")
  public Result getSeckillSkuIdStr(@PathVariable("skuId") Long skuId, HttpServletRequest request) {
    String userId = AuthContextHolder.getUserId(request);
    SeckillGoods seckillGoods = seckillGoodsService.getSeckillGoods(skuId);
    if (null != seckillGoods) {
        Date curTime = new Date();
        if (DateUtil.dateCompare(seckillGoods.getStartTime(), curTime) && DateUtil.dateCompare(curTime, seckillGoods.getEndTime())) {
            //可以动态生成,放在redis存
            String skuIdStr = MD5.encrypt(userId);
            return Result.ok(skuIdStr);
        }
    }
    return Result.fail().message("获取下单码失败");
}

说明:只有在商品秒杀时间范围内,才能获取下单码,这样我们就有效控制了用户非法秒杀,下单码我们可以根据业务自定义规则,目前我们定义为当前用户id MD5加密。

4.3.2.3.2 前端页面

页面获取下单码,进入秒杀场景

queue() {
       seckill.getSeckillSkuIdStr(this.skuId).then(response => {
        var skuIdStr = response.data.data
        window.location.href = '/seckill/queue.html?skuId='+this.skuId+'&skuIdStr='+skuIdStr
    })
},

前端js完整代码如下

<script src="/js/api/seckill.js"></script>
  <script th:inline="javascript">
   var item = new Vue({
      el: '#item',
      data: {
         skuId: [[${item.skuId}]],
         data: [[${item}]],
            timeTitle: '距离开始',
            timeString: '00:00:00',
            isBuy: false
      },

        created() {
            this.init()
        },

        methods: {
            init() {
            // debugger
            // 计算出剩余时间
            var startTime = new Date(this.data.startTime).getTime();
            var endTime = new Date(this.data.endTime).getTime();
            var nowTime = new Date().getTime();

            var secondes = 0;
            // 还未开始抢购
            if(startTime > nowTime) {

               this.timeTitle = '距离开始'
               secondes = Math.floor((startTime - nowTime) / 1000);
            }

            if(nowTime > startTime && nowTime < endTime) {
               this.isBuy = true
               this.timeTitle = '距离结束'
               secondes = Math.floor((endTime - nowTime) / 1000);
            }

            if(nowTime > endTime) {
               this.timeTitle = '抢购结束'
               secondes = 0;
            }

            const timer = setInterval(() => {
               secondes = secondes - 1
               this.timeString = this.convertTimeString(secondes)
            }, 1000);
            // 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
            this.$once('hook:beforeDestroy', () => {
               clearInterval(timer);
            })
            },

            queue() {
                            seckill.getSeckillSkuIdStr(this.skuId).then(response => {

                    var skuIdStr = response.data.data

                    window.location.href = '/seckill/queue.html?skuId='+this.skuId+'&skuIdStr='+skuIdStr

                })

            },

            convertTimeString(allseconds) {
                if(allseconds <= 0) return '00:00:00'
                // 计算天数
                var days = Math.floor(allseconds / (60 * 60 * 24));
                // 小时
                var hours = Math.floor((allseconds - (days * 60 * 60 * 24)) / (60 * 60));
                // 分钟
                var minutes = Math.floor((allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60)) / 60);
                // 秒
                var seconds = allseconds - (days * 60 * 60 * 24) - (hours * 60 * 60) - (minutes * 60);

                //拼接时间
                var timString = "";
                if (days > 0) {
                    timString = days + "天:";
                }
                return timString += hours + ":" + minutes + ":" + seconds;
            }
        }
   })

  </script>

4.3.3 编写排队控制器

SeckillController
@GetMapping("seckill/queue.html")
  public String queue(@RequestParam(name = "skuId") Long skuId,
                  @RequestParam(name = "skuIdStr") String skuIdStr,
                  HttpServletRequest request){
    request.setAttribute("skuId", skuId);
    request.setAttribute("skuIdStr", skuIdStr);
    return "seckill/queue";
}

页面

页面资源: \templates\seckill\queue.html

<div class="cart py-container" id="item">
    <div class="seckill_dev" v-if="show == 1">
        排队中...
    </div>
    <div class="seckill_dev" v-if="show == 2">
        {{message}}
    </div>
    <div class="seckill_dev" v-if="show == 3">
        抢购成功&nbsp;&nbsp;
        <a href="/seckill/trade.html" target="_blank">去下单</a>
    </div>
    <div class="seckill_dev" v-if="show == 4">
        抢购成功&nbsp;&nbsp;
        <a href="/myOrder.html" target="_blank">我的订单</a>
    </div>
  </div>

Js部分

<script src="/js/api/seckill.js"></script>
  <script th:inline="javascript">
    var item = new Vue({
        el: '#item',

        data: {
            skuId: [[${skuId}]],
            skuIdStr: [[${skuIdStr}]],
            data: {},
            show: 1,
            code: 211,
            message: '',
            isCheckOrder: false
        },

        mounted() {
            const timer = setInterval(() => {
                if(this.code != 211) {
                    clearInterval(timer);
                }
                this.checkOrder()
            }, 3000);
            // 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
            this.$once('hook:beforeDestroy', () => {
                clearInterval(timer);
            })
        },

        created() {
            this.saveOrder();
        },

        methods: {
            saveOrder() {
                seckill.seckillOrder(this.skuId, this.skuIdStr).then(response => {
                    debugger
                    console.log(JSON.stringify(response))
                    if(response.data.code == 200) {
                        this.isCheckOrder = true
                    } else {
                        this.show = 2
                        this.message = response.data.message
                    }
                })
            },

            checkOrder() {
                if(!this.isCheckOrder) return

                seckill.checkOrder(this.skuId).then(response => {
                     debugger
                    this.data = response.data.data
                    this.code = response.data.code
                    console.log(JSON.stringify(this.data))

                    //排队中
                    if(response.data.code == 211) {
                        this.show = 1
                    } else {
                        //秒杀成功
                        if(response.data.code == 215) {
                            this.show = 3
                            this.message = response.data.message
                        } else {
                            if(response.data.code == 218) {
                                this.show = 4
                                this.message = response.data.message
                            } else {
                                this.show = 2
                                this.message = response.data.message
                            }
                        }
                    }
                })
            }
        }
    })
  </script>

说明:该页面直接通过controller返回页面,进入页面后显示排队中,然后通过异步执行秒杀下单,提交成功,页面通过轮询后台方法查询秒杀状态

  • 整合秒杀业务

秒杀的主要目的就是获取一个下单资格,拥有下单资格就可以去下单支付,获取下单资格后的流程就与正常下单流程一样,只是没有购物车这一步,总结起来就是,秒杀根据库存获取下单资格,拥有下单资格进入下单页面(选择地址,支付方式,提交订单,然后支付订单)

步骤:

  1. 校验下单码,只有正确获得下单码的请求才是合法请求
  2. 校验状态位state

State为null,说明非法请求;

State0说明已经售罄;

State为1,说明可以抢购

状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力

  1. 前面条件都成立,将秒杀用户加入队列,然后直接返回
  2. 前端轮询秒杀状态,查询秒杀结果

5.1 秒杀下单

5.1.1 添加mq常量MqConst

/**
 * 杀
 */
  public static final String EXCHANGE_DIRECT_SECKILL_USER = "exchange.direct.seckill.user";
  public static final String ROUTING_SECKILL_USER = "seckill.user";
  //public static final String QUEUE_SECKILL_USER  = "queue.seckill.user";

5.1.2 定义实体UserRecode

记录哪个用户要购买哪个商品!

@Data
  public class UserRecode implements Serializable {
  private static final long serialVersionUID = 1L;
   private Long skuId;
   private String userId;
}

5.1.3 编写控制器

SeckillGoodsApiController

@Autowired
  private RabbitService rabbitService;
/**
 * 根据用户和商品ID实现秒杀下单
 * @param skuId
 * @return
 */
  @PostMapping("auth/seckillOrder/{skuId}")
  public Result seckillOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) throws Exception {
    //校验下单码(抢购码规则可以自定义)
      String userId = AuthContextHolder.getUserId(request);
    String skuIdStr = request.getParameter("skuIdStr");
    if (!skuIdStr.equals(MD5.encrypt(userId))) {

        //请求不合法
        return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);
    }

    //产品标识, 1:可以秒杀 0:秒杀结束
      String state = (String) CacheHelper.get(skuId.toString());
    if (StringUtils.isEmpty(state)) {

        //请求不合法
        return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);

    }

    if ("1".equals(state)) {

        //用户记录
                  UserRecode userRecode = new UserRecode();
        userRecode.setUserId(userId);
        userRecode.setSkuId(skuId);

  rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_SECKILL_USER, MqConst.ROUTING_SECKILL_USER, userRecode);
    } else {
        //已售罄
        return Result.build(null, ResultCodeEnum.SECKILL_FINISH);
    }
    return Result.ok();
}

5.2 秒杀下单监听

思路:

  1. 首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了;
  2. 判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,怎么控制呢?很简单,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false,直接返回,过期时间可以根据业务自定义,这样用户这一段咋们就控制注了
  3. 获取队列中的商品,如果能够获取,则商品有库存,可以下单。如果获取的商品id为空,则商品售罄,商品售罄我们要第一时间通知兄弟节点,更新状态位,所以在这里发送redis广播
  4. 将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功
  5. 秒杀成功要更新库存

5.2.1 SeckillReceiver添加监听方法

@Autowired 
private SeckillGoodsService seckillGoodsService;

//  监听用户与商品的消息!
@SneakyThrows
@RabbitListener
(bindings = @QueueBinding(
        value =
@Queue(value = MqConst.QUEUE_SECKILL_USER,durable = "true",autoDelete = "false"),
        exchange =
@Exchange(value = MqConst.EXCHANGE_DIRECT_SECKILL_USER),
        key = {MqConst.
ROUTING_SECKILL_USER}
))

public void seckillUser(UserRecode userRecode,Message message,Channel channel){
   
try {
       
//  判断接收过来的数据
        if (userRecode!=null){
           
//  预下单处理!
            seckillGoodsService.seckillOrder(userRecode.getSkuId(),userRecode.getUserId());
        }
    }
catch (Exception e) {
        e.printStackTrace();
    }
   
//  手动确认
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

5.2.2 预下单接口--->SeckillGoodsService接口

/**
 * 根据用和商品ID实现单
 * @param skuId
 * @param userId
 */
  void seckillOrder(Long skuId, String userId);

5.2.3 实现类

秒杀订单实体类

package com.atguigu.gmall.model.activity;
 @Data
  public class OrderRecode implements Serializable {
   private static final long serialVersionUID = 1L;
   private String userId;
   private SeckillGoods seckillGoods;
   private Integer num;
   private String orderStr;
}
/***
 * 订单
 * @param skuId
 * @param userId
 */
  @Override
  public void seckillOrder(Long skuId, String userId) {
    //状态位 1:可以秒 0:秒杀结束
    String state = (String) CacheHelper.get(skuId.toString());
    if("0".equals(state)) {
        //已售罄
        return;
    }

    //是否下单
    boolean isExist = redisTemplate.opsForValue().setIfAbsent(RedisConst.SECKILL_USER + userId, skuId, RedisConst.SECKILL__TIMEOUT, TimeUnit.SECONDS);
    if (!isExist) {
        return;
    }

    //列中的商品,如果能够获取,商品存在,可以下单
    String goodsId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();
    if (StringUtils.isEmpty(goodsId)) {
        //商品售罄,更新状态位
        redisTemplate.convertAndSend("seckillpush", skuId+":0");
        //已售罄
        return;
    }
    //订单记录
       OrderRecode orderRecode = new OrderRecode();
    orderRecode.setUserId(userId);
    orderRecode.setSeckillGoods(this.getSeckillGoods(skuId));
    orderRecode.setNum(1);
    //生成订单单码
    orderRecode.setOrderStr(MD5.encrypt(userId+skuId));
    //订单数据存入Reids

   redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).put(orderRecode.getUserId(), orderRecode);
//更新this.updateStockCount(orderRecode.getSeckillGoods().getSkuId());

}

5.2.4 更新库存

//  表示更新mysql -- redis 的库存数据!
public void updateStockCount(Long skuId) {
   
//  加锁!
    Lock lock = new ReentrantLock();
   
//  上锁
    lock.lock();
   
try {
       
//  获取到存储库存剩余数!
        //  key = seckill:stock:46
       
String stockKey = RedisConst.SECKILL_STOCK_PREFIX + skuId;
       
//  redisTemplate.opsForList().leftPush(key,seckillGoods.getSkuId());
       
Long count = redisTemplate.boundListOps(stockKey).size();
       
//  减少库存数!方式一减少压力!
       
if (count%2==0){
           
//  开始更新数据!
            SeckillGoods seckillGoods = this.getSeckillGoods(skuId);
           
//  赋值剩余库存数!
            seckillGoods.setStockCount(count.intValue());
           
//  更新的数据库!
            seckillGoodsMapper.updateById(seckillGoods);
           
//  更新缓存!
            redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(),seckillGoods);
        }
    }
finally {
       
//  解锁!
        lock.unlock();
    }

}

5.3 页面轮询接口

思路:

1.  判断用户是否在缓存中存在

2.  判断用户是否抢单成功

3.  判断用户是否下过订单

4.  判断状态位

5.3.1 接口

SeckillGoodsService接口
/***
 * 根据商品idID订单信息
  * @param skuId
 * @param userId
 * @return
 */
  Result checkOrder(Long skuId, String userId);

5.3.2 实现类

@Override
public Result checkOrder(Long skuId, String userId) {
   
// 用户在缓存中存在,有机会秒杀到商品
   
boolean isExist =redisTemplate.hasKey(RedisConst.SECKILL_USER + userId);
   
if (isExist) {
       
//判断用户是否正在排队
       
//判断用户是否下单
       
boolean isHasKey = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).hasKey(userId);
       
if (isHasKey) {
           
//抢单成功
           
OrderRecode orderRecode = (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);
           
// 秒杀成功!
           
return Result.build(orderRecode, ResultCodeEnum.SECKILL_SUCCESS);
        }
    }

   
//判断是否下单
   
boolean isExistOrder = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).hasKey(userId);
   
if(isExistOrder) {
        String orderId = (String)
redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).get(userId);
       
return Result.build(orderId, ResultCodeEnum.SECKILL_ORDER_SUCCESS);
    }

    String state = (String) CacheHelper.get(skuId.toString());
   
if("0".equals(state)) {
       
//已售罄 抢单失败
       
return Result.build(null, ResultCodeEnum.SECKILL_FAIL);
    }

   
//正在排队中
   
return Result.build(null, ResultCodeEnum.SECKILL_RUN);
}

5.3.3 控制器

SeckillGoodsApiController
@GetMapping(value = "auth/checkOrder/{skuId}")

  public Result checkOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) {

    //前登户

    String userId = AuthContextHolder.getUserId(request);

    return seckillGoodsService.checkOrder(skuId, userId);

}

5.4 轮询排队页面

该页面有四种状态:

  1. 排队中
  2. 各种提示(非法、已售罄等)
  3. 抢购成功,去下单
  4. 抢购成功,已下单,显示我的订单

抢购成功,页面显示去下单,跳转下单确认页面

<div class="seckill_dev" v-if="show == 3">
    抢购成功&nbsp;&nbsp;
    <a href="/seckill/trade.html" target="_blank">去下单</a>
  </div>

5.5 下单页面

 

我们已经把下单信息记录到redis缓存中,所以接下来我们要组装下单页数据

5.5.1 下单页数据数据接口

SeckillGoodsApiController

@Autowired
  private RedisTemplate redisTemplate;
/*
 * 认订单
 * @param request
 * @return
 */
  @GetMapping("auth/trade")
  public Result trade(HttpServletRequest request) {
    // 取到用Id
    String userId = AuthContextHolder.getUserId(request);

    // 先得到用想要购买的商品!
    OrderRecode orderRecode = (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);
    if (null == orderRecode) {
        return Result.fail().message("非法操作");
    }

    SeckillGoods seckillGoods = orderRecode.getSeckillGoods();

    //取用地址
    List<UserAddress> userAddressList = userFeignClient.findUserAddressListByUserId(userId);

    // 明一集合储订单细
    ArrayList<OrderDetail> detailArrayList = new ArrayList<>();

    OrderDetail orderDetail = new OrderDetail();
    orderDetail.setSkuId(seckillGoods.getSkuId());
    orderDetail.setSkuName(seckillGoods.getSkuName());
    orderDetail.setImgUrl(seckillGoods.getSkuDefaultImg());
    orderDetail.setSkuNum(orderRecode.getNum());
    orderDetail.setOrderPrice(seckillGoods.getCostPrice());

    // 添加到集合
    detailArrayList.add(orderDetail);

    // 额
         OrderInfo orderInfo = new OrderInfo();
    orderInfo.setOrderDetailList(detailArrayList);
    orderInfo.sumTotalAmount();
    Map<String, Object> result = new HashMap<>();
    result.put("userAddressList", userAddressList);
    result.put("detailArrayList", detailArrayList);
    // 保存额
        result.put("totalAmount", orderInfo.getTotalAmount());
    return Result.ok(result);
}

5.5.2 service-activity-client添加接口

ActivityFeignClient
/**
 * 认订单
  * @return
 */
  @GetMapping("/api/activity/seckill/auth/trade")
Result<Map<String, Object>> trade();
ActivityDegradeFeignClient
@Override
  public Result<Map<String, Object>> trade() {
    return Result.fail();
}

5.5.3 web-all 编写去下单控制器--->SeckillControlle


/**
 * 认订单
 * @param model
 * @return
 */
  @GetMapping("seckill/trade.html")
  public String trade(Model model) {
    Result<Map<String, Object>> result = activityFeignClient.trade();
    if(result.isOk()) {
        model.addAllAttributes(result.getData());
        return "seckill/trade";
    } else {
        model.addAttribute("message",result.getMessage());
        return "seckill/fail";
    }
}

页面资源: \templates\seckill\trade.html;\templates\seckill\fail.html

5.5.4 下单确认页面

该页面与正常下单页面类似,只是下单提交接口不一样,因为秒杀下单不需要正常下单的各种判断,因此我们要在订单服务提供一个秒杀下单接口,直接下单

5.5.4.1 service-order模块提供秒杀下单接口

OrderApiController
/**
 * 提交订单,秒杀订单不需要做前置判,直接下单
 * @param orderInfo
 * @return
 */

  @PostMapping("inner/seckill/submitOrder")
  public Long submitOrder(@RequestBody OrderInfo orderInfo) {
    Long orderId = orderService.saveOrderInfo(orderInfo);
    return orderId;
}

5.5.4.2 service-order-client模块暴露接口--->OrderFeignClient

/**
 * 提交秒杀订单
 * @param orderInfo
 * @return
 */
  @PostMapping("/api/order/inner/seckill/submitOrder")
Long submitOrder(@RequestBody OrderInfo orderInfo);
OrderDegradeFeignClient
@Override
  public Long submitOrder(OrderInfo orderInfo) {
    return null;
}

5.5.4.3 service-activity模块秒杀下单-->SeckillGoodsApiController

@Autowired
private OrderFeignClient orderFeignClient;
  @PostMapping("auth/submitOrder")
  public Result submitOrder(@RequestBody OrderInfo orderInfo, HttpServletRequest request) {
    String userId = AuthContextHolder.getUserId(request);

    orderInfo.setUserId(Long.parseLong(userId));

    Long orderId = orderFeignClient.submitOrder(orderInfo);
    if (null == orderId) {
        return Result.fail().message("下单失败,请重新操作");
    }
    //除下信息
  redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).delete(userId);
    //单记录
  redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).put(userId, orderId.toString());
    return Result.ok(orderId);
}

页面提交订单代码片段

submitOrder() {
    seckill.submitOrder(this.order).then(response => {
        if (response.data.code == 200) {
            window.location.href = 'http://payment.gmall.com/pay.html?orderId=' + response.data.data
        } else {
            alert(response.data.message)
        }
    })
},

说明:下单成功后,后续流程与正常订单一致

5.6 秒杀结束清空redis缓存

秒杀过程中我们写入了大量redis缓存,我们可以在秒杀结束或每天固定时间清楚缓存

,释放缓存空间;

实现思路:假如根据业务,我们确定每天18点所有秒杀业务结束,那么我们编写定时任务,每天18点发送mq消息,service-activity模块监听消息清理缓存

Service-task发送消息

5.6.1 添加常量MqConst

/**
 * 务
 */
  public static final String ROUTING_TASK_18 = "seckill.task.18";
  //
  public static final String QUEUE_TASK_18  = "queue.task.18";

5.6.2 编写定时任务发送消息

/**
 * 每天下午18行
 */
//@Scheduled(cron = "0/35 * * * * ?")
  @Scheduled(cron = "0 0 18 * * ?")
  public void task18() {
  rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_18, "");
}

5.6.3 接收消息并处理

Service-activity接收消息--->SeckillReceiver

//  监听删除消息!
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MqConst.QUEUE_TASK_18,durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK),
        key = {MqConst.ROUTING_TASK_18}
))
public void deleteRedisData(Message message, Channel channel){
    try {
        //  查询哪些商品是秒杀结束的!end_time , status = 1
        //  select * from seckill_goods where status = 1 and end_time < new Date();
        QueryWrapper<SeckillGoods> seckillGoodsQueryWrapper = new QueryWrapper<>();
        seckillGoodsQueryWrapper.eq("status",1);
        seckillGoodsQueryWrapper.le("end_time",new Date());
        List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(seckillGoodsQueryWrapper);

        //  对应将秒杀结束缓存中的数据删除!
        for (SeckillGoods seckillGoods : seckillGoodsList) {
            //  seckill:stock:46 删除库存对应key
            redisTemplate.delete(RedisConst.SECKILL_STOCK_PREFIX+seckillGoods.getSkuId());
            //  如果有多个秒杀商品的时候,
            //  redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).delete(seckillGoods.getSkuId());
        }
        //  删除预热等数据! 主要针对于预热数据删除! 我们项目只针对一个商品的秒杀! 如果是多个秒杀商品,则不能这样直接删除预热秒杀商品的key!
              //  46 : 10:00 -- 10:30 | 47 : 18:10 -- 18:30
    redisTemplate.delete(RedisConst.SECKILL_GOODS);
        //  预下单
        redisTemplate.delete(RedisConst.SECKILL_ORDERS);
        //  删除真正下单数据
        redisTemplate.delete(RedisConst.SECKILL_ORDERS_USERS);

        //  修改数据库秒杀对象的状态!
        SeckillGoods seckillGoods = new SeckillGoods();
        //  1:表示审核通过 ,2:表示秒杀结束
        seckillGoods.setStatus("2");
        seckillGoodsMapper.update(seckillGoods,seckillGoodsQueryWrapper);
    } catch (Exception e) {
        e.printStackTrace();
    }
    //  手动确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

说明:情况redis缓存,同时更改秒杀商品活动结束

行秒杀下单,提交成功,页面通过轮询后台方法查询秒杀状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值