所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有三种限制:库存限制、时间限制、购买量限制。
(1)库存限制:商家只拿出限量的商品来秒杀。比如某商品实际库存是200件,但只拿出50件来参与秒杀。我们可以将其称为“秒杀库存”。商家赔本赚吆喝,图啥?人气!
(2)时间限制:通常秒杀都是有特定的时间段,只能在设定时间段进行秒杀活动;
(3)购买量限制:同一个商品只允许用户最多购买几件。比如某手机限购1件。张某第一次买个1件,那么在该次秒杀活动中就不能再次抢购
需求:B2B2C
(1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
(2)运营商审核秒杀申请
(3)秒杀频道首页列出当天的秒杀商品,点击秒杀商品图片跳转到秒杀商品详细页。
(4)商品详细页显示秒杀商品信息,点击立即抢购进入秒杀,抢购成功时预减库存。当库存为0或不在活动期范围内时无法秒杀。
(5)秒杀成功,进入下单页填写收货地址、电话、收件人等信息,完成下订单,然后跳转到支付页面,支付成功,跳转到成功页,完成秒杀。
(6)当用户秒杀下单30分钟内未支付,取消订单,调用微信支付或支付宝的关闭订单接口。
列表页
详情页
排队页
下单页
支付页
秒杀商品表seckill_goods
(1)秒杀的商品要提前放入到redis中(缓存预热),什么时间放入?凌晨放入当天的秒杀商品数据。
(2)状态位控制访问请求,何为状态位?就是我们在内存中保存一个状态,当抢购开始时状态为1,可以抢购,当库存为0时,状态位0,不能抢购;状态位的好处,他是在内存中判断,压力很小,可以阻止很多不必要的请求
(3)用户提交秒杀请求,将秒杀商品与用户id关联发送给mq,然后返回,秒杀页面通过轮询接口查看是否秒杀成功
(4)我们秒杀只是为了获取一个秒杀资格,获取秒杀资格就可以到下单页下订单,后续业务与正常订单一样
(5)下单我们需要注意的问题:
状态位如何同步到集群中的其他节点?
如何控制一个用户只下一个订单?
如何控制库存超买?
如何控制访问压力?
这些问题,我们都在后续陆续讲到
我们先把秒杀模块搭建好,秒杀一共有三个模块,秒杀微服务模块service-activity,负责封装秒杀全部服务端业务;秒杀前端模块web-all,负责前端显示业务;service-activity-client api接口模块
搭建方式如service-order
<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.0modelVersion>
<parent>
<groupId>com.atguigu.gmallgroupId>
<artifactId>serviceartifactId>
<version>1.0version>
parent>
<version>1.0version>
<artifactId>service-activityartifactId>
<packaging>jarpackaging>
<name>service-activityname>
<description>service-activitydescription>
<dependencies>
<dependency>
<groupId>com.atguigu.gmallgroupId>
<artifactId>service-user-clientartifactId>
<version>1.0version>
dependency>
<dependency>
<groupId>com.atguigu.gmallgroupId>
<artifactId>service-product-clientartifactId>
<version>1.0version>
dependency>
<dependency>
<groupId>com.atguigu.gmallgroupId>
<artifactId>service-order-clientartifactId>
<version>1.0version>
dependency>
<dependency>
<groupId>com.atguigu.gmallgroupId>
<artifactId>rabbit-utilartifactId>
<version>1.0version>
dependency>
dependencies>
<build>
<finalName>service-activityfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
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
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);
}
}
搭建方式如service-order-client
<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.0modelVersion>
<parent>
<groupId>com.atguigu.gmallgroupId>
<artifactId>service-clientartifactId>
<version>1.0version>
parent>
<artifactId>service-activity-clientartifactId>
<version>1.0version>
<packaging>jarpackaging>
<name>service-activity-clientname>
<description>service-activity-clientdescription>
project>
<dependency>
<groupId>com.atguigu.gmallgroupId>
<artifactId>service-activity-clientartifactId>
<version>1.0version>
dependency>
- 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队列保证了原子性),队列为空了说明商品售罄
在service-task模块发送消息
搭建方式如service-mq
<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.0modelVersion>
<parent>
<groupId>com.atguigu.gmallgroupId>
<artifactId>serviceartifactId>
<version>1.0version>
parent>
<artifactId>service-taskartifactId>
<version>1.0version>
<packaging>jarpackaging>
<name>service-taskname>
<description>service-taskdescription>
<dependencies>
<dependency>
<groupId>com.atguigu.gmallgroupId>
<artifactId>rabbit-utilartifactId>
<version>1.0version>
dependency>
dependencies>
<build>
<finalName>service-taskfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
说明:引入依赖
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);
}
}
定义凌晨一点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;
@Component
@EnableScheduling
@Slf4j
public class ScheduledTask {
@Autowired
private RabbitService rabbitService;
/**
* 每天凌晨1点执行
*/
//@Scheduled(cron = "0/30 * * * * ?")
@Scheduled(cron = "0 0 1 * * ?")
public void task1() {
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_1, "");
}
}
cron表达式各占位符解释:
{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}
{秒数}{分钟} ==> 允许值范围: 0~59 ,不允许为空值,若值不合法,调度器将抛出SchedulerException异常
“*” 代表每隔1秒钟触发;
“,” 代表在指定的秒数触发,比如”0,15,45”代表0秒、15秒和45秒时触发任务
“-“代表在指定的范围内触发,比如”25-45”代表从25秒开始触发到45秒结束触发,每隔1秒触发1次
“/”代表触发步进(step),”/”前面的值代表初始值(““等同”0”),后面的值代表偏移量,比如”0/20”或者”/20”代表从0秒钟开始,每隔20秒钟触发1次,即0秒触发1次,20秒触发1次,40秒触发1次;”5/20”代表5秒触发1次,25秒触发1次,45秒触发1次;”10-45/20”代表在[10,45]内步进20秒命中的时间点触发,即10秒触发1次,30秒触发1次
{小时} ==> 允许值范围: 0~23 ,不允许为空值,若值不合法,调度器将抛出SchedulerException异常,占位符和秒数一样
{日期} ==> 允许值范围: 1~31 ,不允许为空值,若值不合法,调度器将抛出SchedulerException异常
{星期} ==> 允许值范围: 1~7 (SUN-SAT),1代表星期天(一星期的第一天),以此类推,7代表星期六(一星期的最后一天),不允许为空值,若值不合法,调度器将抛出SchedulerException异常
{年份} ==> 允许值范围: 1970~2099 ,允许为空,若值不合法,调度器将抛出SchedulerException异常
注意:日期和星期的问题
?"与{日期}互斥,即意味着若明确指定{日期}触发,则表示{星期}无意义,以免引起冲突和混乱
常用实例:
“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秒触发任务
在service-activity模块绑定与监听消息,处理缓存逻辑,更新状态位
//秒杀商品前缀
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;
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> {
}
导入工具包{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);
}
}
由于我们的秒杀服务时集群部署service-activity的,我们面临一个问题?RabbitMQ 如何实现对同一个应用的多个节点进行广播呢?
RabbitMQ 只能对绑定到交换机上面的不同队列实现广播,对于同一队列的消费者他们存在竞争关系,同一个消息只会被同一个队列下其中一个消费者接收,达不到广播效果;
我们目前的需求是定时任务发送消息,我们将秒杀商品导入缓存,同事更新集群的状态位,既然RabbitMQ 达不到广播的效果,我们就放弃吗?当然不是,我们很容易就想到一种解决方案,通过redis的发布订阅模式来通知其他兄弟节点,这不问题就解决了吗?
过程大致如下
应用启动,多个节点监听同一个队列(此时多个节点是竞争关系,一条消息只会发到其中一个节点上)
消息生产者发送消息,同一条消息只被其中一个节点收到
收到消息的节点通过redis的发布订阅模式来通知其他兄弟节点
接下来配置redis发布与订阅
package com.atguigu.gmall.activity.redis;
@Configuration
public class RedisChannelConfig {
/*
docker exec -it e222dac4e559 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>();
/**
* 加入缓存
*
* @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
监听已经配置好,接下来我就发布消息,更改秒杀监听器{ 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);
}
说明:到目前我们就实现了商品信息导入缓存,同时更新状态位的工作
package com.atguigu.gmall.activity.service;
public interface SeckillGoodsService {
/**
* 返回全部列表
* @return
*/
List<SeckillGoods> findAll();
/**
* 根据ID获取实体
* @param id
* @return
*/
SeckillGoods getSeckillGoods(Long id);
}
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());
}
}
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));
}
}
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();
}
}
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}">¥0b>
<b class='ever-price' th:text="'¥'+${item.price}">¥0b>
div>
<div class='num'>
<div th:text="'已售'+${item.num}">已售1div>
<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}">0b>件div>
div>
<a class='sui-btn btn-block btn-buy' th:href="'/seckill/'+${item.skuId}+'.html'" target='_blank'>立即抢购a>
li>
ul>
div>
说明:
1,立即购买,该按钮我们要加以控制,该按钮就是一个链接,页面只是控制能不能点击,一般用户可以绕过去,直接点击秒杀下单,所以我们要加以控制,在秒杀没有开始前,不能进入秒杀页面
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";
}
<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}">0em>
<span th:text="'原价:'+${item.price}">原价:0span>
div>
<div class="fr remark">
剩余库存:<span th:text="${item.stockCount}">0span>
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>
思路:页面初始化时,拿到商品秒杀开始时间和结束时间等信息,实现距离开始时间和活动倒计时。
活动未开始时,显示距离开始时间倒计时;
活动开始后,显示活动结束时间倒计时。
倒计时代码片段
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;
}
在进入秒杀功能前,我们加一个下单码,只有你获取到该下单码,才能够进入秒杀方法进行秒杀
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加密。
页面获取下单码,进入秒杀场景
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>
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,前端轮询秒杀状态,查询秒杀结果
/**
* 秒杀
*/
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";
记录哪个用户要购买哪个商品!
@Data
public class UserRecode implements Serializable {
private static final long serialVersionUID = 1L;
private Long skuId;
private String userId;
}
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();
}
思路:
1,首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了;
2,判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,怎么控制呢?很简单,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false,直接返回,过期时间可以根据业务自定义,这样用户这一段咋们就控制注了
3,获取队列中的商品,如果能够获取,则商品有库存,可以下单。如果获取的商品id为空,则商品售罄,商品售罄我们要第一时间通知兄弟节点,更新状态位,所以在这里发送redis广播
@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);
}
秒杀订单实体类
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());
}
// 表示更新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();
}
}
思路:
1. 判断用户是否在缓存中存在
2. 判断用户是否抢单成功
3. 判断用户是否下过订单
4. 判断状态位
SeckillGoodsService接口
/***
* 根据商品id与用户ID查看订单信息
* @param skuId
* @param userId
* @return
*/
Result checkOrder(Long skuId, String userId);
@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);
}
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);
}
该页面有四种状态:
抢购成功,页面显示去下单,跳转下单确认页面
<div class="seckill_dev" v-if="show == 3">
抢购成功
<a href="/seckill/trade.html" target="_blank">去下单a>
div>
我们已经把下单信息记录到redis缓存中,所以接下来我们要组装下单页数据
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);
}
ActivityFeignClient
/**
* 秒杀确认订单
* @return
*/
@GetMapping("/api/activity/seckill/auth/trade")
Result<Map<String, Object>> trade();
ActivityDegradeFeignClient
@Override
public Result<Map<String, Object>> trade() {
return Result.fail();
}
SeckillController
/**
* 确认订单
* @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
该页面与正常下单页面类似,只是下单提交接口不一样,因为秒杀下单不需要正常下单的各种判断,因此我们要在订单服务提供一个秒杀下单接口,直接下单
OrderApiController
/**
* 秒杀提交订单,秒杀订单不需要做前置判断,直接下单
* @param orderInfo
* @return
*/
@PostMapping("inner/seckill/submitOrder")
public Long submitOrder(@RequestBody OrderInfo orderInfo) {
Long orderId = orderService.saveOrderInfo(orderInfo);
return orderId;
}
OrderFeignClient
/**
* 提交秒杀订单
* @param orderInfo
* @return
*/
@PostMapping("/api/order/inner/seckill/submitOrder")
Long submitOrder(@RequestBody OrderInfo orderInfo);
OrderDegradeFeignClient
@Override
public Long submitOrder(OrderInfo orderInfo) {
return null;
}
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)
}
})
},
说明:下单成功后,后续流程与正常订单一致
秒杀过程中我们写入了大量redis缓存,我们可以在秒杀结束或每天固定时间清楚缓存
,释放缓存空间;
实现思路:假如根据业务,我们确定每天18点所有秒杀业务结束,那么我们编写定时任务,每天18点发送mq消息,service-activity模块监听消息清理缓存
Service-task发送消息
/**
* 定时任务
*/
public static final String ROUTING_TASK_18 = "seckill.task.18";
//队列
public static final String QUEUE_TASK_18 = "queue.task.18";
/**
* 每天下午18点执行
*/
//@Scheduled(cron = "0/35 * * * * ?")
@Scheduled(cron = "0 0 18 * * ?")
public void task18() {
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_18, "");
}
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);
}