高并发秒杀优化操作

1. 减少数据库的操作

  1. 判断是否重复抢购这个操作可以优化,大致思路是把用户订单放到Redis里,键中加上用户,抢购时判断是否已存在信息。来代替查询数据库

    1. 具体操作 java //生成订单时 redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goods.getId(), JsonUtil.object2JsonStr(seckillOrder));

      java //秒杀操作前 SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" +user.getId() + ":" + goodsId); //如果取出的信息不为空代表已经购买过此产品,不能再参与(限购为1情况下) if(seckillOrder != null){ return RespBean.error(RespBeanEnum.REPEATE_ERROR); }

  2. 库存可以在初始化时候就加入到Redis里,秒杀的时候直接减去Redis里的库存,然后再使用RabbitMQ消息队列分别进行处理

    1. 具体操作

      • 配置初始化库存

      ```java //接口调用implements InitializingBean @RestController @RequestMapping("/seckill") public class SecKillController implements InitializingBean{ //实现这个接口的方法 //系统初始化,把商品库存数量加载到Redis @Override public void afterPropertiesSet() throws Exception{ //从数据库找出有秒杀商品 List list = goodsService.findGoodsVo(); if(CollectionUtils.isEmpty(list)){ return; } //把每个秒杀产品的库存信息加载到Redis里 list.forEach(goodsVo -> redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount())); }

      } ```

      • 秒杀接口里配置库存减少操作

      java @RequestMapping("/doSeckill",method = RequestMethod.POST) public RespBean doSeckill(User user,Long goodsId){ //判断用户是否存在 //一堆代码判断是否重复抢购 //判断是否重复抢购后 ValueOperations valueOperations = redisTemplate.opsForValue(); //预减库存,每次执行自动-1 Long stock = valueOperations.decrement("seckillGoods:" + goodsId); //如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题 if (stock < 0){ //每次执行自动加一,防止库存出现-1的情况 valueOperations.increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTY_STOCK); } //生成订单,以后会替换成SeckillMessage Order order = orderService.seckill(user,goods); return RespBean.success(Order); }

  3. 把秒杀请求封装成一个对象发送给RabbitMQ。把操作异步掉,用消息队列达到流量削锋的目的。

    1. 具体操作

      • 创建封装对象SeckillMessage

      java public class SeckillMessage{ private User user; private Long goodId; //getter、setter、构造器省略 }

      • 选择使用RabbitMQ的Topic模式,进行config配置

      ```java @Configuration public class RabbitMQTopicConfig{ private static final String QUEUE = "seckillQueue"; private static final String EXCHANGE = "seckillExchange";

      @Bean
      public Queue queue(){
          return new Queue(QUEUE);
      }
      
      @Bean
      public TopicExchange topicExchange(){
          return new TopicExchange(EXCHANGE);
      }
      
      @Bean
      public Binding binding(){
          return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
      }

      } ```

      • MQSender配置

      ```java @Service //lombok的注解 @Slf4j public class MQSender { @Autowired private RabbitTemplate rabbitTemplate;

      public void sendSeckillMessage(String message){
          log.info("发送消息:"  + message);
          rabbitTemplate.converAndSend("seckillExchange", "seckill.message", message);
      }

      }

      ```

      • 配置完后回到Controller接口修改,加入MQSender

      ```java @Autowired private MQSender mqSender;

      @RequestMapping("/doSeckill",method = RequestMethod.POST) public RespBean doSeckill(User user,Long goodsId){ //判断用户是否存在 //一堆代码判断是否重复抢购 //判断是否重复抢购后 ValueOperations valueOperations = redisTemplate.opsForValue(); //预减库存,每次执行自动-1 Long stock = valueOperations.decrement("seckillGoods:" + goodsId); //如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题 if (stock < 0){ //每次执行自动加一,防止库存出现-1的情况 valueOperations.increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTY_STOCK); } //替换处 SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); //使用RabbitMQ实现异步,把订单生成放入另一部分操作,这样能快速返回信息。这里是JsonUtil是自己编写的工具类,在下文 mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage)); //前端收到0,要作出"排队中"的响应 return RespBean.success(0); } ```

      • MQReceiver,接受sender消息,异步掉操作

      ```java @Service //lombok的注解 @Slf4j public class MQReceiver { @Autowired private RabbitTemplate rabbitTemplate;

      @Autowired
      private IGoodsService goodsService;
      
      @Autowired
      private RedisTemplate redisTemplate;
      
      @Autowired
      private OrderService orderService;
      
      @RabbitListener(queues = "seckillQueue")
      public void receive(String message){
          //先打印下看看能不能拿到正确的message
          log.info("接受到的消息:" + message);
          //转换消息从JSON到对象
          SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
          //获取goodId和用户
          User user = seckillMessage.getUser();
          Long goodsId = seckillMessage.getGoodId();
          //获取商品信息
          GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
          //判断库存
          if(goodsVo.getStockCount() < 1){
              return;
          }
          //判断是否重复抢购
          SeckillOrder seckillOrder = 
              (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":"
                                                            + goodsId);
          if(seckillOrder != null){
              return;
          }
          //下单操作
          orderService.seckill(user,goodsVo);
      }

      } ```

      • -
  4. 内存标记,减少库存卖光之后对redis的访问

    1. 具体操作

      • 在Controller里设置个内存标记

      java //Long对应不同的商品id private Map<Long,Boolean> EmptyStockMap = new HashMap<>();

      • 之前的初始化方法重写,加入内存标记初始化

      ```java @Override public void afterPropertiesSet() throws Exception{ //从数据库找出有秒杀商品 List list = goodsService.findGoodsVo(); if(CollectionUtils.isEmpty(list)){ return; } //把每个秒杀产品的库存信息加载到Redis里 list.forEach(goodsVo -> { redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount())); EmptyStockMap.put(goodsVo.getId(),false); }

      }

      ```

      • 接口里具体实现内存标记判断

      ```java @Autowired private MQSender mqSender;

      @RequestMapping("/doSeckill",method = RequestMethod.POST) public RespBean doSeckill(User user,Long goodsId){ //判断用户是否存在 //一堆代码判断是否重复抢购 //判断是否重复抢购后 ValueOperations valueOperations = redisTemplate.opsForValue(); //预减库存,每次执行自动-1 Long stock = valueOperations.decrement("seckillGoods:" + goodsId); //内存标记判断,减少Redis访问 if(EmptyStockMap.get(goodsId)){ return RespBean.error(RespBeanEnum.EMPTYSTOCK); } //如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题 if (stock < 0){ //修改处,标记内存 EmptyStockMap.put(goodsId,true); //每次执行自动加一,防止库存出现-1的情况 valueOperations.increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTYSTOCK); } SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); //这里是JsonUtil是自己编写的工具类,在下文 mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage)); //前端收到0,要作出"排队中"的响应 return RespBean.success(0); } ```

      • -
  5. 轮询判断是否秒杀成功,因为之前只返回了一个排队中

    1. 具体操作

      • 在秒杀的Controller里再新建一个接口,专门获取秒杀结果,返回一个orderId代表成功,返回-1代表秒杀失败,返回0代表排队中

      • Controller

      java @RequestMapping("/result",method = RequestMethod.POST) public RespBean getResult(User user, Long goodsId){ if(user == null){ return RespBean.error(RespBeanEnum.SESSION_ERROR); } Long orderId = seckillOrderService.getResult(user,goodsId); return RespBean.success(orderId); }

      • Service层创建接口并实现(相关自动加载注解就不写了)

      java @Override public Long getResult(User user, Long goodsId){ SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder)().eq("user_id", user.getId()).eq("goods_id",goodsId); //如果订单表里有这个用户下了这个产品的订单,就说明秒杀成功,返回订单编号。 if(seckillOrder != null){ return seckillOrder.getOrderId(); }else if(redisTemplate.hasKey("isStockEmpty:" + goodsId)){ //如果Redis里这个产品的isStockEmpty标记为是,就返回库存为空的标记 return -1L; }else{ //如果没有这个订单,就代表秒杀失败 return 0L; } }

      • 在生成订单的时候要加上判断库存为空

      ```java @Transactional @Override public Order seckill(User user,GoodsVo goods){ ValueOperations valueOperations = redisTemplate.opsForValue(); //秒杀商品表减库存操作...

      //判断是否还有库存
      if(seckillGoods.getStockCount() < 1){
          valueOperations.set("isStockEmpty:" + goods.getId(), "0");
          return null;
      }

      } ```

      • 接下来就是前端写轮询发送请求判断了,若返回-1就是秒杀失败不用轮询,若返回0排队中就要写设置间隔几秒发送请求到getResult接口。
  6. 脚本优化

    • 经历以上操作,超卖问题是解决了,但是存在一个问题,单个用户多次秒杀会使得redis库存多次减少并且超出限购。还会让redis库存扣完但是数据库没有,所以需要优化。(这个方法好像没有用,自己预想是用用户标记)

    • lua脚本实现分布式锁,具体作用看视频

    • image-20220405232633646.png

    • RedisConfig调用

    • image-20220405233014417.png

    • 在配置一个stock.lua判断库存

    • image-20220405234320336.png

    • RedisConfig配置

    • image-20220405234413655.png

    • Controller里判断库存修改

      java @Autowired private RedsiScript<Long> script;

    • image-20220405234718836.png

  7. JSONUtil 或者Maven添加fastJson依赖

    ```java package com.bank.seckill2022.utils;

    import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.List;

    /**

    • 功能描述:
    • @param: Json工具类
    • @return:
    • @author 来自于网络
    • @date: 2022/3/25 23:36 */

    public class JSONUtil { private static ObjectMapper objectMapper = new ObjectMapper(); /* * 将对象转换成json字符串 * * @param obj * @return */ public static String object2JsonStr(Object obj) { try { return objectMapper.writeValueAsString(obj); } catch (JsonProcessingException e) { //打印异常信息 e.printStackTrace(); } return null; } /* * 将字符串转换为对象 * * @param 泛型 / public static T jsonStr2Object(String jsonStr, Class clazz) { try { return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz); } catch (JsonParseException e) { e.printStackTrace(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } / * * 将json数据转换成pojo对象list *

    Title: jsonToList

    *

    Description:

    * * @param jsonStr * @param beanType * @return */ public static List jsonToList(String jsonStr, Class beanType) { JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType); try { List list = objectMapper.readValue(jsonStr, javaType); return list; } catch (Exception e) { e.printStackTrace(); } return null; } }

    ```

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值