Java高性能高并发实战之Rabbitmq接口优化(六)

前言

这是系列的第六篇文章接口优化设计首先来回顾一下之前的几篇文章:

章节名称 博客地址
安装部署Redis 集成Redis(已完结)
页面登陆功能设计 登录功能设计(更新优化中)
秒杀页面具体设计 秒杀详情页(已完结)
JMeter初级压测学习 Jmeter压测入门学习(已完结)
页面优化设计 页面优化设计(已完结)
接口优化 RabbbitMq接口优化(已完结)
图形验证码等 图形验证码及接口防刷(更新优化中)

在前面的第五篇介绍到了页面优化技术,但是对于秒杀来说仅仅在前端做一些缓存的处理和页面静态化就想解决高并发访问的问题未免太过于天真了,并且对于一个前后端分离的项目来说,就更需要后端的一些技术支持。这一节就来具体介绍一些关于接口优化的方案。具体代码参见我的个人github源码

对于接口的优化处理主要有以下的几个部分:

  1. Redis预减库存来减少数据库访问。
  2. 内存标记来介绍Redis访问。
  3. 将请求先行放入到缓冲队列中,异步下单。
  4. nginx水平扩展。

思路

  1. 首先在系统初始化时候,就把商品的数量加载到Redis里面。进行提前的加载操作。
  2. 收到请求以后 Redis预减库存,就是reids先对库存进行一个减操作,库存不足的时候直接就回返回错误信息。
    思考会有什么好处:例如我们就有100件商品,此时数据的加载时候就会预先加载到redis中,前端对数据进行请求,完成了100件订单以后,对于后续的订单就算是来再多也会显示没有了库存就不会请求到数据库,此时对数据库的访问几乎是零。
  3. 对于库存充足的情况下就会进入到第三步,将请求的信息入队(rabbitmq队列)。立即返回的是排队中 。不是返回成功或者失败,因为是入队但是还不能判断是否成功。
  4. 客户端会进行轮询操作,自己的这次请求是成功还是失败了 。对于服务端来说:将请求出队(就是放在队列中待执行的请求进行执行操作)生成订单,减少库存。

Rabbitmq安装与整合

  1. 首先在自己的电脑上安装Rabbitmq并进行启动,这里若是没有安装的小伙伴可以参看下面这篇文章,不仅有详细的安装方法,还附带百度云盘供大家下载对应的版本下载与安装
  2. 完成安装以后启动起来,既然是和Spring Boot进行整合操作急需要添加依赖,添加如下依赖在自己的pom.xml文件中
<dependency>  
		<groupId>org.springframework.boot</groupId>  
		<artifactId>spring-boot-starter-amqp</artifactId>  
	</dependency>  
  1. 添加配置信息,这里的配置文件是properties如下:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
# 因为我们的客户端在连接本地的rabbitmq时候并不是真正意义上连接到服务器上,对于服务器来说也会虚拟出很多的服务器,供不同的客户端建立连接,每个虚拟的服务器之间都是相互独立的,默认就是一个斜杠来访问。
#\u6D88\u8D39\u8005\u6570\u91CF
spring.rabbitmq.listener.simple.concurrency= 10
# 消费者数量,定义是1的时候 表示就一个消费者,此时就是串行化执行。
spring.rabbitmq.listener.simple.max-concurrency= 10
#\u6D88\u8D39\u8005\u6BCF\u6B21\u4ECE\u961F\u5217\u83B7\u53D6\u7684\u6D88\u606F\u6570\u91CF
spring.rabbitmq.listener.simple.prefetch= 1
# 每次取出来几个从队列中。也可以设置为多个,势必会增加处理的效率,但是若是长时间不消费(不进行处理)也是会产生问题,所以这里综合考量 选择处置数量为1,可以在秒杀业务中对数据进行快速的处理。
#\u6D88\u8D39\u8005\u81EA\u52A8\u542F\u52A8
spring.rabbitmq.listener.simple.auto-startup=true
# morn开启
#\u6D88\u8D39\u5931\u8D25\uFF0C\u81EA\u52A8\u91CD\u65B0\u5165\u961F
spring.rabbitmq.listener.simple.default-requeue-rejected= true
# 消费者消费失败以后,会重新将数据压入到队列中
#\u542F\u7528\u53D1\u9001\u91CD\u8BD5
spring.rabbitmq.template.retry.enabled=true 
# 重试确定
spring.rabbitmq.template.retry.initial-interval=1000 
# 一秒
spring.rabbitmq.template.retry.max-attempts=3
# 最多重试次数
spring.rabbitmq.template.retry.max-interval=10000
# 最大的间隔是10s
spring.rabbitmq.template.retry.multiplier=1.0
# 以上只是对当前项目所需进行一个简单配置文件配置有具体需求小伙伴可以查看官网配置

若是yml配置文件如下,具体的每一个字段的含义在上面的配置中有写到:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        concurrency: 10
        max-concurrency: 10
        prefetch: 1
        auto-startup: true
        default-requeue-rejected: true
    template:
      retry:
        enabled: true
        initial-interval: 1000
        max-attempts: 3
        max-interval: 10000
        multiplier: 1.0

Rabbitmq交换机

对于Rabbitmq的交换机来说具体的几种模式这里我也是学习别人,这里贴出别人大佬的地址Rabbitmq交换机学习这里就不进行具体的讲解了,下面开始具体部分实例学习:

Direct 模式

对于direct模式也是最简单的模式,就是先行创建一个队列。生产者发送消息,消费者通过队列的名称与生产者建立关联。
首先我们来创建一个config配置文件:

@Configuration
public class MQConfig {
public  static  final  String QUEUE="queue";
    @Bean
    public Queue queue(){
        return  new Queue(QUEUE,true);
    }
}

然后创建生产者,我们已经成功的进行了pom依赖的引入和连接到本地的rabbitmq服务器上,就可以使用到AmqpTemplate使用注入的方式。

@Service
public class MQSend {
    private  static Logger logger= LoggerFactory.getLogger(MQConfig.class);
    // 控制台进行打印
    @Autowired
    AmqpTemplate amqpTemplate;
    public void send (Object message){
        String msg= RedisService.beanToString(message);
        // redis的将对象类型转为string类型
        logger.info("send Message:"+msg);
        amqpTemplate.convertAndSend(MQConfig.QUEUE,msg);
        // 调用方法,提供队列名称,和想要发送的消息。
    }
}

生成者创建完成创建消费者:


@Service
public class MQReceiver {

    private  static Logger logger= LoggerFactory.getLogger(MQConfig.class);
    //控制台打印
    @RabbitListener(queues = MQConfig.QUEUE)
    // 添加监听的注解,表示监听这个队列
    public  void receiver(String message){
        logger.info("receive Message:"+message);
    }
}

以上完成以后我们在Controller中进行调用

//注入调用
@Autowired
    MQSend mqSend;

    @RequestMapping("/mq")
    @ResponseBody
    public Result<String> mq() {
        mqSend.send("hello world");
        return Result.success("Hello world");
    }

测试

如下图所示,访问此地址,就可以调用我们的的send()方法:
在这里插入图片描述
因为我们在控制台打印了输出信息,进行查看:发现对于生成者发送的消息对于接收端也真的进行到了接受并正确打印出信息。
在这里插入图片描述

Topic 模式

对于Topic模式还是和Direct模式有部分的区别:

  1. 首先我们还是创建两个queue
  2. 对于Topic模式需要创建一个交换机。
  3. 利用key值将交换机和Queue进行一个绑定。
   @Bean
    public Queue topicQueue1(){
        return  new Queue(TOPIC_QUEUE1,true);
    }
    @Bean
    public Queue topicQueue2(){
        return  new Queue(TOPIC_QUEUE2,true);
    }
    // 创建交换机
    @Bean
    public TopicExchange topicExchange(){
        return new  TopicExchange(TOPIC_EXCHANGE);
    }
    // 创建一个绑定,将队列一与交换机进行一个绑定。key值是 topic.key1。
    @Bean
    public Binding topicBind1(){
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.key1");
    }
    // 创建第二个绑定,将队列二与交换机进行一个绑定 key值是topic.# 
    //# 表示通配符匹配,对于任意的topic.XX 都可以进行一个匹配。
    @Bean
    public Binding topicBind2(){
        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.#");
    }

在完成了config配置之后我们来看一下如何发送:如下代码所示,我们发送两条消息对于第一个key值来说可以匹配到topic.key1和topic.#(因为对于 topic.#来说可以通配匹配),但是对于第二个key:topic.key2不能够成功匹配topic.key1,却能够匹配 topic.#

/**
     * Topic 
     * @param message
     */
    public void sendTopic (Object message){
        String msg= RedisService.beanToString(message);
        logger.info("send Message:"+msg);
        amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE,"topic.key1",msg+"1");
        amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE,"topic.key2",msg+"2");

    }

下面来看接收者

@Service
public class MQReceiver {

     private  static Logger logger= LoggerFactory.getLogger(MQConfig.class);
//    //控制台打印
//    @RabbitListener(queues = MQConfig.QUEUE)
//    // 添加监听的注解,表示监听这个队列
//    public  void receiver(String message){
//        logger.info("receive Message:"+message);
//    }

    @RabbitListener(queues = MQConfig.TOPIC_QUEUE1)
    // 添加监听的注解,表示监听这个队列
    public  void receiveTopic1(String message){
        logger.info("receive QUeue1:"+message);
    }
    @RabbitListener(queues = MQConfig.TOPIC_QUEUE2)
    public  void receiveTopic2(String message){
        logger.info("receive Queue2:"+message);
    }
}

完成以后我们就可以在前端访问发起一个请求可以看到后端控制台的输出情况:

 @RequestMapping("/mq/topic")
    @ResponseBody
    public Result<String> mqTopic() {
        mqSend.sendTopic("hello world");
        return Result.success("Hello,world");
    }

测试

我们先思考一下,我们同Direct模式类似,先行创建Queue(注意创建时候选择import org.springframework.amqp.core.Queue;),然后创建一个交换机,通过创建两个key值将queue和交换机进行一个绑定。对于第一个key来说只能接受特定key值,但是对于第二个key来说就可以进行通配符匹配进行接收处理,发送端发送,接收端接受,前端进行一个调度访问(这里使用main函数也可以进行一个测试)。
如下图所示:应该出现三条接收的显示,这里出现四条,暂时还没有搞明白。先进行一个讲解,后续再思考为什么出现这个问题
对于发送端:表示发给这个交换机,key值是topic.key1,然后是后缀的信息。

 amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE,"topic.key1",msg+"1");
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE,"topic.z",msg+"2");

对于配置部分:发现两个队列都有绑定到这个交换机,并且对于第一个key是对应的对于第二个key是通配符匹配也会进行一个接收处理。

@Bean
  public Binding topicBind1(){
      return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.key1");
  }
  @Bean
  public Binding topicBind2(){
      return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.#");
  }

最后是接收端:由于对两个队列都进行了一个监听处理,所以会打印出两条消息。表示这两个队列都有收到消息。

 @RabbitListener(queues = MQConfig.TOPIC_QUEUE1)
    // 添加监听的注解,表示监听这个队列
    public  void receiveTopic1(String message){
        logger.info("receive QUeue1:"+message);
    }
    @RabbitListener(queues = MQConfig.TOPIC_QUEUE2)
    public  void receiveTopic2(String message){
        logger.info("receive Queue2:"+message);
    }

在这里插入图片描述

Fanout 模式(广播)

对于广播模式顾名思义,我们创建两个queue,再创建一个fanout类型的交换机,将交换机同queue进行一个绑定处理。因为是广播模式所以就不再需要key值。
对于config配置如下,这里借用上一步创建的两个queue

 /**
     * fanout 模式 借用前面创建的队列。
     * @return
     */
    @Bean
    public Queue topicQueue1(){
        return  new Queue(TOPIC_QUEUE1,true);
    }
    @Bean
    public Queue topicQueue2(){
        return  new Queue(TOPIC_QUEUE2,true);
    }
    // 创建fanout类型的交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange(FANOUT_EXCHANGE);
    }
    // 将交换机和队列进行一个绑定处理
    @Bean
    public Binding fanoutBind1(){
        return BindingBuilder.bind(topicQueue1()).to(fanoutExchange());
    }
    @Bean
    public Binding fanoutBind2(){
        return BindingBuilder.bind(topicQueue2()).to(fanoutExchange());
    }

配置完成config以后来看发送端,调用的是FANOUT_EXCHANGE

public void fanout (Object message){
        String msg= RedisService.beanToString(message);
        logger.info("send Message:"+msg);
        amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE,"",msg);
    }

来看接收端,因为使用到的Queue是相同的,所以这里通topic模式相比没有变化:

 @RabbitListener(queues = MQConfig.TOPIC_QUEUE1)
  // 添加监听的注解,表示监听这个队列
  public  void receiveTopic1(String message){
      logger.info("receive QUeue1:"+message);
  }
  @RabbitListener(queues = MQConfig.TOPIC_QUEUE2)
  public  void receiveTopic2(String message){
      logger.info("receive Queue2:"+message);
 }

测试

完成了配置以后,就可以模拟访问,查看发送和接受的打印情况:这里采用广播的模式,向两个queue都发送了消息,所以应该是发送一条消息,对于两个queue都能够收到消息。
在这里插入图片描述

Header 模式

对于Header模式可能和前几种模式还是有一定的区别的,是一种key和value模式:
首先来看以下config下的配置:

 /**
     * Header模式 交换机Exchange
     * date 2020.5.6
     * */
    public static final String HEADER_QUEUE = "header.queue";
    public static final String HEADERS_EXCHANGE = "headersExchage";
    @Bean
    public HeadersExchange headersExchage(){
        return new HeadersExchange(HEADERS_EXCHANGE);
    }
    @Bean
    public Queue headerQueue1() {
        return new Queue(HEADER_QUEUE, true);
    }
    @Bean
    public Binding headerBinding() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("header1", "value1");
        map.put("header2", "value2");
        return BindingBuilder.bind(headerQueue1()).to(headersExchage()).whereAll(map).match();
    }

然后我们编写生产者,具体其中的properties.serHeader()要和map.put()中的内容进行对应。这里暂时先进行一个测试用例的编写,具体其中的缘由会在后面的文章中进行一个具体详细的介绍。

   public void sendHeader(Object message) {
        String msg = RedisService.beanToString(message);
        logger.info("send header message:"+msg);
        MessageProperties properties = new MessageProperties();
        properties.setHeader("header1", "value1");
        properties.setHeader("header2", "value2");
        Message obj = new Message(msg.getBytes(), properties);
        amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", obj);
    }

接收者,对于发送者发送的是字节数组,所以对于接收者也应该进行对应的接受字节数组类型。

@RabbitListener(queues=MQConfig.HEADER_QUEUE)
 public void receiveHeaderQueue(byte[] message) {
     logger.info(" header  queue message:"+new String(message));
 }

最后我们编写测试用例:

测试

 @RequestMapping("/mq/header")
   @ResponseBody
   public Result<String> header() {
       mqSend.sendHeader("hello world");
       return Result.success("Hello,world");
   }

发现成功进行发送与接受:
在这里插入图片描述
以上就是对rabbitmq的四种交换机模式进行了一个讲解,只是进行了栗子的讲解,来演示具体的工作流程,其中的原理部分没有过多进行一个展开(后续会进行一个讲解,敬请关注)

秒杀接口优化

上面部分我们实现了对于rabbitmq的安装,集成和四种交换机的基础学习,下面就可以依据我们学习的内容对之前的秒杀接口进行一个优化处理。在此之前我们为了能够减少数据库的访问有设计以下页面的优化技术,详情可以查看第五部分的页面优化技术。

首先我们来看一下之前的秒杀接口是如何实现的:

  1. 首先判断用户是否已经登录,防止用户跳过登录直接获取到秒杀的url进行恶意秒杀操作。
  2. 用户成功登录以后,判断库存是否还充足(是否满足当前的秒杀订单)。充足跳到第三步,不充足直接返回错误信息。
  3. 判断用户是否秒杀过此商品,因为秒杀成功有一个秒杀订单,可以根据用户id和商品id进行查询。
  4. 以上都执行完成以后就开始秒杀,首先是减库存,然后写入订单和秒杀订单。

具体的思路见文章开始思路部分详解介绍
以下内容有点长,且看且珍惜

redis预先加载库存

对于这部分思考一下对于不同的商品需要加载到redis中,加载的同时需要加载商品的数量,所以需要先查询出来商品,这个操作是在程序运行的时候进行的就需要将Controller函数继承InitializingBean然后再度实现afterPropertiesSet()一个方法即可(具体的栗子大家可以自行百度查看)。在方法体里面需要加载商品的id和数量,就需要我们用到之前封装的Redis设置一个key值,为永不过期,之前有设置页面缓存过期时间为一分钟(这里不再展开)。这样就可以将id和库存的数量加载到redis中。
GoodsKey

public class GoodsKey extends BasePrefix{
	private GoodsKey(int expireSeconds, String prefix) {
		super(expireSeconds, prefix);
	}
	// 前两行是商品的列表和商品的详细信息。
	public static GoodsKey getGoodsList = new GoodsKey(60, "gl");
	public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
	// 表示商品永无过期,就是加载到reids中会一直存在,除非手动redis数据库中删除。
	public static GoodsKey miaoshaGoodsStock = new GoodsKey(0, "");
}

剩下就是具体实现afterPropertiesSet()方法。

@Override
	public void afterPropertiesSet() throws Exception {
	// 最开始还是会查询数据库
		List<GoodsVo> goodsVoList=goodsService.listGoodsVo();
		if(goodsVoList==null)
			return;
		for(GoodsVo goodsVo: goodsVoList){
		// foreach循环将每个商品和数量信息都保存到reids中
	redisService.set(GoodsKey.miaoshaGoodsStock,""+goodsVo.getId(),goodsVo.getStockCount());
		}
	}

redis预减库存等一系列操作

既然是进行接口的优化处理,这里我们先来查看以下原来的工作逻辑是什么,然后思考有哪些地方可以进行一个改动的处理:

  1. 预减库存操作,我们之前是直接进入到数据库中判断库存,这里既然已经将数据预先加载到redis中,那么就可以使用到我们封装的redis进行对应的数据的预减操作。
  2. 对于是否已经秒杀,我们没有加入到redis中,这里就还是进行一个数据库的查询。不会进行更改操作。
  3. 对于减库存,下订单操作,我们前面讲过,这里会使用到我们的rabbitmq将请求入队先,然后再度进行后续的操作(这里我们可以参见对于有时候我们的购票时候,点击购票并不会立马成功,而是会锁定座位,然后会等到一小段时间,再度出票原理一样
   @RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
    @ResponseBody
    public Result<OrderInfo> miaosha(Model model,MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	if(user == null) {
    		return Result.error(CodeMsg.SESSION_ERROR);
    	}
    	//判断库存
    	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10个商品,req1 req2
    	int stock = goods.getStockCount();
    	if(stock <= 0) {
    		return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//判断是否已经秒杀到了
    	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    	if(order != null) {
    		return Result.error(CodeMsg.REPEATE_MIAOSHA);
    	}
    	//减库存 下订单 写入秒杀订单
    	OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
        return Result.success(orderInfo);
    }

在有了以上的分析以后我们更改后的代码如下,进行自己的学习和对比操作可以查看miaosha_5miaosha_6对比两个方法。

 @RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
    @ResponseBody
    public Result<Integer> miaosha(Model model,MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId) {
    	model.addAttribute("user", user);
    	if(user == null) {
    		return Result.error(CodeMsg.SESSION_ERROR);
    	}
    	// 预减库存
		long stock=redisService.decr(GoodsKey.miaoshaGoodsStock,""+goodsId);
    	if(stock<0)
			return Result.error(CodeMsg.MIAO_SHA_OVER);

		//判断是否已经秒杀到了
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
		if(order != null) {
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//入队 因为订单就需要用户的id和商品的id,这里先自行定义一个对象,进行一个分装的处理。
		// 后续将Object转换为String类型
		MiaoshaMessage miaoshaMessage=new MiaoshaMessage();
		miaoshaMessage.setUser(user);
		miaoshaMessage.setGoodsId(goodsId);
		mqSend.sendMiaoshaMessage(miaoshaMessage);
		return Result.success(0);
		//注意这里,code为0表示排队中,因为确实这里是要进入队列的。
    }

这里就使用到我们前面讲的几种交换机的模式,这里我们就使用最简单的direct模式:

@Service
public class MQSend {
   private  static Logger logger= LoggerFactory.getLogger(MQConfig.class);
   // 控制台进行打印
   @Autowired
   AmqpTemplate amqpTemplate;

   public void sendMiaoshaMessage(MiaoshaMessage miaoshaMessage) {
       String msg= RedisService.beanToString(miaoshaMessage);
       logger.info("send Message:"+msg);
       // Queue 还用之前的定义好的
       amqpTemplate.convertAndSend(MQConfig.QUEUE,msg);
   }
 }

以上就是说此用户成功预减了库存,并且没有秒杀过,就进入到队列中,后端消费者取出队列中的信息,就可以开始数据库层面的库存的减少,和订单的生产,这部分都可以在receive中进行处理。

@RabbitListener(queues = MQConfig.QUEUE)
   // 添加监听的注解,表示监听这个队列
   public  void receiver(String message){
       logger.info("receive Message:"+message);
      MiaoshaMessage miaoshaMessage=RedisService.stringToBean(message,MiaoshaMessage.class);
       MiaoshaUser user=miaoshaMessage.getUser();
       long goodsId=miaoshaMessage.getGoodsId();
       // 判断商品是否还有
       GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10个商品,req1 req2
       int stock = goods.getStockCount();
       if(stock <= 0) {
           return;
       }
       //判断是否已经秒杀到了
       MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
       if(order != null) {
           return;
       }
       // 秒杀订单
       miaoshaService.miaosha(user, goods);
   }

关于订单的生成部分也做了一定的优化处理,我们先来看之前的设计,是有一些小的问题所在的:这里直接就进行了商品的减库存,和订单的生成,但是我们若是在减库存中出现了问题,这里没有进行一个解决呢。

  1. 这里就考量到了日常的编程习惯,对于数据库的操作我们在insert一般都会返回一个int类型的值,表示插入是否成功。对于修改操作也是返回一个返回一个int类型,然后判断此int是否是大于零的来返回一个boolean类型表示是否成功执行。
  2. 对于商品已经销售完成我们也可以进行一个判断是否卖完的操作,减少数据库的访问。
	@Transactional
	public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
		//减库存 下订单 写入秒杀订单
		goodsService.reduceStock(goods);
		//order_info maiosha_order
		return orderService.createOrder(user, goods);
	}

所以修改后的代码如下:

@Transactional
	public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
		//减库存 下订单 写入秒杀订单
		boolean res =goodsService.reduceStock(goods);
		//如果成功,表示减库存成功,写入订单。
		if(res)
		return orderService.createOrder(user, goods);
		else
		{
			// 不成功  设置此商品已经销售完成。
			setGoodsOver(goods.getId());
			return  null;
		}
	}
	private  void setGoodsOver(long goodsid){
		redisService.set(MiaoshaKey.isover,""+goodsid,true);
	}

秒杀结果前端设计

以上我们只是完成了一小部分:
首先是预加载,然后预减,判断是否已经秒杀,进入到队列中,队列执行正在的减库存操作,生成订单。
下面来看一下前端的操作,既然后端有了更改前端也是要相应进行一个修改:
在修改之前我们还是看之前的操作是如何进行的,一个函数接受请求url,传参id,加载到具体的页面;
1.之前我们的code是0表示成功,现在我们的0表示是成功入队表示排队中,所以为0时候我们要轮询服务端,看是否处理已经成功。
2. 我们可以再写一个函数来表示秒杀的结果来根据结果判断情况,因为再轮询中也是会有多种的情况出现。

function doMiaosha(){
	$.ajax({
		url:"/miaosha/do_miaosha",
		type:"POST",
		data:{
			goodsId:$("#goodsId").val(),
		},
		success:function(data){
			if(data.code == 0){
				//window.location.href="/order_detail.htm?orderId="+data.data.id;
				//将其修改为 getMiaoshaResult($("#goodsId").val()); 调用这个函数来写具体的内容
			}else{
				layer.msg(data.msg);
			}
		},
		error:function(){
			layer.msg("客户端请求有误");
		}
	});

}

getMiaoshaResult

function  getMiaoshaResult(goodsId) {
        g_getQueryString();
        $.ajax({
            url:"/miaosha/result",
            type:"GET",
            data:{
                goodsId:$("#goodsId").val(),
            },
            success:function(data){
                if(data.code == 0){
                  var res=data.data;
                  if(res<0){
                  //表示失败
                      layer.msg("失败")
                  }
                  else if(res==0){
                      // 继续轮询 就是自己再度调用自己。表示每50ms轮询查看一次是否已经成功。
                      setTimeout(function () {
                          getMiaoshaResult(goodsId);
                      },50);
                  }
                  else {
                  // 表示成功 这个时候我们再度使用查看详细信息。
                      layer.confirm("秒杀成功! 查看订单",{btn:["确定","取消"]},
                      function () {
                          window.location.href="/order_detail.htm?orderId="+res;
                      },
                      function () {
                          layer.closeAll();
                      });
                  }
                }else{
                    layer.msg(data.msg);
                }
            },
            error:function(){
                layer.msg("客户端请求有误");
            }
        });
    }

秒杀结果后端设计

请求的**url:"/miaosha/result"**地址


	/**
	 * 正确执行返回商品id信息
	 * 库存没有了返回 -1 表示错误信息
	 * 在排队中返回 0 
	 * @param model
	 * @param user
	 * @param goodsId
	 * @return
	 */
	@RequestMapping(value="/result", method=RequestMethod.GET)
	@ResponseBody
	public Result<Long> result(Model model,MiaoshaUser user,
								   @RequestParam("goodsId")long goodsId) {
		model.addAttribute("user", user);
		if(user == null) {
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		long orderId= miaoshaService.getMiaoshaResult(user.getId(),goodsId);
		return  Result.success(orderId);
	}

getMiaoshaResult

private  boolean getGoodsOver(long goodsid){
		return  redisService.exists(MiaoshaKey.isover,""+goodsid);
	}
	public long getMiaoshaResult(Long userid, long goodsId) {
		MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdGoodsId(userid,goodsId);
		if(order!=null)
			// 秒杀成功 获取到订单的id信息。
			return  order.getOrderId();
			/**表示不成功时候,就是还没有获取到订单的信息,有两种情况
			1. 还处在轮询中就是还没有轮到自己。
			2. 判断是否已经没有了库存。
			**/
		else {
			boolean isover=getGoodsOver(goodsId);
			if(isover){
				return  -1; // 表示已经卖完。
			}
			else {
				return  0;// 没有卖完 且没有成功 继续轮询
			}
		}
	}

测试

首先我们在本地连接好数据库和redis同时启动rabbitmq进行连接(这些配置前面都有讲述)。然后启动,在启动之前我们先看本地的Redis数据库:可以看到并没有Goodskey,我将之前的都删除。
在这里插入图片描述
启动之后查看:
在这里插入图片描述
表示成功的从数据库中预加载到redis的缓存中。
我们进行一个测试:
在这里插入图片描述
然后再来进行一个查看,发现商品的数量也相对应的进行了一个减一的操作,证明我们的逻辑和思路没有问题。
在这里插入图片描述
同时也可以查看本地的数据库的情况,会发现自己的数据库数量也会减一。
当我们在完成一次秒杀之后,再进行一个秒杀操作会出现以下问题:也验证了我们的设计没有问题。
在这里插入图片描述

后记

以上就是对之前我们简单设计的秒杀接口进行的一个优化,下面来总结以下:

  1. 首先我们思考使用到rabbitmq于是就先学习了基本的四种交换机模式,最后我们选择了较为简单的direct模式。
  2. 完成学习以后,我们实现redis的预加载,使用到继承InitializingBean然后再度实现afterPropertiesSet()方法。在测试中也有验证。
  3. 然后就是基础的逻辑判断,是否登录,是否已经秒杀,库存是否充足,以上条件满足,意味着可以进行队列中,实现异步秒杀操作。
  4. 使用到最为简单的Direct模式,先排队,然后再接收者中,再度进行真正意义上的减库存和生成订单部分。
  5. 对于结果的处理,由于前端要轮询操作,所以设计了一个秒杀结果函数,对于各种情况返回的不同情况进行判断是直接返回错误信息,还是进行继续轮询操作。
  6. 最后进行测试验证代码的完整性与正确性。
    :最后这部分的源码部分参见我的github个人仓库仓库miasha_6部分,可以去第五部分进行一个对比操作与学习,这里只是我的个人理解部分,很多地方理解的还不是很透彻,大家可以下载源码根据我的前几篇博客学习以下,最后的最后大家觉得还不错,恬不知耻地想大家给它点上一个小Star呀,谢谢了。

全文19647字

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/105923594