秒杀接口优化
- 一. 秒杀接口优化
接口优化核心思路:减少数据库的访问。(数据库抗并发的能力有限)
- 使用Redis预减库存减少对数据库的访问
- 使用内存标记减少Redis的访问
- 使用RabbitMQ队列缓冲,异步下单,增强用户体验
一. 秒杀接口优化
①. 系统初始化,把商品库存数量加载到Redis上面来
// afterPropertiesSet系统初始化时,进行一些操作
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVO> list = goodsService.getGoodsVO();
if (list == null) {
return;
}
for (GoodsVO goodsVO : list) { // 删除旧缓存
redisService.delete(GoodsPrefix.goodsMiaoshaStock, "" + goodsVO.getId());
}
for (GoodsVO goodsVO : list) { //缓存预热
redisService.set(GoodsPrefix.goodsMiaoshaStock, "" + goodsVO.getId(), goodsVO.getStockCount());
localOverMap.put(goodsVO.getId(), false); //初始化 存储false
}
}
②. 验证path,没有则非法请求
③. 通过goodsId判断秒杀是否结束,失败直接返回,减少redis访问
④. 判断缓存中是否已经有订单了
⑤. 预减库存 redis库存减一,返回剩余库存
⑥. 交给RabbitMQ进行处理,入队,通过队列把同步请求变为异步请求,减少等待时间
- 库存充足,且无重复秒杀,将秒杀请求封装后放入消息队列,同时给前端返回一个字符串"排队中",即代表正在排队中(返回的并不是失败或者成功,此时还不能判断)
// ⑤.入队,通过队列把同步请求变为异步请求,减少等待时间
MiaoshaMessageDTO miaoshaMessageDTO = new MiaoshaMessageDTO();
miaoshaMessageDTO.setGoodsId(goodsId);
miaoshaMessageDTO.setUser(user);
// 【减库存 下订单 写入秒杀订单】 全在mq消息队列里面做了
mqSender.sendMessage(miaoshaMessageDTO);
// 异步返回排队中
return ResultUtil.success("排队中");
封装MQ入队信息类
@Data
public class MiaoshaMessageDTO {
//用户信息
private User user;
//商品ID
private Long goodsId;
}
⑦. RabbitMQ处理异步订单 ——Rabbit配置
1. 导包
<!--消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. application.yml配置
3. Rabbitmq配置类_队列_交换机
package com.xizi.miaosha.rabbitmq;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author xizizzz
* @description: 消息队列配置类
* @date 2021-6-23下午 07:48
*/
@Configuration
public class MQConfig {
//秒杀队列名
public static final String MIAOSHA_QUEUE = "miaosha.queue";
//秒杀交换机
public static final String MIAOSHA_EXCHANGE = "miaosha.exchange";
//队列注入ioc容器中去
@Bean
public Queue queue() {
return new Queue(MQConfig.MIAOSHA_QUEUE, true);
}
}
4. 发送信息组件
package com.xizi.miaosha.rabbitmq;
import com.alibaba.fastjson.JSON;
import com.xizi.miaosha.dto.MiaoshaMessageDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author xizizzz
* @description:
* @date 2021-6-23下午 07:48
*/
@Slf4j
@Component
public class MQSender {
//注入默认的 amqp模板
@Resource
private AmqpTemplate amqpTemplate;
//发送消息方法
public void sendMessage(MiaoshaMessageDTO miaoshaMessageDTO) {
//打印日志
log.info("【MQ请求入队】,message={}", miaoshaMessageDTO.toString());
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, JSON.toJSONString(miaoshaMessageDTO));
}
}
5. 接收者监听组件
⑧. 前端接收到数据后,显示排队中,并根据商品id和用户id轮询请求服务器(200ms轮询一次)
1. 请求获取订单接口
2. 后台商品id和用户id判断缓存中是否有秒杀订单
轮询查询秒杀结果,成功:orderId 失败:-1 排队中:0
// 轮询查询秒杀结果
// 成功:orderId 失败:-1 排队中:0
@GetMapping(value = "/result")
@ResponseBody
public ResultVO result(@RequestParam(value = "goodsId") Long goodsId, User user) {
//先判断用户
if (user == null) {
return ResultUtil.error(ResultEnum.SESSION_OVERDUE);
}
//根据用户id和商品id判断 订单的结果
Long result = miaoshaOrderService.getMiaoshaResult(user.getId(), goodsId);
return ResultUtil.success(result);
}
根据用户id和商品id查缓存中 判断订单的结果
直接从redis中根据用户id和商品id h获取订单的信息
⑨. 后端RabbitMQ监听秒杀MIAOSHA_QUEUE队列
- 如果有消息过来就获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单、写入订单详情)
- 此时,前端根据商品id轮询请求result接口查看是否生成了商品订单,如果返回-1代表秒杀失败,返回0代表排队中,返回>0代表秒杀成功
1. Rabbitmq接收者监听
2. miaosha关键业务处理
//秒杀操作是一个事务,需使用@Transactional注解来标识,如果减少库存失败,则回滚
@Transactional
public OrderInfo miaosha(User user, GoodsVO goods) {
//1. 减库存---更新数据库中的数据
int i = miaoshaGoodsService.reduceStockById(goods.getId());
if (i == 0) { //返回更新i=0
// 秒杀结束 redis存入标记
setGoodsOver(goods.getId());
// 抛出自定义异常 秒杀结束
throw new CustomException(ResultEnum.MIAOSHA_OVER);
}
// 2. 根据用户信息和商品信息创建订单
OrderInfo orderInfo = orderInfoService.createOrder(user, goods);
// 3. 将订单信息存入到redis中 前端在定时器不断轮询查询订单的结果
redisService.set(MiaoshaOrderPrefix.getByUserIdAndGoodsId, "" + user.getId() + "_" + goods.getId(), orderInfo);
//4. 返回订单信息
return orderInfo;
}
3. 减库存—更新数据库中的数据
public interface MiaoshaGoodsMapper extends Mapper<MiaoshaGoods> {
//更新数据库中减库存1 并且stock_count>0
@Update("update miaosha_goods set stock_count=stock_count-1 where goods_id=#{goodsId} and stock_count>0")
public int reduceStockById(@Param("goodsId") Long goodsId);
}
4. 创建订单
//创建订单
@Transactional
public OrderInfo createOrder(User user, GoodsVO goodsVO) {
//创建订单信息表
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsCount(1);
orderInfo.setGoodsId(goodsVO.getId());
orderInfo.setGoodsName(goodsVO.getGoodsName());
orderInfo.setGoodsPrice(goodsVO.getMiaoshaPrice());
orderInfo.setUserId(user.getId());
orderInfo.setOrderChannel(1);
orderInfo.setStatus(PayStatusEnum.CREATE_NOT_PAY.getCode());
//插入到订单信息表中
orderInfoMapper.insertOrderInfo(orderInfo);
//创建秒杀订单
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goodsVO.getId());
miaoshaOrder.setUserId(user.getId());
miaoshaOrder.setOrderId(orderInfo.getId() );
//插入到秒杀订单中
miaoshaOrderService.createOrder(miaoshaOrder);
//返回订单信息
return orderInfo;
}
5. 将订单信息存入到redis中 前端在定时器不断轮询查询订单的结果
// 3. 将订单信息存入到redis中 前端在定时器不断轮询查询订单的结果
redisService.set(MiaoshaOrderPrefix.getByUserIdAndGoodsId, "" + user.getId() + "_" + goods.getId(), orderInfo);