尚品汇-秒杀下单实现-页面轮询查询订单状态(五十三)

目录:

(1)整合秒杀业务

(2)秒杀下单

(3)秒杀下单监听

(4)页面轮询接口

(1)整合秒杀业务

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

步骤:

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

State为null,说明非法请求;

State0说明已经售罄;

State为1,说明可以抢购

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

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

(2)秒杀下单

添加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";

定义实体UserRecode

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

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

全局统一返回结果类: 

package com.atguigu.gmall.common.result;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;/*** 全局统一返回结果类**/
@Data
@ApiModel(value = "全局统一返回结果")
public class Result<T> {@ApiModelProperty(value = "返回码")private Integer code;@ApiModelProperty(value = "返回消息")private String message;@ApiModelProperty(value = "返回数据")private T data;public Result(){}// 返回数据protected static <T> Result<T> build(T data) {Result<T> result = new Result<T>();if (data != null)result.setData(data);return result;}public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {Result<T> result = build(body);result.setCode(resultCodeEnum.getCode());result.setMessage(resultCodeEnum.getMessage());return result;}public static<T> Result<T> ok(){return Result.ok(null);}/*** 操作成功* @param data* @param <T>* @return*/public static<T> Result<T> ok(T data){Result<T> result = build(data);return build(data, ResultCodeEnum.SUCCESS);}public static<T> Result<T> fail(){return Result.fail(null);}/*** 操作失败* @param data* @param <T>* @return*/public static<T> Result<T> fail(T data){Result<T> result = build(data);return build(data, ResultCodeEnum.FAIL);}public Result<T> message(String msg){this.setMessage(msg);return this;}public Result<T> code(Integer code){this.setCode(code);return this;}public boolean isOk() {if(this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue()) {return true;}return false;}
}

统一返回结果状态信息类:

package com.atguigu.gmall.common.result;import lombok.Getter;/*** 统一返回结果状态信息类**/
@Getter
public enum ResultCodeEnum {SUCCESS(200,"成功"),FAIL(201, "失败"),SERVICE_ERROR(2012, "服务异常"),ILLEGAL_REQUEST( 204, "非法请求"),PAY_RUN(205, "支付中"),LOGIN_AUTH(208, "未登陆"),PERMISSION(209, "没有权限"),SECKILL_NO_START(210, "秒杀还没开始"),SECKILL_RUN(211, "正在排队中"),SECKILL_NO_PAY_ORDER(212, "您有未支付的订单"),SECKILL_FINISH(213, "已售罄"),SECKILL_END(214, "秒杀已结束"),SECKILL_SUCCESS(215, "抢单成功"),SECKILL_FAIL(216, "抢单失败"),SECKILL_ILLEGAL(217, "请求不合法"),SECKILL_ORDER_SUCCESS(218, "下单成功"),COUPON_GET(220, "优惠券已经领取"),COUPON_LIMIT_GET(221, "优惠券已发放完毕"),;private Integer code;private String message;private ResultCodeEnum(Integer code, String message) {this.code = code;this.message = message;}
}

(3)秒杀下单监听

思路:

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

SeckillReceiver添加监听方法

@Autowired
private SeckillGoodsService seckillGoodsService;//  监听用户与商品的消息!
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = MqConst.QUEUE_SECKILL_USER,durable = "true",autoDelete = "false"),exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_SECKILL_USER),key = {MqConst.ROUTING_SECKILL_USER}
))
public void seckillUser(UserRecode userRecode,Message message,Channel channel){try {//  判断接收过来的数据if (userRecode!=null){//  预下单处理!seckillGoodsService.seckillOrder(userRecode.getSkuId(),userRecode.getUserId());}} catch (Exception e) {e.printStackTrace();}//  手动确认channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

预下单接口SeckillGoodsService接口


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

秒杀订单实体类

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.toString(), RedisConst.SECKILL__TIMEOUT, TimeUnit.SECONDS);if (!isExist) {return;}//获取队列中的商品,取List中的一个,从右边出来一个,如果能够获取,则商品存在,可以下单String goodsId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();if (StringUtils.isEmpty(goodsId)) {//商品售罄,更新状态位  0位售罄状态  发布订阅消息,商品已经销售完了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));//订单数据存入ReidsredisTemplate.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:46String stockKey = RedisConst.SECKILL_STOCK_PREFIX + skuId;//  redisTemplate.opsForList().leftPush(key,seckillGoods.getSkuId());//获取库存//Long count = this.redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).size();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();}
}
 /*** 获取商品详情* @param skuId* @return*/@Overridepublic SeckillGoods getSeckillGoods(Long skuId) {return (SeckillGoods) this.redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).get(skuId.toString());}

 

减少一个:之前9个 

生成一个订单

用户买过之后生成一个用户的信息(防止多次下单)

(4)页面轮询接口

 

思路:

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);}}//判断是否下单,查询总订单 seckill:orders:users userId OrderIdboolean 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);
}

 

 

 

轮询排队页面

该页面有四种状态:

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

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

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

总结:

商品秒杀流程:

1.用户抢单的时候先会生成一个下单码,后面会先校验用户的下单码,只有正确获得下单码的请求才是合法请求,然后再校验状态位state,状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力,前面条件都成立,将秒杀用户加入队列,然后直接返回

2.监听队列,进行清单,首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了

然后判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false

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

然后将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功,秒杀成功要更新库存(更新Mysql的库存和Redis中的库存)

3.前端页面轮训查询订单状态,判断用户是否抢单成功,下单了Redis生成临时订单数据(支付了会删除临时订单)抢单成功,去下单页面,判断用户是否下单查询总订单数据是否支付,下单了去我们订单页面,判断状态位,是否售罄

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/143798.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【iOS】——JSONModel源码

JSONModel用法 基本用法 将传入的字典转换成模型&#xff1a; 首先定义模型类&#xff1a; interface Person : JSONModel property (nonatomic, copy) NSString *name; property (nonatomic, copy) NSString *sex; property (nonatomic, assign) NSInteger age; end接…

索引的介绍

目录 1.索引的介绍 1.1 什么是索引 1.2 为什么要使用索引 2.索引应该选择哪种数据结构 3.MYSQL中的页 3.1为什么要使用页 3.2页文件头和页文件尾 3.3 页主体 3.3页目录 3.4数据页头 4.B在MYSQL索引中的应用 4.1计算三层树高的B树可以存放多少条记录 5.索引分类 5.1 主…

居中左右对齐且加粗的蓝色文本

//test.html <!DOCTYPE html> <html> <head> <title>我的网页</title> <link rel"stylesheet" href"styles.css"> </head> <body> <p class"left-text">这是居左对齐且加粗的…

Element走马灯组件循环播放两个页面是方向不一致

摘要&#xff1a;使用Carousel 走马灯循环播放同一类型的图片、文字等内容&#xff0c;会在循环内容为两组是出现下图 [1]中的现象。本文记录下如何解决 之前项目遇到过一次这个问题&#xff0c;由于indicator-position 指示器不用显示&#xff0c;则判断内容长度为2时&#xf…

springboot通过tomcat部署项目(包含jar、war两种方式,迄今为止全网最详细!2024更新..建议收藏,教学!万字长文!)

本博客参考的所有文章均已在结尾声明&#xff01;&#xff01;&#xff01; 在 Spring Boot 项目中&#xff0c;有两种常见的部署方式&#xff1a; 1、使用 Spring Boot 自带的 内置 Tomcat&#xff0c;将项目打包为 jar 并直接运行。 2、使用 外置 Tomcat&#xff0c;将项目打…

【电路笔记】-运算放大器比较器

运算放大器比较器 文章目录 运算放大器比较器1、概述2、表示2.1 同相比较器2.2 反相比较器3、临界点转换4、施密特触发器4.1 同相触发器4.2 反相触发器4.3 应用5、总结1、概述 在前面的大多数运算放大器文章中,电路都有一个到反相输入的反馈环路。 这种设计是最常见的,因为它…

海外社媒干货:Twitter的特点及运营策略

当你在海外社交媒体上运营&#xff0c;了解不同平台的特点和具体实践是非常重要的。本期让小编来为你介绍推特&#xff08;Twitter&#xff09;以及一些相关的运营干货&#xff1a; &#xff08;图片源于网络&#xff09; Twitter简介 推特是一家美国社交网络及微博客服务的公…

通过Python代码发送量化交易信号邮件通知

量化交易利用数学模型和计算机算法来分析市场数据,并生成交易信号,本文将介绍如何使用Python编写一个简单的脚本,通过发送邮件通知量化交易信号。 开启SMTP服务 首先要在发件箱的邮件设置中,将POP3/SMPT服务开启,记录下授权密码,在本地可通过此密码登录,注意有效期和保…

【C++ Primer Plus习题】16.9

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: #include <iostream> #include <ctime> #include <v…

6000 字掌握 Java IO 知识体系

“子谦&#xff0c;Java IO 也太上头了吧&#xff1f;”新兵蛋子小二向头顶很凉快的老韩抱怨道&#xff0c;“你瞧&#xff0c;我就按照传输方式对 IO 进行了一个简单的分类&#xff0c;就能搞出来这么多的玩意&#xff01;” 好久没搞过 IO 了&#xff0c;老王看到这幅思维导图…

LLM - 理解 多模态大语言模型(MLLM) 的 预训练(Pre-training) 与相关技术 (三)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142167709 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 完备(F…

数字自然资源领域的实现路径

在数字化浪潮的推动下&#xff0c;自然资源的管理与利用正经历着前所未有的变革。本文将从测绘地理信息与遥感专业的角度&#xff0c;深度分析数字自然资源领域的实现路径。 1. 基础数据的数字化 数字自然资源的构建&#xff0c;首先需要实现基础数据的数字化。这包括地形地貌…

GUI编程16:图片按钮、单选框、多选框

视频链接&#xff1a;18、图片按钮、单选框、多选框_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1DJ411B75F?p18&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.图片按钮代码示例 package com.yundait.lesson05;import javax.swing.*; import java.awt.*; impo…

【Linux:共享内存】

共享内存的概念&#xff1a; 操作系统通过页表将共享内存的起始虚拟地址映射到当前进程的地址空间中共享内存是由需要通信的双方进程之一来创建但该资源并不属于创建它的进程&#xff0c;而属于操作系统 共享内存可以在系统中存在多份&#xff0c;供不同个数&#xff0c;不同进…

Google SERP API 对接说明

Google SERP API 对接说明 Google SERP&#xff08;Search Engine Results Page&#xff09;是用户在Google搜索引擎中输入查询后看到的结果页面。它显示自然搜索结果、广告、特色摘要、知识图谱以及图片、视频等多种内容&#xff0c;旨在为用户提供最相关的信息。 本文将详细…

心觉:成功学就像一把刀,有什么作用关键在于使用者(二)

Hi&#xff0c;我是心觉&#xff0c;与你一起玩转潜意识、脑波音乐和吸引力法则&#xff0c;轻松掌控自己的人生&#xff01; 挑战每日一省写作174/1000天 上一篇文章讲了成功学到底是个啥 是如何起作用的 为什么有些人觉得没有用&#xff1f; 今天我们再展开来剖析一下这…

运维监控专项学习笔记-id:0-需求场景、监控作用、监控能力

参考来源&#xff1a; 极客时间专栏-运维监控系统实战笔记&#xff0c;作者&#xff1a;秦晓辉 一、需求场景 学习监控知识&#xff0c;得先了解为什么&#xff0c;也就是监控是因何产生的&#xff0c;解决了什么问题&#xff0c;有哪些典型的方案&#xff0c;分别有什么优缺…

转行大模型开发:挑战与机遇,如何有效学习以实现职业转变

前言 甚至随着技术的进步&#xff0c;我们每个人都可能面临失业风险&#xff0c;因为未来我们所处的整个行业都可能被颠覆&#xff0c;公司也会不复存在。司机这一职业就是随着科技发展而不断演进的典型案例&#xff0c;从最早的马车夫&#xff0c;到现在的汽车驾驶员&#xf…

前端学习杂乱记录

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、Html二、CSS1. BFC布局2. 定位总结3. 动画1. transform变换2. transition过渡3. keyframes 和 animation 3. 伸缩盒模型&#xff1a;flex布局 三、JS1. 逻辑中断…

Aigtek功率放大器能应用哪些行业

功率放大器是一种在各个行业中发挥关键作用的技术设备&#xff0c;其应用涉及广泛&#xff0c;包括但不限于以下几个主要领域&#xff1a; 1.医疗行业&#xff1a; 在医疗领域&#xff0c;功率放大器常用于医学超声成像系统。超声波传感器通过发射和接收声波&#xff0c;生成图…