Bootstrap

【学习笔记】seckill-秒杀项目--(9)接口优化

一、redis

通过redis预减库存,来减少数据库访问。
可以在初始化阶段,将商品库存加入到redis中。后续直接在redis中进行预减库存操作。后续的下单可以先返回给客户端提示信息,同时将请求发送到消息队列,来实现订单的创建等操作,实现异步操作。
客户端的页面使用轮询来判断订单是否创建成功。

1.1 预减库存

两步操作

  • 项目启动时,通过实现InitializingBean里的afterPropertiesSet()方法,把系统库存加载到redis中。
  • 通过redis扣减库存,并进行判断。
  • 下单通过RabbitMQ来实现。
/**
 * 系统初始化,把商品库存数量加载到redis
 * @author 47roro
 * @date 2022/5/12
 * @param
 **/
@Override
public void afterPropertiesSet() throws Exception {
    List<GoodsVo> list = goodsService.findGoodsVo();
    if(CollectionUtils.isEmpty(list)){
        return;
    }
    list.forEach(goodsVo ->
        redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()));
}

二、内存标记

通过内存标记减少redis访问。
如果当库存减为0时,大量请求访问,还是要和redis进行通信,这样的话也会加重redis的消耗,可以通过内存来进行标记。

2.1 控制层逻辑

在系统初始化中添加内存标记。

/**
 * 系统初始化,把商品库存数量加载到redis
 * @author 47roro
 * @date 2022/5/12
 * @param
 **/
@Override
public void afterPropertiesSet() throws Exception {
    List<GoodsVo> list = goodsService.findGoodsVo();
    if(CollectionUtils.isEmpty(list)){
        return;
    }
    list.forEach(goodsVo -> {
                redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
                EmptyStockMap.put(goodsVo.getId(), false);
            }
    );
}

预减库存操作之前先判断内存标记。

/**
 * 秒杀
 * @author 47roro
 * @date 2022/4/16
 * @param model
 * @param user
 * @param goodsId
 * @return java.lang.String
 **/
@RequestMapping(value = "/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(Model model, User user, Long goodsId){
    if(user == null){
        return RespBean.error(RespBeanEnum.SESSION_ERROR);
    }
    ValueOperations valueOperations = redisTemplate.opsForValue();
    //判断是否重复抢购(mybatis plus)
    SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
    if(seckillOrder != null){
        return RespBean.error(RespBeanEnum.REPEAT_ERROR);
    }
    //内存标记减少redis访问
    if(EmptyStockMap.get(goodsId)){
        return RespBean.error(RespBeanEnum.EMPT_STOCK);
    }
    //预减库存
    Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
    if(stock < 0){
        EmptyStockMap.put(goodsId, true);
        valueOperations.increment("seckillGoods:" + goodsId);
        return RespBean.error(RespBeanEnum.EMPT_STOCK);
    }

    SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
    mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
    return RespBean.success(0);
}

三、消息队列

请求进入消息队列,实现异步下单。

3.1 RabbitMQ安装

首先将erlang以及RabbitMQ安装包传到虚拟机中,注意两个版本需要对应。
在这里插入图片描述
执行安装。 yum -y install esl-erlang_23.0.2-1_centos_7_amd64.rpm
安装完成后,查看erl,输出以下内容即安装成功。
在这里插入图片描述
然后安装rabbitMQ。yum -y install rabbitmq-server-3.8.5-1.el7.noarch.rpm
安装完成后,还要安装一个可视化的管理控制台,以插件形式存在。
rabbitmq-plugins enable rabbitmq_management.
安装完成后,启动rabbitmq systemctl start rabbitmq-server.service
rabbitmq默认端口号:15672.

3.2 rabbitmq远程登录

进入目录cd /etc/rabbitmq
添加配置文件vim rabbitmq.config,在配置文件中添加配置:[{rabbit,[{loopback_users,[]}]}].
最后的这个.不可缺少。
重新启动rabbitmq服务。
重启成功后,在网页上输入网址,用默认的用户名密码登录。可成功登录。
用户名:guest
密码:guest
在这里插入图片描述

3.3 SpringBoot继承RabbitMQ

在pom中添加依赖

<!--AMQP依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在application中进行配置。这里的端口应该是5672。因为15672是可视化界面的登录端口。实际的服务端口在5672

spring:
    # RabbitMQ
    rabbitmq:
        host: 192.168.222.129
        username: guest
        password: guest
        virtual-host: /
        port: 5672
        listener:
            simple:
                # 消费者最小数量
                concurrency: 10
                # 消费者最大数量
                max-concurrency: 10
                # 限制消费者每次处理消息的数量
                prefetch: 1
        template:
          retry:
              # 发布重试
              enabled: true

3.3.1添加配置类

/**
 * @author 47roro
 * @create 2022/5/12
 * @description: RabbitMQ配置类
 */
@Configuration
public class RabbitMQConfig {
	//seckill
    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.#");
    }
    
}

3.3.2 准备消息发送者和消费者

/**
 * @author 47roro
 * @create 2022/5/12
 * @description: 消息发送者
 */
@Service
@Slf4j
public class MQSender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    //发送秒杀信息
    public void sendSeckillMessage(Message msg){
        log.info("发送消息:" + msg);
        rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", msg);
    }
}

/**
 * @author 47roro
 * @create 2022/5/12
 * @description: 消息消费者
 */
@Service
@Slf4j
public class MQReceiver {
    @Autowired
    private IGoodsService goodsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private IOrderService orderService;

    /**
     * 下单操作
     * @author 47roro
     * @date 2022/5/12
     * @param msg
     **/
    @RabbitListener(queues = "seckillQueue")
    public void receive(String msg){
        log.info("接收消息:" + msg);
        SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(msg, SeckillMessage.class);
        Long goodsId = seckillMessage.getGoodId();
        User user = seckillMessage.getUser();
        //判断库存
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        if(goodsVo.getStockCount() < 1){
            return;
        }
        //判断是否重复抢购(mybatis plus)
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if(seckillOrder != null){
            return;
        }
        //下单操作
        Order order = orderService.seckill(user, goodsVo);

    }
}

3.3.3 控制层逻辑

即内存标记中的代码。

四、 下单结果

之前是返回order对象,成功后直接跳转订单详情。现在前面下单返回的是return RespBean.success(0);,只是显示排队中,客户无法知道下单是否成功。所以现在要修改前端页面来实现查询下单结果。

4.1 静态页面修改

function doSeckill(){
    $.ajax({
        url:'/seckill/doSeckill',
        type:'POST',
        data:{
            goodsId:$("#goodsId").val()
        },
        success:function(data){
            if(data.code == 200){
                //window.location.href="/orderDetail.htm?orderId="+data.obj.id;
                getResult($("#goodsId").val());
            }else{
                layer.msg(data.message);
            }
        },
        error:function(){
            layer.msg("客户端请求出错");
        }
    })
}
function getResult(goodsId){
    g_showLoading();
    $.ajax({
        url:"/seckill/result",
        type:"GET",
        data:{
            goodsId:goodsId,
        },
        success:function (data){
            if(data.code==200){
                var result = data.obj;
                if(result < 0){
                    layer.msg("秒杀失败");
                } else if(result == 0){
                    setTimeout(function(){
                        getResult(goodsId);
                    }, 50);
                } else {
                    layer.confirm("秒杀成功,是否查看订单?", {btn:["确定","取消"]},
                    function(){
                        window.location.herf="/orderDetail.htm?orderId=" + result;
                    },
                    function(){
                        layer.close();
                    })
                }
            }
        },
        error:function(){
            layer.msg("客户端请求错误");
        }
    })
}

4.2 控制器修改

/**
 * 获取秒杀结果
 * @author 47roro
 * @date 2022/5/12
 * @param user
 * @param goodsId
 * @return com.example.seckill.vo.RespBean
 * orderId: 成功; -1:秒杀失败;  0:排队中
 **/
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
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);
}

4.3 服务层修改

@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
 * 获取秒杀结果
 * @author 47roro
 * @date 2022/5/12
 * @param user
 * @param goodsId
 * @return java.lang.Long
 * orderId: 成功; -1:秒杀失败;  0:排队中
 **/
@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)){
        return -1L;
    } else {
        return 0L;
    }
}

四、结果测试

在这里插入图片描述
在这里插入图片描述
redis缓存
在这里插入图片描述

数据库记录
在这里插入图片描述

五、压力测试

订单详情,正好10条:
在这里插入图片描述

之前缓存的QPS大约为460
可以看到优化后的QPS约为580.
在这里插入图片描述

;