一、秒杀业务分析
1.1 需求分析
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有三种限制:库存限制、时间限制、购买量限制。
(1)库存限制:商家只拿出限量的商品来秒杀。比如某商品实际库存是200件,但只拿出50件来参与秒杀。我们可以将其称为“秒杀库存”。商家赔本赚吆喝,图啥?人气!
(2)时间限制:通常秒杀都是有特定的时间段,只能在设定时间段进行秒杀活动;
(3)购买量限制:同一个商品只允许用户最多购买几件。比如某手机限购1件。张某第一次买个1件,那么在该次秒杀活动中就不能再次抢购
需求:
(1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
(2)运营商审核秒杀申请
(3)秒杀频道首页列出当天的秒杀商品,点击秒杀商品图片跳转到秒杀商品详细页。
(4)商品详细页显示秒杀商品信息,点击立即抢购进入秒杀,抢购成功时预减库存。当库存为0或不在活动期范围内时无法秒杀。
(5)秒杀成功,进入下单页填写收货地址、电话、收件人等信息,完成下订单,然后跳转到支付页面,支付成功,跳转到成功页,完成秒杀。
(6)当用户秒杀下单30分钟内未支付,取消订单,调用微信支付或支付宝的关闭订单接口。
1.2 秒杀功能分析
1.3 数据库表
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
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
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 编写定时任务
3.1.1 搭建service-task服务
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.eq(“status”,“1”).gt(“stock_count”,0);
_seckillGoodsQueryWrapper.eq(“DATE_FORMAT(start_time,’%Y-%m-%d’)”, DateUtil._formatDate(new Date()));
_// 获取到当天秒杀的商品列表! _List
_// 将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 /> _}
_// 秒杀商品在初始化的时候:状态位初始化 1<br /> // publish seckillpush 46:1 | 后续业务如果说商品被秒杀完了! 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 / 加入缓存 * @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 },如下
完整代码如下
@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.eq(“status”, 1); queryWrapper.gt(“stock_count”, 0); queryWrapper.eq(“DATE_FORMAT(start_time,’%Y-%m-%d’)”, DateUtil._formatDate(new Date())); List //把数据放在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 根据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 List 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**);
}
}
_//判断是否下单<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 /> }
String state = (String) CacheHelper._get_(skuId.toString());<br /> **if**(**"0"**.equals(state)) {<br /> _//已售罄 抢单失败<br /> _**return **Result._build_(**null**, ResultCodeEnum.**_SECKILL_FAIL_**);<br /> }
_//正在排队中<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 下单页面
我们已经把下单信息记录到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 _// 声明一个集合来存储订单明细 _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.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 |
---|
ActivityDegradeFeignClient@Override public Result |
5.5.3 web-all 编写去下单控制器
SeckillController / 确认订单 @param **model __ @return ** **/ @GetMapping(“seckill/trade.html”) public String trade(Model model) { Result |
---|
页面资源: \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.eq(“status”,1);
seckillGoodsQueryWrapper.le(“end_time”,new Date());
List
_// 对应将秒杀结束缓存中的数据删除! _**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 | 47 : 18:10 -- 18:30<br /> _**redisTemplate**.delete(RedisConst.**_SECKILL_GOODS_**);<br /> _// 预下单 _**redisTemplate**.delete(RedisConst.**_SECKILL_ORDERS_**);<br /> _// 删除真正下单数据 _**redisTemplate**.delete(RedisConst.**_SECKILL_ORDERS_USERS_**);
_// 修改数据库秒杀对象的状态! _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缓存,同时更改秒杀商品活动结束
行秒杀下单,提交成功,页面通过轮询后台方法查询秒杀状态