目录:
(1)整合秒杀业务
(2)秒杀下单
(3)秒杀下单监听
(4)页面轮询接口
(1)整合秒杀业务
秒杀的主要目的就是获取一个下单资格,拥有下单资格就可以去下单支付,获取下单资格后的流程就与正常下单流程一样,只是没有购物车这一 步,总结起来就是,秒杀根据库存获取下单资格,拥有下单资格进入下单页面(选择地址,支付方式,提交订单,然后支付订单)
步骤:
- 校验下单码,只有正确获得下单码的请求才是合法请求
- 校验状态位state,
State为null,说明非法请求;
State为0说明已经售罄;
State为1,说明可以抢购
状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力
- 前面条件都成立,将秒杀用户加入队列,然后直接返回
- 前端轮询秒杀状态,查询秒杀结果
(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)秒杀下单监听
思路:
- 首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了;
- 判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,怎么控制呢?很简单,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false,直接返回,过期时间可以根据业务自定义,这样用户这一段咋们就控制注了
- 获取队列中的商品,如果能够获取,则商品有库存,可以下单。如果获取的商品id为空,则商品售罄,商品售罄我们要第一时间通知兄弟节点,更新状态位,所以在这里发送redis广播
- 将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功
- 秒杀成功要更新库存
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));
//订单数据存入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 = 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
*/
@Override
public 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 OrderId
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>
总结:
商品秒杀流程:
1.用户抢单的时候先会生成一个下单码,后面会先校验用户的下单码,只有正确获得下单码的请求才是合法请求,然后再校验状态位state,状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力,前面条件都成立,将秒杀用户加入队列,然后直接返回
2.监听队列,进行清单,首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了
然后判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false
要控制库存数量,不能超卖,提供一种解决方案,那就我们在导入商品缓存数据时,同时将商品库存信息导入队列{list},利用redis队列的原子性,保证库存不超卖
然后将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功,秒杀成功要更新库存(更新Mysql的库存和Redis中的库存)
3.前端页面轮训查询订单状态,判断用户是否抢单成功,下单了Redis生成临时订单数据(支付了会删除临时订单)抢单成功,去下单页面,判断用户是否下单查询总订单数据是否支付,下单了去我们订单页面,判断状态位,是否售罄