学习秒杀系统-异步下单(包含RabbitMQ基础知识)
文章目录
- 前言
- 预备知识:RabbitMQ定义及常用组件概念
- 基础概念
- 常用组件
- 工作模式
- 2.1 简单模式 (Simple Mode)
- 2.2 工作队列模式 (Work Queues Mode)
- 2.1,2.2的代码实现:
- 2.3 发布/订阅模式 (Publish/Subscribe Mode)
- 2.3代码实现
- 2.4 路由模式 (Routing Mode)
- 2.5 主题模式 (Topics Mode)
- 2.4,2.5代码实现
- RabbitMQ安装与Spring boot集成
- 正文实现:Redis预减库存减少数据库访问
- 初始化加载库存数量
- 预减库存和判断重复下单操作:
- 异步下单
- 请求先入队
- 轮询检查如何判断是秒杀失败还没处理完
前言
本篇记录的是接口优化,其中核心思路就是减少数据库访问。方法步骤主要是
1.系统初始化,把商品库存数量加载到Redis中
2.收到请求,Redis预减库存,库存不足,直接返回。否则进入3
异步下单
3.请求入队,立即返回排队中(不是成功不是失败)
4.请求出队,生成订单,减少库存
5.客户端轮询,是否秒杀成功
通过这种方式请求线程不会一个一个阻塞
为什么说程序不阻塞了?(我们判断库存,入队后能够快速返回)
传统方式(阻塞):用户请求进来后,必须等待减库存、下单、数据库写入等所有步骤完成,才能得到响应。这个过程可能耗时几百毫秒甚至更久。在高并发下,线程阻塞时间长,导致请求堆积、系统崩溃。
异步方式(非阻塞):请求进来后,只做预减库存和入队操作(极快)。然后立即返回“排队中”,释放请求线程。后续的下单、减库存由队列消费者异步完成。请求线程不阻塞,能快速处理下一个请求,吞吐量大。
预备知识:RabbitMQ定义及常用组件概念
基础概念
什么是RabbitMQ?RabbitMQ是一个开源的消息队列软件(消息代理),它实现了高级消息队列协议(AMQP)。它允许应用程序通过发送和接收消息来进行通信,这些消息可以是简单的文本字符串、JSON对象或者是更为复杂的数据结构。RabbitMQ充当了消息生产者和消费者之间的中介,确保消息可靠地从一个应用传递到另一个应用。看不懂没关系,这里可以简单理解为它是一个中介,能确保发送者的消息可靠地传递给接收者。
为什么会有RabbitMQ?微服务架构中,不同服务之间需要一种可靠的机制来交换信息。传统的单体应用内部模块间可以直接调用函数或方法进行通信,但当系统被拆分成多个独立部署的服务时,直接调用变得不切实际。这时就需要一种异步的、松耦合的方式来处理服务间的通信,而消息队列就是为此设计的一种解决方案。
举个例子解释以下MQ地作用:
想象一下你经营着一家餐厅,你需要管理从前台接收到的订单到厨房准备食物再到服务员上菜的整个流程。
没有MQ之前的情况
在没有使用类似RabbitMQ这样的消息队列系统之前,你的前台、厨房和服务员之间的沟通可能如下:
前台接到顾客点餐后,直接走到厨房口头告知厨师需要准备什么菜品。
如果厨房很忙,前台就不得不等待直到厨师有空接收新订单。
同时,服务员也需要不断地询问厨房哪些菜品已经准备好可以端给顾客。
这种方式导致了效率低下,尤其是在高峰期,前台、厨房和服务员都手忙脚乱,容易出错。
有了MQ之后的情况
前台接单:当顾客下单后,前台不再需要直接去找厨师,而是将订单信息发送到一个“订单队列”中。这就像前台把订单放入了一个共享的任务篮子。
厨房处理订单:厨师们(可以有多个)从这个队列中获取订单,按照顺序或者根据自己的能力选择合适的订单进行处理。如果某位厨师暂时忙碌,其他空闲的厨师可以继续从队列中取任务,不会影响整体的工作流程。
通知服务员:一旦菜品准备好,厨师会把完成的通知放入另一个“菜品准备好”的队列里。服务员监听这个队列,一旦发现有菜品准备好就会及时去厨房取餐并送到顾客桌上。
这样是不是前台收到消息就可以忙别的了,后台厨房是不是也可以专心做饭了即使自身出了意外也不怕消息丢失了。
所以这里在开发场景中解决了以下问题:
异步处理(常用):RabbitMQ允许系统在不同的组件之间进行异步通信。这意味着生产者可以在消息被处理之前继续执行其他任务,从而提高系统的效率和响应速度。
负载均衡:RabbitMQ可以通过将消息分发到多个消费者来实现负载均衡,从而有效分配工作负载,防止单个消费者过载。
可靠性:RabbitMQ提供了持久化消息的功能,确保消息即使在系统崩溃或重启后仍然存在。此外,它支持确认机制,确保消息被成功处理。
解耦组件:通过使用消息队列,系统的不同部分可以解耦,这意味着它们可以独立开发、部署和扩展。这样可以提高系统的灵活性和可维护性。
常用组件
主要组件:
生产者(Producer):发送消息到 RabbitMQ 的应用程序。
消费者(Consumer):从 RabbitMQ 接收并处理消息的应用程序。
交换机(Exchange):负责接收生产者发送的消息,并根据一定的路由规则将消息转发到绑定的队列。
队列(Queue):存储消息,等待消费者进行处理。
绑定(Binding):连接交换机和队列的规则,定义消息如何从交换机路由到队列。
工作模式
2.1 简单模式 (Simple Mode)
简单模式,顾名思义,是最基础的消息通信模式,也被称为“Hello World”模式。它由一个生产者、一个队列和一个消费者组成。
特点:生产者将消息发送到队列,消费者从队列中获取消息并处理。消息按照先进先出(FIFO)的顺序进行处理。这种模式实现了最简单的点对点通信。
应用场景:适用于简单的消息传递,例如发送一次性通知、处理单个任务等。
2.2 工作队列模式 (Work Queues Mode)
工作队列模式(也称为任务队列或竞争消费者模式)旨在解决单个消费者处理消息速度慢的问题。在这种模式下,一个生产者将消息发送到一个队列,但有多个消费者同时监听该队列,共同竞争消息。
特点:
负载均衡:RabbitMQ 默认采用轮询(Round-robin)的方式将消息分发给不同的消费者,从而实现消息的负载均衡。
消息确认:为了确保消息不丢失,消费者在处理完消息后需要发送确认信号。如果消费者在处理过程中崩溃,未确认的消息会被重新投递给其他消费者。
应用场景:适用于处理耗时任务的场景,例如图片处理、视频转码、邮件发送等。通过增加消费者数量,可以提高系统的吞吐量和处理能力。
2.1,2.2的代码实现:
首先创建一个MQ配置类,并打上注解@Configuration。再创建MQsender类,最后创建MQreceiver类
这个配置类内部主要包含交换机和队列的bean,所以我们需要在springboot启动时候就要全部交给容器管理(因为我们用的时候直接用发送者对象,将消息发送到指定的交换机/队列,所以如果容器中没有,发送者就会找不到)
在MQ配置类中定义一个队列,queue是队列名
@Beanpublic Queue queue() {return new Queue("queue", true);}
在MQsender中,首先将消息转化为字符串或者json,注意并不是RabbitMQ传输消息只能传送这两个,只是因为兼容和便于处理,因为接收者发送者可能都不是同一个模块,处理起来当然不同(内部传输消息任意格式的二进制数据都可以)。其次,将消息发送到指定的队列中
@AutowiredAmqpTemplate amqpTemplate ;public void send(Object message) {String msg = RedisService.beanToString(message);log.info("send message:"+msg);amqpTemplate.convertAndSend("queue", msg);}
最后在MQreceiver中,
//这部分是监听配置类中队列名为queue的消息@RabbitListener(queues="queue")public void receive(String message) {//这里只是展示如何用,所以msg没有过多处理,实际上还需经过将stringtobean的方法。log.info("receive message:"+message);}
2.3 发布/订阅模式 (Publish/Subscribe Mode)
发布/订阅模式(Pub/Sub)实现了消息的广播。在这种模式下,消息不再直接发送到队列,而是发送到 Fanout Exchange。所有绑定到该交换机的队列都会收到消息,然后这些队列再将消息分发给各自的消费者。
特点:
广播:一个消息可以被多个消费者同时接收和处理。
解耦:生产者和消费者之间完全解耦,生产者无需知道有多少个消费者,消费者也无需知道消息来自哪个生产者。
应用场景:适用于需要将同一消息发送给多个订阅者的场景,例如日志系统(所有日志都发送给所有日志处理服务)、事件通知(用户注册成功后通知所有相关服务)等。
2.3代码实现
在配置类中定义一个交换机两个队列
@Beanpublic Queue topicQueue1() {return new Queue(TOPIC_QUEUE1, true);}@Beanpublic Queue topicQueue2() {return new Queue(TOPIC_QUEUE2, true);}@Beanpublic FanoutExchange fanoutExchage(){return new FanoutExchange(FANOUT_EXCHANGE);}@Beanpublic Binding FanoutBinding1() {//将队列1绑定到交换机中去return BindingBuilder.bind(topicQueue1()).to(fanoutExchage());}@Beanpublic Binding FanoutBinding2() {return BindingBuilder.bind(topicQueue2()).to(fanoutExchage());}
在发送者类中定义:
public void sendFanout(Object message) {String msg = RedisService.beanToString(message);log.info("send fanout message:"+msg);//注意这里是将消息发送到交换机amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE, "", msg);}
在接受者定义
//监听永远是监听队列@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)public void receiveTopic1(String message) {log.info(" topic queue1 message:"+message);}@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)public void receiveTopic2(String message) {log.info(" topic queue2 message:"+message);}
2.4 路由模式 (Routing Mode)
路由模式允许消息根据 routing key 进行有选择地路由。在这种模式下,消息发送到 Direct Exchange,队列在绑定到交换机时会指定一个 binding key。只有当消息的 routing key 与队列的 binding key 完全匹配时,消息才会被路由到该队列。
特点:
选择性接收:消费者可以根据自己感兴趣的 routing key 来接收消息。
灵活路由:通过定义不同的 routing key,可以实现消息的精细化路由。
应用场景:适用于需要根据消息的特定属性进行分类处理的场景,例如日志系统(根据日志级别路由到不同的处理服务,如 error 级别的日志发送到错误处理服务,info 级别的日志发送到信息记录服务)。
2.5 主题模式 (Topics Mode)
主题模式是路由模式的增强版,它允许 routing key 和 binding key 使用通配符进行模式匹配。消息发送到 Topic Exchange,队列在绑定到交换机时会指定一个包含通配符的 binding key。
特点:
更灵活的匹配:* 匹配一个单词,# 匹配零个或多个单词。这使得消息路由更加灵活和强大。
多条件订阅:消费者可以订阅符合特定模式的消息。
应用场景:适用于需要根据多个条件进行消息过滤和订阅的场景,例如股票行情系统(用户可以订阅 *.stock.us 来获取所有美国股票信息,或者订阅 ibm.# 来获取所有 IBM 相关的消息)。
2.4,2.5代码实现
首先在配置类中定义一个交换机和两个队列,随后定义一个绑定函数,就是将消息定向到哪个队列中
public static final String TOPIC_QUEUE1 = "topic.queue1";public static final String TOPIC_QUEUE2 = "topic.queue2";public static final String TOPIC_EXCHANGE = "topicExchage";@Beanpublic Queue topicQueue1() {return new Queue(TOPIC_QUEUE1, true);}@Beanpublic Queue topicQueue2() {return new Queue(TOPIC_QUEUE2, true);}@Beanpublic TopicExchange topicExchage(){return new TopicExchange(TOPIC_EXCHANGE);}@Beanpublic Binding topicBinding1() {将队列1绑定一个单词topic.key1,只有单词完全匹配时消息才会定向到该队列中return BindingBuilder.bind(topicQueue1()).to(topicExchage()).with("topic.key1");}@Beanpublic Binding topicBinding2() {将队列2绑定一个topic.开头的单词,#为通配符匹配,凡是以topic.开头的都会匹配到队列2return BindingBuilder.bind(topicQueue2()).to(topicExchage()).with("topic.#");}
在发送者类中定义:
public void sendTopic(Object message) {String msg = RedisService.beanToString(message);log.info("send topic message:"+msg);//将消息发送到topic交换机,单词为topic.key1的队列进行匹配amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg+"1");amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg+"2");}
在接受者类中定义:
@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)public void receiveTopic1(String message) {log.info(" topic queue1 message:"+message);}@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)public void receiveTopic2(String message) {log.info(" topic queue2 message:"+message);}
最后topic2 对打印 msg1 和 msg2 ,因为两个它是通配符都能匹配,而topic1只能打印队列1
文字部分参考文章:RabbitMQ 消息队列:从入门到Spring Boot实战
RabbitMQ安装与Spring boot集成
安装erlang 安装RabbitMQ 请移步到 RabbitMQ安装过程
假设你安装好了,现在开始导入依赖和配置项
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
配置项
你的rabbitmq服务器地址
spring.rabbitmq.host=10.110.3.62
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#\u6D88\u8D39\u8005\u6570\u91CF
# 设置消费者初始并发消费者数量(每个队列启动的消费者线程数)
spring.rabbitmq.listener.simple.concurrency=10
# 设置消费者最大并发数量(动态扩容时的最大线程数)
spring.rabbitmq.listener.simple.max-concurrency=10
# 每个消费者每次从队列中获取的消息数量(用于控制预取数量,适用于限流)
spring.rabbitmq.listener.simple.prefetch=1
# 是否自动启动消费者(默认为 true)
spring.rabbitmq.listener.simple.auto-startup=true
# 当消费者处理消息抛出异常时,是否将消息重新放回队列(默认为 true)
spring.rabbitmq.listener.simple.default-requeue-rejected=true
# 启用消息发送失败的重试机制
spring.rabbitmq.template.retry.enabled=true
# 重试的初始间隔时间(单位:毫秒)
spring.rabbitmq.template.retry.initial-interval=1000
# 最大重试次数(包括第一次发送,总共尝试 3 次)
spring.rabbitmq.template.retry.max-attempts=3
# 最大重试间隔时间(单位:毫秒)
spring.rabbitmq.template.retry.max-interval=10000
# 重试间隔时间的增长倍数(1.0 表示每次重试间隔时间不变)
spring.rabbitmq.template.retry.multiplier=1.0
我们不需要定义对应的配置类去接受这些参数。
这里有个问题是为什么我们写redis配置类的时候,需要接受连接池的参数。而RabbitMQ不需要呢,这是因为Spring Boot 提供了对 RabbitMQ 的自动配置支持,只要你引入了依赖(比如 spring-boot-starter-amqp)并在 application.yml 中配置了 RabbitMQ 的连接信息,Spring Boot 会自动创建连接工厂和 RabbitTemplate。
正文实现:Redis预减库存减少数据库访问
前置知识完成,终于到正文了。我们想要预减redis中库存,我们就需要把库存加载到redis中,而这个过程最适合放在系统初始化。
初始化加载库存数量
我们通过实现InitializingBean接口中的方法 afterPropertiesSet()
public void afterPropertiesSet() throws Exception {List<GoodsVo> goodsList = goodsService.listGoodsVo();if(goodsList == null) {return;}for(GoodsVo goods : goodsList) {//将库存数量放入redis中redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());//刚开始库存肯定不为0,所以将对应商品的值列为falselocalOverMap.put(goods.getId(), false);}}
这段代码可以在spring管理的任意类中定义,它是在应用启动过程中bean初始化后自动执行这段代码。
预减库存和判断重复下单操作:
//内存标记,减少redis访问boolean over = localOverMap.get(goodsId);if(over) {return Result.error(CodeMsg.MIAO_SHA_OVER);}//预减库存long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10if(stock < 0) {localOverMap.put(goodsId, true);return Result.error(CodeMsg.MIAO_SHA_OVER);}
疑问点1:内存标记有什么用呢?没有会怎样?
假如没有内存标记,我们直接执行redis中库存–操作,这种方式也是可行的,但是假如库存数量没了,后续的线程都需要访问redis减少库存,这样做是无意义的不如直接排除。例如秒杀商品只有十个 但有五千个线程 剩下四千九百多都访问redis也没什么用,所以我们采用内存标记将其排除
疑问点2:为什么减少操作不需要加锁?多个线程并发执行这一行代码怎么办?
其实这个问题在于你对redis概念不清楚,redis是单线程模型,天然满足原子操作。所以假如库存数量是10,就只有前十个线程通过此部分。
重复下单:
//判断是否已经秒杀到了MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order != null) {return Result.error(CodeMsg.REPEATE_MIAOSHA);}
此部分无需多言,可能有疑问是这部分不也访问数据库了?答案是的,但我们前一步已经过滤了非常多的线程,留下来的只有十个,所以这部分访问是允许的。
异步下单
请求先入队缓冲,异步下单,增强用户体验。这部分主要是为了线程不在等待上一个线程全部执行完,它才能执行,而是执行到一半(其实不到一半)你就可以执行了。
请求先入队
首先,我们将消息发送到队列中,这个消息是什么呢?消息就是接收者要操作的对象,针对此场景,接受者要处理秒杀业务,所以需要减库存,下订单 ,自然就需要知道商品id,和用户是谁。所以我们要将用户和商品封装为一个新对象传给队列。
MiaoshaMessage mm = new MiaoshaMessage();mm.setUser(user);mm.setGoodsId(goodsId);sender.sendMiaoshaMessage(mm);return Result.success(0);//排队中
发送者请求
public void sendMiaoshaMessage(MiaoshaMessage mm) {//转化为字符串形式String msg = RedisService.beanToString(mm);log.info("send message:"+msg);amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);}
接收者处理,我们还需要重新判断库存已经订单是否重复秒杀,此时就是针对数据库的操作了,因为我们要做的是下真实订单了。虽然这边是针对数据库,但是能到这里的线程是少数的,所以并不耗太多时间。
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)public void receive(String message) {log.info("receive message:"+message);//转化为对象MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);MiaoshaUser user = mm.getUser();long goodsId = mm.getGoodsId();判断数据库库存GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);int stock = goods.getStockCount();if(stock <= 0) {return;}//判断是否已经秒杀到了,redis中MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order != null) {return;}//减库存 下订单 写入秒杀订单miaoshaService.miaosha(user, goods);}
//减库存 下订单 写入秒杀订单
@Transactionalpublic OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {//减库存 下订单 写入秒杀订单boolean success = goodsService.reduceStock(goods);//这里sucess是指库存大于等于0的情况if(success) {//order_info maiosha_orderreturn orderService.createOrder(user, goods);}else {//为商品设置标记也就是售空了或者别的setGoodsOver(goods.getId());return null;}}//注意这里的ret我们是大于0 ,这个的意思是受影响的行数,也就是说减去库存影响了哪一行,如果成功则影响此行,如果失败则返回0(因为谁也没影响)public boolean reduceStock(GoodsVo goods) {MiaoshaGoods g = new MiaoshaGoods();g.setGoodsId(goods.getId());int ret = goodsDao.reduceStock(g);return ret > 0;}
// 这里加了个判断条件库存大于0,是防止超卖
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")public int reduceStock(MiaoshaGoods g);
疑问点:为什么外边加了@Transactional注解,内部的两个函数不是要么全成功要么全失败嘛 ?为什么还要判断 减库存是否成功呢?
注意澄清一下事务注解的失败与成功的定义,失败是指执行出错了(代码写错,逻辑有问题等)抛异常,而不是说你更新这个操作没更新成功,boolean success = goodsService.reduceStock(goods); 比如这个更新是失败的,但是没有抛出异常所以说他其实就是成功的执行了。我们的事务注解针对的是你代码执行出异常了那后边的也不用执行了,或者后边异常了 前边的会回滚到之前的状态。
所以此时判断成功失败,只是将其创建订单或做好标记。
创建订单方法
@Transactionalpublic OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {OrderInfo orderInfo = new OrderInfo();orderInfo.setCreateDate(new Date());orderInfo.setDeliveryAddrId(0L);orderInfo.setGoodsCount(1);orderInfo.setGoodsId(goods.getId());orderInfo.setGoodsName(goods.getGoodsName());orderInfo.setGoodsPrice(goods.getMiaoshaPrice());orderInfo.setOrderChannel(1);orderInfo.setStatus(0);orderInfo.setUserId(user.getId());//插入订单orderDao.insert(orderInfo);MiaoshaOrder miaoshaOrder = new MiaoshaOrder();miaoshaOrder.setGoodsId(goods.getId());miaoshaOrder.setOrderId(orderId);miaoshaOrder.setUserId(user.getId());orderDao.insertMiaoshaOrder(miaoshaOrder);redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder);return orderInfo;}
@SelectKey(keyColumn="id", keyProperty="id", resultType=long.class, before=false, statement="select last_insert_id()")public long insert(OrderInfo orderInfo);
orderDao.insert 到底怎么返回的id?
我们用了注解@SelectKey(keyColumn=“id”, keyProperty=“id”, resultType=long.class, before=false, statement=“select last_insert_id()”) 会返回自增的主键id给orderinfo上,这里我们用不用返回值接受都可以,他自动添加到我们插入的对象中去
轮询检查如何判断是秒杀失败还没处理完
当我们点击秒杀按钮时
function doMiaosha(){$.ajax({url:"/miaosha/do_miaosha",type:"POST",data:{goodsId:$("#goodsId").val(),},//成功时success:function(data){if(data.code == 0){//window.location.href="/order_detail.htm?orderId="+data.data.id;getMiaoshaResult($("#goodsId").val());}else{layer.msg(data.msg);}},//失败时error:function(){layer.msg("客户端请求有误");}});}
然后我们对应的就是上边返回的结果。
function getMiaoshaResult(goodsId){g_showLoading();$.ajax({url:"/miaosha/result",type:"GET",data:{goodsId:$("#goodsId").val(),},success:function(data){if(data.code == 0){var result = data.data;if(result < 0){layer.msg("对不起,秒杀失败");}else if(result == 0){//继续轮询setTimeout(function(){getMiaoshaResult(goodsId);}, 50);}else{layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]},function(){window.location.href="/order_detail.htm?orderId="+result;},function(){layer.closeAll();});}}else{layer.msg(data.msg);}},error:function(){layer.msg("客户端请求有误");}});
}
这个是轮询检查后端是否成功。
如何检查呢,我们需要写一个新的检查结果的函数,因为第一步我们只是异步下单了,后续的函数处理成功失败我们不知道。首先添加对应的请求,其次我们判断最后成功与否,我们只需要判断redis中的秒杀订单表是否有该订单即可,因为我们最后成功了会直接写入redis。 查看秒杀订单表,我们需要知道用户的id 和商品的id 才能知道最后是否成功
@RequestMapping(value="/result", method=RequestMethod.GET)@ResponseBodypublic Result<Long> miaoshaResult(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) {model.addAttribute("user", user);if(user == null) {return Result.error(CodeMsg.SESSION_ERROR);}long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId);return Result.success(result);}
在service下创建对应的方法
public long getMiaoshaResult(Long userId, long goodsId) {MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);if(order != null) {//秒杀成功return order.getOrderId();}else {//这里 在之前的一步,如果减库存失败了 这里存储到redis中就是true。boolean isOver = getGoodsOver(goodsId);//如果是true返回失败if(isOver) {return -1;}else {//如果不是,继续等待轮询return 0;}}}
setGoodsOver(goods.getId());
return null;
其实从获取结果上说明了这一步的必要性,因为如果没有这一步返回结果没有-1.只能是0或者1,所以我们要在可能会失败的步骤上添加上此结果。
总的来说,当我们点击秒杀按钮,我们会访问对应的后端方法,它会帮我们筛选一部分线程进入下一阶段(异步下单了),而大部分线程会直接返回失败。进入下一阶段的,我们首先判断数据库的库存以及订单表的订单(重复下单),然后再减库存,减库存时我们要加一个失败了则对对应的商品做一个失败标记,成功则创建订单,生成秒杀商品订单并保存到redis中,最后轮询查询结果,主要看redis有没有用户对应的商品订单,有就返回成功,无则返回失败(大概率)。