尚品汇商城

一、秒杀业务分析

1.1 需求分析

所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有三种限制:库存限制、时间限制、购买量限制。
(1)库存限制:商家只拿出限量的商品来秒杀。比如某商品实际库存是200件,但只拿出50件来参与秒杀。我们可以将其称为“秒杀库存”。商家赔本赚吆喝,图啥?人气!

(2)时间限制:通常秒杀都是有特定的时间段,只能在设定时间段进行秒杀活动;
(3)购买量限制:同一个商品只允许用户最多购买几件。比如某手机限购1件。张某第一次买个1件,那么在该次秒杀活动中就不能再次抢购
需求:
(1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
(2)运营商审核秒杀申请
(3)秒杀频道首页列出当天的秒杀商品,点击秒杀商品图片跳转到秒杀商品详细页。
(4)商品详细页显示秒杀商品信息,点击立即抢购进入秒杀,抢购成功时预减库存。当库存为0或不在活动期范围内时无法秒杀。
(5)秒杀成功,进入下单页填写收货地址、电话、收件人等信息,完成下订单,然后跳转到支付页面,支付成功,跳转到成功页,完成秒杀。
(6)当用户秒杀下单30分钟内未支付,取消订单,调用微信支付或支付宝的关闭订单接口。

1.2 秒杀功能分析

列表页
17 秒杀 - 图1
详情页

17 秒杀 - 图2
排队页

17 秒杀 - 图3

下单页
17 秒杀 - 图4
支付页
17 秒杀 - 图5

1.3 数据库表

秒杀商品表seckill_goods
17 秒杀 - 图6

1.4 秒杀实现思路

(1)秒杀的商品要提前放入到redis中(缓存预热),什么时间放入?凌晨放入当天的秒杀商品数据。
(2)状态位控制访问请求,何为状态位?就是我们在内存中保存一个状态,当抢购开始时状态为1,可以抢购,当库存为0时,状态位0,不能抢购;状态位的好处,他是在内存中判断,压力很小,可以阻止很多不必要的请求
(3)用户提交秒杀请求,将秒杀商品与用户id关联发送给mq,然后返回,秒杀页面通过轮询接口查看是否秒杀成功
(4)我们秒杀只是为了获取一个秒杀资格,获取秒杀资格就可以到下单页下订单,后续业务与正常订单一样
(5)下单我们需要注意的问题:
状态位如何同步到集群中的其他节点?
如何控制一个用户只下一个订单?
如何控制库存超买?
如何控制访问压力?
这些问题,我们都在后续陆续讲到

二、搭建秒杀模块

我们先把秒杀模块搭建好,秒杀一共有三个模块,秒杀微服务模块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/* # 路径匹配

三、秒杀商品导入缓存

缓存数据实现思路: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>
<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-taskspring.profiles.active=devspring.cloud.nacos.discovery.server-addr=192.168.200.129:8848spring.cloud.nacos.config.server-addr=192.168.200.129:8848spring.cloud.nacos.config.prefix=${spring.application.name}spring.cloud.nacos.config.file-extension=yamlspring.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 {

}

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 seckillGoodsQueryWrapper = new QueryWrapper<>();
seckillGoodsQueryWrapper.eq(“status”,“1”).gt(“stock_count”,0);
// select DATEFORMAT(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 seckillGoodsList = seckillGoodsMapper.selectList(seckillGoodsQueryWrapper);

  1. _// 将seckillGoodsList 这个集合数据放入缓存! _**for **(SeckillGoods seckillGoods : seckillGoodsList) {<br /> _// 考虑使用哪种数据类型,以及缓存的key!使用hash! hset key field value hget key field<br /> // 定义key = SECKILL_GOODS field = skuId value = seckillGoods<br /> // 判断当前缓存key 中是否有 秒杀商品的skuId<br /> _Boolean flag = **redisTemplate**.boundHashOps(RedisConst.**_SECKILL_GOODS_**).hasKey(seckillGoods.getSkuId().toString());<br /> _// 判断 _**if **(flag){<br /> _// 表示缓存中已经当前的商品了。 _**continue**;<br /> }<br /> _// 没有就放入缓存! _**redisTemplate**.boundHashOps(RedisConst.**_SECKILL_GOODS_**).put(seckillGoods.getSkuId().toString(),seckillGoods);<br /> _// 将每个商品对应的库存剩余数,放入redis-list 集合中! _**for **(Integer i = 0; i < seckillGoods.getStockCount(); i++) {<br /> _// 放入list key = seckill:stock:skuId;<br /> _String key = RedisConst.**_SECKILL_STOCK_PREFIX_**+seckillGoods.getSkuId();<br /> **redisTemplate**.opsForList().leftPush(key,seckillGoods.getSkuId().toString());<br /> _// redisTemplate.boundListOps(key).leftPush(seckillGoods.getSkuId());<br /> _}
  2. _// 秒杀商品在初始化的时候:状态位初始化 1<br /> // publish seckillpush 46:1 &#124; 后续业务如果说商品被秒杀完了! publish seckillpush 46:0<br /> _**redisTemplate**.convertAndSend(**"seckillpush"**,seckillGoods.getSkuId()+**":1"**);<br /> }<br /> } **catch **(Exception e) {<br /> e.printStackTrace();<br /> }<br /> _// 手动确认消息 _channel.basicAck(message.getMessageProperties().getDeliveryTag(),**false**);<br />}<br /> } |

| —- |

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 cacheMap = new ConcurrentHashMap();

/
加入缓存

*
@param **
key
__
*@param cacheObject
__
*/
public static void put(String key, Object cacheObject) {
cacheMap.put(key, cacheObject);
}

/
获取缓存
@param **
key
__
@return
**
**
/
public static Object get(String key) {
return cacheMap.get(key);
}

/
清除缓存

*
@param **
key
__
@return
**
**
/
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 表示商品id为1,状态位为1

3.2.2.2 redis发布消息

监听已经配置好,接下来我就发布消息,更改秒杀监听器{ SeckillReceiver },如下
17 秒杀 - 图7
完整代码如下

@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 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 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.1 封装秒杀列表与详情接口

4.1.1 封装接口

package com.atguigu.gmall.activity.service;


public interface SeckillGoodsService {

/
返回全部列表
@return
* /
_List 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 findAll() {
List 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;


/
返回全部列表

*
@return
* /
@GetMapping(“/findAll”)
public Result findAll() {
return Result.ok(seckillGoodsService.findAll());
}

/
获取实体

*
@param **
skuId
__
@return
**
**
/
@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”> 抢购成功  

<a href=”/seckill/trade.html” target=”_blank”>去下单</a> </div> <div class=”seckill_dev” v-if=”show == 4”> 抢购成功  

<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,说明请求非法;
State为0说明已经售罄;
State为1,说明可以抢购
状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力
3,前面条件都成立,将秒杀用户加入队列,然后直接返回
4,前端轮询秒杀状态,查询秒杀结果

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添加监听方法

@Autowiredprivate 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接口

/
根据商品id与用户ID查看订单信息
*
@param **
skuId
__
*@param userId
__
_ @return
**
**
/
_Result checkOrder(Long skuId, String userId);

5.3.2 实现类

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

  1. _//判断是否下单<br /> _**boolean **isExistOrder = **redisTemplate**.boundHashOps(RedisConst.**_SECKILL_ORDERS_USERS_**).hasKey(userId);<br /> **if**(isExistOrder) {<br /> String orderId = (String)**redisTemplate**.boundHashOps(RedisConst.**_SECKILL_ORDERS_USERS_**).get(userId);<br /> **return **Result._build_(orderId, ResultCodeEnum.**_SECKILL_ORDER_SUCCESS_**);<br /> }
  2. String state = (String) CacheHelper._get_(skuId.toString());<br /> **if**(**"0"**.equals(state)) {<br /> _//已售罄 抢单失败<br /> _**return **Result._build_(**null**, ResultCodeEnum.**_SECKILL_FAIL_**);<br /> }
  3. _//正在排队中<br /> _**return **Result._build_(**null**, ResultCodeEnum.**_SECKILL_RUN_**);<br />}<br /> |

| —- |

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”> 抢购成功  

<a href=”/seckill/trade.html” target=”_blank”>去下单</a> </div>

5.5 下单页面

17 秒杀 - 图8

我们已经把下单信息记录到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 userAddressList =
userFeignClient.findUserAddressListByUserId(userId);

_// 声明一个集合来存储订单明细
_ArrayList 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 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> trade();
ActivityDegradeFeignClient@Override
public Result> trade() {
return Result.fail();
}

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

SeckillController
/
确认订单
@param **
model
__
@return
**
**
/
@GetMapping(“seckill/trade.html”)
public String trade(Model model) {
Result> 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
@Autowiredprivate 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 seckillGoodsQueryWrapper =
new QueryWrapper<>();
seckillGoodsQueryWrapper.eq(
“status”,1);
seckillGoodsQueryWrapper.le(
“end_time”,new Date());
List seckillGoodsList =
seckillGoodsMapper**.selectList(seckillGoodsQueryWrapper);

  1. _// 对应将秒杀结束缓存中的数据删除! _**for **(SeckillGoods seckillGoods : seckillGoodsList) {<br /> _// seckill:stock:46 删除库存对应key<br /> _**redisTemplate**.delete(RedisConst.**_SECKILL_STOCK_PREFIX_**+seckillGoods.getSkuId());<br /> _// 如果有多个秒杀商品的时候, // redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).delete(seckillGoods.getSkuId());<br /> _}<br /> _// 删除预热等数据! 主要针对于预热数据删除! 我们项目只针对一个商品的秒杀! 如果是多个秒杀商品,则不能这样直接删除预热秒杀商品的key! // 46 : 10:00 -- 10:30 &#124; 47 : 18:10 -- 18:30<br /> _**redisTemplate**.delete(RedisConst.**_SECKILL_GOODS_**);<br /> _// 预下单 _**redisTemplate**.delete(RedisConst.**_SECKILL_ORDERS_**);<br /> _// 删除真正下单数据 _**redisTemplate**.delete(RedisConst.**_SECKILL_ORDERS_USERS_**);
  2. _// 修改数据库秒杀对象的状态! _SeckillGoods seckillGoods = **new **SeckillGoods();<br /> _// 1:表示审核通过 ,2:表示秒杀结束 _seckillGoods.setStatus(**"2"**);<br /> **seckillGoodsMapper**.update(seckillGoods,seckillGoodsQueryWrapper);<br /> } **catch **(Exception e) {<br /> e.printStackTrace();<br /> }<br /> _// 手动确认消息 _channel.basicAck(message.getMessageProperties().getDeliveryTag(),**false**);<br />}<br /> |

| —- |


说明:情况redis缓存,同时更改秒杀商品活动结束
行秒杀下单,提交成功,页面通过轮询后台方法查询秒杀状态