消息队列之RabbitMQ

1.1现存问题

服务调用:两个服务调用时,我们可以通过传统的HTTP方式,让服务A直接去调用服务B的接口,但是这种方式是同步的方式,虽然可以采用SpringBoot提供的@Async注解实现异步调用,但是这种方式无法确保请求一定会访问到服务B的接口。那如何保证服务A的请求信息一定能送达到服务B去完成一些业务操作呢? 如何实现异步调用

海量请求:在我们在做一些秒杀业务时,可能会在某个时间点突然出现大量的并发请求,这可能已经远远超过服务器的并发瓶颈,这时我们需要做一些削峰的操作,也就是将大量的请求缓冲到一个队列中,然后慢慢的消费掉。如何提供一个可以存储千万级别请求的队列呢?

在微服务架构下,可能一个业务会出现同时调用多个其他服务的场景,而且这些服务之间一般会用到Feign的方式进行轻量级的通讯,如果存在一个业务,用户创建订单成功后,还需要去给用户添加积分、通知商家、通知物流系统、扣减商品库存,而在执行这个操作时,如果任意一个服务出现了问题,都会导致整体的下单业务失败,并且会导致给用户反馈的时间延长。这时就造成了服务之间存在一个较高的耦合性的问题。如何可以降低服务之间的耦合性呢?

1.2处理问题

RabbitMQ就可以解决上述的全部问题

服务之间如何想实现可靠的异步调用,可以通过RabbitMQ的方式实现,服务A只需要保证可以把消息发送到RabbitMQ的队列中,服务B就一定会消费到队列中的消息只不过会存在一定的延时。| 异步访问

忽然的海量请求可以存储在RabbitMQ的队列中,然后由消费者慢慢消费掉,RabbitMQ的队列本身就可以存储上千万条消息

在调用其他服务时,如果允许延迟效果的出现,可以将消息发送到RabbitMQ中,再由消费者慢慢消费| 服务解耦

1.3 RabbitMQ介绍

百度百科:

RabbitMQ 是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

首先RabbitMQ基于AMQP协议开发,所以很多基于AMQP协议的功能RabbitMQ都是支持的,比如SpringCloud中的消息总线bus

其次RabbitMQ是基于Erlang编写,这是也是RabbitMQ天生的优势,Erlang被称为面向并发编程的语言,并发能力极强,在众多的MQ中,RabbitMQ的延迟特别低,在微秒级别,所以一般的业务处理RabbitMQ比Kafka和RocketMQ更有优势。

最后RabbitMQ提供自带了图形化界面,操作方便,还自带了多种集群模式,可以保证RabbitMQ的高可用,并且SpringBoot默认就整合RabbitMQ,使用简单方便。

二、RabbitMQ安装

2.1 安装RabbitMQ

这里推荐搭建采用Docker的方式在Linux中安装RabbitMQ,如果对Docker不了解,推荐去学习一下Docker的应用,不然学习其他的知识时,安装的成本都特别高,这里我们就采用Docker的方式安装RabbitMQ。

直接使用docker-compose.yml文件即可安装RabbitMQ服务

yml 复制代码
version: '3.1'
services:
  rabbitmq:
    restart: always
    image: daocloud.io/library/rabbitmq:3.8.8
    volumes:
      - ./data/:/var/lib/rabbitmq/
      - ./log/:/var/log/rabbitmq/log/
    ports:
      - 15672:15672
      - 5672:5672

执行 docker-compose up -d运行

测试效果:curl localhost:5672

查看效果

2.2 开启图形化界面

默认情况下,当前镜像的图形化界面默认没有开启,需要进入到容器内部开启图形化管理界面

启动图形化界面插件

通过浏览器访问15672,查看图形化界面

查看登录页面

默认用户和密码均为:guest,查看首页

查看首页

三、RabbitMQ构架

RabbitMQ的架构可以查看官方地址:rabbitmq.com/tutorials/a...

官方简单架构

可以看出RabbitMQ中主要分为三个角色:

  • Publisher:消息的发布者,将消息发布到RabbitMQ中的Exchange
  • RabbitMQ服务:Exchange接收Publisher的消息,并且根据Routes策略将消息转发到Queue中
  • Consumer:消息的消费者,监听Queue中的消息并进行消费

官方提供的架构图相对简洁,我们可以自己画一份相对完整一些的架构图:

RabbitMQ架构图
可以看出Publisher和Consumer都是单独和RabbitMQ服务中某一个Virtual Host建立Connection的客户端

后续通过Connection可以构建Channel通道,用来发布、接收消息

一个Virtual Host中可以有多个Exchange和Queue,Exchange可以同时绑定多个Queue

在基于架构图查看图形化界面,会更加清晰

图形化界面信息

四、RabbitMQ通讯方式


RabbitMQ提供了很多种通讯方式,依然可以去官方查看:rabbitmq.com/getstarted....

七种通讯方式

4.1 RabbitMQ提供的通讯方式

4.2 构建Connection工具类

  • 导入依赖:amqp-client,junit

    xml 复制代码
    <dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.9.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
  • 构建工具类:

    java 复制代码
    package com.mashibing.util;
    
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author zjw
     * @description
     */
    public class RabbitMQConnectionUtil {
    
        public static final String RABBITMQ_HOST = "192.168.11.32";
    
        public static final int RABBITMQ_PORT = 5672;
    
        public static final String RABBITMQ_USERNAME = "guest";
    
        public static final String RABBITMQ_PASSWORD = "guest";
    
        public static final String RABBITMQ_VIRTUAL_HOST = "/";
    
        /**
         * 构建RabbitMQ的连接对象
         * @return
         */
        public static Connection getConnection() throws Exception {
            //1. 创建Connection工厂
            ConnectionFactory factory = new ConnectionFactory();
    
            //2. 设置RabbitMQ的连接信息
            factory.setHost(RABBITMQ_HOST);
            factory.setPort(RABBITMQ_PORT);
            factory.setUsername(RABBITMQ_USERNAME);
            factory.setPassword(RABBITMQ_PASSWORD);
            factory.setVirtualHost(RABBITMQ_VIRTUAL_HOST);
    
            //3. 返回连接对象
            Connection connection = factory.newConnection();
            return connection;
        }
    
    }

4.3 Hello World

通讯方式

生产者:

java 复制代码
package com.mashibing.helloworld;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;

/**
 * @author zjw
 * @description
 * @date 2022/1/24 22:54
 */
public class Publisher {

    public static final String QUEUE_NAME = "hello";

    @Test
    public void publish() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //4. 发布消息
        String message = "Hello World!";
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("消息发送成功!");
    }
}

消费者:

java 复制代码
package com.mashibing.helloworld;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;

import java.io.IOException;

/**
 * @author zjw
 * @description
 * @date 2022/1/24 23:02
 */
public class Consumer {

    @Test
    public void consume() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建队列
        channel.queueDeclare(Publisher.QUEUE_NAME,false,false,false,null);

        //4. 监听消息
        DefaultConsumer callback = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者获取到消息:" + new String(body,"UTF-8"));
            }
        };
        channel.basicConsume(Publisher.QUEUE_NAME,true,callback);
        System.out.println("开始监听队列");

        System.in.read();
    }
}

4.4 Work Queues

WorkQueues需要学习的内容
  • 生产者:生产者和Hello World的形式是一样的,都是将消息推送到默认交换机。
  • 消费者:让消费者关闭自动ack,并且设置消息的流控,最终实现消费者可以尽可能去多消费消息
java 复制代码
 package com.mashibing.workqueues;

 import com.mashibing.util.RabbitMQConnectionUtil;
 import com.rabbitmq.client.*;
 import org.junit.Test;

 import java.io.IOException;

 /**
  * @author zjw
  * @description
  * @date 2022/1/25 19:52
  */
 public class Consumer {

     @Test
     public void consume1() throws Exception {
         //1. 获取连接对象
         Connection connection = RabbitMQConnectionUtil.getConnection();

         //2. 构建Channel
         Channel channel = connection.createChannel();

         //3. 构建队列
         channel.queueDeclare(Publisher.QUEUE_NAME,false,false,false,null);

         //3.5 设置消息的流控
         channel.basicQos(3);

         //4. 监听消息
         DefaultConsumer callback = new DefaultConsumer(channel){
             @Override
             public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                 try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("消费者1号-获取到消息:" + new String(body,"UTF-8"));
                 channel.basicAck(envelope.getDeliveryTag(),false);
             }
         };
         channel.basicConsume(Publisher.QUEUE_NAME,false,callback);
         System.out.println("开始监听队列");

         System.in.read();
     }

     @Test
     public void consume2() throws Exception {
         //1. 获取连接对象
         Connection connection = RabbitMQConnectionUtil.getConnection();

         //2. 构建Channel
         Channel channel = connection.createChannel();

         //3. 构建队列
         channel.queueDeclare(Publisher.QUEUE_NAME,false,false,false,null);

         channel.basicQos(3);

         //4. 监听消息
         DefaultConsumer callback = new DefaultConsumer(channel){
             @Override
             public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("消费者2号-获取到消息:" + new String(body,"UTF-8"));
                 channel.basicAck(envelope.getDeliveryTag(),false);
             }
         };
         channel.basicConsume(Publisher.QUEUE_NAME,false,callback);
         System.out.println("开始监听队列");

         System.in.read();
     }
 }

4.5 Publish/Subscribe

自定义一个交换机

生产者:自行构建Exchange并绑定指定队列(FANOUT类型)

java 复制代码
package com.mashibing.pubsub;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;

/**
 * @author zjw
 * @description
 * @date 2022/1/25 20:08
 */
public class Publisher {

    public static final String EXCHANGE_NAME = "pubsub";
    public static final String QUEUE_NAME1 = "pubsub-one";
    public static final String QUEUE_NAME2 = "pubsub-two";
    @Test
    public void publish() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

        //4. 构建队列
        channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
        channel.queueDeclare(QUEUE_NAME2,false,false,false,null);

        //5. 绑定交换机和队列,使用的是FANOUT类型的交换机,绑定方式是直接绑定
        channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"");
        channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"");

        //6. 发消息到交换机
        channel.basicPublish(EXCHANGE_NAME,"45jk6h645jk",null,"publish/subscribe!".getBytes());
        System.out.println("消息成功发送!");
    }
}

4.6 Routing

DIRECT类型Exchange

生产者:在绑定Exchange和Queue时,需要指定好routingKey,同时在发送消息时,也指定routingKey,只有routingKey一致时,才会把指定的消息路由到指定的Queue

java 复制代码
package com.mashibing.routing;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;

/**
 * @author zjw
 * @description
 * @date 2022/1/25 20:20
 */
public class Publisher {

    public static final String EXCHANGE_NAME = "routing";
    public static final String QUEUE_NAME1 = "routing-one";
    public static final String QUEUE_NAME2 = "routing-two";
    @Test
    public void publish() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        //4. 构建队列
        channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
        channel.queueDeclare(QUEUE_NAME2,false,false,false,null);

        //5. 绑定交换机和队列
        channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"ORANGE");
        channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"BLACK");
        channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"GREEN");

        //6. 发消息到交换机
        channel.basicPublish(EXCHANGE_NAME,"ORANGE",null,"大橙子!".getBytes());
        channel.basicPublish(EXCHANGE_NAME,"BLACK",null,"黑布林大狸子".getBytes());
        channel.basicPublish(EXCHANGE_NAME,"WHITE",null,"小白兔!".getBytes());
        System.out.println("消息成功发送!");


    }

}

4.7 Topic

Topic模式

生产者:TOPIC类型可以编写带有特殊意义的routingKey的绑定方式

java 复制代码
package com.mashibing.topics;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;

/**
 * @author zjw
 * @description
 * @date 2022/1/25 20:28
 */
public class Publisher {

    public static final String EXCHANGE_NAME = "topic";
    public static final String QUEUE_NAME1 = "topic-one";
    public static final String QUEUE_NAME2 = "topic-two";
    @Test
    public void publish() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

        //4. 构建队列
        channel.queueDeclare(QUEUE_NAME1,false,false,false,null);
        channel.queueDeclare(QUEUE_NAME2,false,false,false,null);

        //5. 绑定交换机和队列,
        // TOPIC类型的交换机在和队列绑定时,需要以aaa.bbb.ccc..方式编写routingkey
        // 其中有两个特殊字符:*(相当于占位符),#(相当通配符)
        channel.queueBind(QUEUE_NAME1,EXCHANGE_NAME,"*.orange.*");
        channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"*.*.rabbit");
        channel.queueBind(QUEUE_NAME2,EXCHANGE_NAME,"lazy.#");

        //6. 发消息到交换机
        channel.basicPublish(EXCHANGE_NAME,"big.orange.rabbit",null,"大橙兔子!".getBytes());
        channel.basicPublish(EXCHANGE_NAME,"small.white.rabbit",null,"小白兔".getBytes());
        channel.basicPublish(EXCHANGE_NAME,"lazy.dog.dog.dog.dog.dog.dog",null,"懒狗狗狗狗狗狗".getBytes());
        System.out.println("消息成功发送!");

    }
}

4.8 RPC(了解)

因为两个服务在交互时,可以尽量做到Client和Server的解耦,通过RabbitMQ进行解耦操作

需要让Client发送消息时,携带两个属性:

  • replyTo告知Server将相应信息放到哪个队列
  • correlationId告知Server发送相应消息时,需要携带位置标示来告知Client响应的信息
RPC方式

客户端:

java 复制代码
package com.mashibing.rpc;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;

import java.io.IOException;
import java.util.UUID;

/**
 * @author zjw
 * @description
 * @date 2022/2/8 20:03
 */
public class Publisher {

    public static final String QUEUE_PUBLISHER = "rpc_publisher";
    public static final String QUEUE_CONSUMER = "rpc_consumer";

    @Test
    public void publish() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建队列
        channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
        channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);

        //4. 发布消息
        String message = "Hello RPC!";
        String uuid = UUID.randomUUID().toString();
        AMQP.BasicProperties props = new AMQP.BasicProperties()
                .builder()
                .replyTo(QUEUE_CONSUMER)
                .correlationId(uuid)
                .build();
        channel.basicPublish("",QUEUE_PUBLISHER,props,message.getBytes());

        channel.basicConsume(QUEUE_CONSUMER,false,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String id = properties.getCorrelationId();
                if(id != null && id.equalsIgnoreCase(uuid)){
                    System.out.println("接收到服务端的响应:" + new String(body,"UTF-8"));
                }
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });
        System.out.println("消息发送成功!");

        System.in.read();
    }


}

服务端:

java 复制代码
package com.mashibing.rpc;

import com.mashibing.helloworld.Publisher;
import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;

import java.io.IOException;

/**
 * @author zjw
 * @description
 * @date 2022/1/24 23:02
 */
public class Consumer {

    public static final String QUEUE_PUBLISHER = "rpc_publisher";
    public static final String QUEUE_CONSUMER = "rpc_consumer";

    @Test
    public void consume() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建队列
        channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
        channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);


        //4. 监听消息
        DefaultConsumer callback = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者获取到消息:" + new String(body,"UTF-8"));
                String resp = "获取到了client发出的请求,这里是响应的信息";
                String respQueueName = properties.getReplyTo();
                String uuid = properties.getCorrelationId();
                AMQP.BasicProperties props = new AMQP.BasicProperties()
                        .builder()
                        .correlationId(uuid)
                        .build();
                channel.basicPublish("",respQueueName,props,resp.getBytes());
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        channel.basicConsume(QUEUE_PUBLISHER,false,callback);
        System.out.println("开始监听队列");

        System.in.read();
    }
}

五、SpringBoot操作RabbitMQ


5.1 SpringBoot声明信息

  • 创建项目

  • 导入依赖

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  • 配置RabbitMQ信息

    yml 复制代码
    spring:
      rabbitmq:
        host: 192.168.11.32
        port: 5672
        username: guest
        password: guest
        virtual-host: /
  • 声明交换机&队列

    java 复制代码
    package com.mashibing.rabbitmqboot.config;
    
    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author zjw
     * @description
     * @date 2022/2/8 20:25
     */
    @Configuration
    public class RabbitMQConfig {
    
        public static final String EXCHANGE = "boot-exchange";
        public static final String QUEUE = "boot-queue";
        public static final String ROUTING_KEY = "*.black.*";
    
    
        @Bean
        public Exchange bootExchange(){
            // channel.DeclareExchange
            return ExchangeBuilder.topicExchange(EXCHANGE).build();
        }
    
        @Bean
        public Queue bootQueue(){
            return QueueBuilder.durable(QUEUE).build();
        }
    
        @Bean
        public Binding bootBinding(Exchange bootExchange,Queue bootQueue){
            return BindingBuilder.bind(bootQueue).to(bootExchange).with(ROUTING_KEY).noargs();
        }
    }

5.2 生产者操作

java 复制代码
package com.mashibing.rabbitmqboot;

import com.mashibing.rabbitmqboot.config.RabbitMQConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author zjw
 * @description
 * @date 2022/2/8 21:05
 */
@SpringBootTest
public class PublisherTest {

    @Autowired
    public RabbitTemplate rabbitTemplate;

    @Test
    public void publish(){
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE,"big.black.dog","message");
        System.out.println("消息发送成功");
    }


    @Test
    public void publishWithProps(){
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE, "big.black.dog", "messageWithProps", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setCorrelationId("123");
                return message;
            }
        });
        System.out.println("消息发送成功");
    }
}

5.3 消费者操作

java 复制代码
package com.mashibing.rabbitmqboot;

import com.mashibing.rabbitmqboot.config.RabbitMQConfig;
import com.rabbitmq.client.Channel;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author zjw
 * @description
 * @date 2022/2/8 21:11
 */
@Component
public class ConsumeListener {

    @RabbitListener(queues = RabbitMQConfig.QUEUE)
    public void consume(String msg, Channel channel, Message message) throws IOException {
        System.out.println("队列的消息为:" + msg);
        String correlationId = message.getMessageProperties().getCorrelationId();
        System.out.println("唯一标识为:" + correlationId);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }
}

六、RabbitMQ保证消息可靠性


6.1 保证消息一定送达到Exchange

Confirm机制

可以通过Confirm效果保证消息一定送达到Exchange,官方提供了三种方式,选择了对于效率影响最低的异步回调的效果

java 复制代码
//4. 开启confirms
channel.confirmSelect();

//5. 设置confirms的异步回调
channel.addConfirmListener(new ConfirmListener() {
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息成功的发送到Exchange!");
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息没有发送到Exchange,尝试重试,或者保存到数据库做其他补偿操作!");
    }
});

6.2 保证消息可以路由到Queue

Return机制

为了保证Exchange上的消息一定可以送达到Queue

java 复制代码
//6. 设置Return回调,确认消息是否路由到了Queue
channel.addReturnListener(new ReturnListener() {
    @Override
    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("消息没有路由到指定队列,做其他的补偿措施!!");
    }
});
//7. 在发送消息时,将basicPublish方法参数中的mandatory设置为true,即可开启Return机制,当消息没有路由到队列中时,就会执行return回调

6.3 保证Queue可以持久化消息

DeliveryMode设置消息持久化

DeliveryMode设置为2代表持久化,如果设置为1,就代表不会持久化。

java 复制代码
//7. 设置消息持久化
AMQP.BasicProperties props = new AMQP.BasicProperties()
    .builder()
    .deliveryMode(2)
    .build();

//7. 发布消息
channel.basicPublish("","confirms",true,props,message.getBytes());

6.4 保证消费者可以正常消费消息

详情看WorkQueue模式

6.5 SpringBoot实现上述操作

6.5.1 Confirm
  • 编写配置文件开启Confirm机制

    yml 复制代码
    spring:
      rabbitmq:
        publisher-confirm-type: correlated  # 新版本
        publisher-confirms: true  # 老版本 
  • 在发送消息时,配置RabbitTemplate

    java 复制代码
    @Test
    public void publishWithConfirms() throws IOException {
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    System.out.println("消息已经送达到交换机!!");
                }else{
                    System.out.println("消息没有送达到Exchange,需要做一些补偿操作!!retry!!!");
                }
            }
        });
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE,"big.black.dog","message");
        System.out.println("消息发送成功");
    
        System.in.read();
    }
6.5.2 Return
  • 编写配置文件开启Return机制

    yml 复制代码
    spring:
      rabbitmq:
        publisher-returns: true # 开启Return机制
  • 在发送消息时,配置RabbitTemplate

    java 复制代码
    @Test
    public void publishWithReturn() throws IOException {
        // 新版本用 setReturnsCallback ,老版本用setReturnCallback
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                String msg = new String(returned.getMessage().getBody());
                System.out.println("消息:" + msg + "路由队列失败!!做补救操作!!");
            }
        });
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE,"big.black.dog","message");
        System.out.println("消息发送成功");
    
        System.in.read();
    }
6.5.3 消息持久化
java 复制代码
@Test
public void publishWithBasicProperties() throws IOException {
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE, "big.black.dog", "message", new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            // 设置消息的持久化!
            message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
            return message;
        }
    });
    System.out.println("消息发送成功");
}

七、RabbitMQ死信队列&延迟交换机

7.1 什么是死信

死信&死信队列

死信队列的应用:

  • 基于死信队列在队列消息已满的情况下,消息也不会丢失
  • 实现延迟消费的效果。比如:下订单时,有15分钟的付款时间

7.2 实现死信队列

7.2.1 准备Exchange&Queue
java 复制代码
                                             package com.mashibing.rabbitmqboot.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zjw
 * @description
 * @date 2022/2/10 15:04
 */
@Configuration
public class DeadLetterConfig {

    public static final String NORMAL_EXCHANGE = "normal-exchange";
    public static final String NORMAL_QUEUE = "normal-queue";
    public static final String NORMAL_ROUTING_KEY = "normal.#";

    public static final String DEAD_EXCHANGE = "dead-exchange";
    public static final String DEAD_QUEUE = "dead-queue";
    public static final String DEAD_ROUTING_KEY = "dead.#";


    @Bean
    public Exchange normalExchange(){
        return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE).build();
    }

    @Bean
    public Queue normalQueue(){
        return QueueBuilder.durable(NORMAL_QUEUE).deadLetterExchange(DEAD_EXCHANGE).deadLetterRoutingKey("dead.abc").build();
    }

    @Bean
    public Binding normalBinding(Queue normalQueue,Exchange normalExchange){
        return BindingBuilder.bind(normalQueue).to(normalExchange).with(NORMAL_ROUTING_KEY).noargs();
    }


    @Bean
    public Exchange deadExchange(){
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE).build();
    }

    @Bean
    public Queue deadQueue(){
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }

    @Bean
    public Binding deadBinding(Queue deadQueue,Exchange deadExchange){
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
    }

}
7.2.2 实现效果
  • 基于消费者进行reject或者nack实现死信效果

    java 复制代码
    package com.mashibing.rabbitmqboot;
    
    import com.mashibing.rabbitmqboot.config.DeadLetterConfig;
    import com.rabbitmq.client.Channel;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    
    /**
     * @author zjw
     * @description
     * @date 2022/2/10 15:17
     */
    @Component
    public class DeadListener {
    
        @RabbitListener(queues = DeadLetterConfig.NORMAL_QUEUE)
        public void consume(String msg, Channel channel, Message message) throws IOException {
            System.out.println("接收到normal队列的消息:" + msg);
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
        }
    }
  • 消息的生存时间

    • 给消息设置生存时间

      java 复制代码
      @Test
      public void publishExpire(){
          String msg = "dead letter expire";
          rabbitTemplate.convertAndSend(DeadLetterConfig.NORMAL_EXCHANGE, "normal.abc", msg, new MessagePostProcessor() {
              @Override
              public Message postProcessMessage(Message message) throws AmqpException {
                  message.getMessageProperties().setExpiration("5000");
                  return message;
              }
          });
      }
    • 给队列设置消息的生存时间

      java 复制代码
      @Bean
      public Queue normalQueue(){
          return QueueBuilder.durable(NORMAL_QUEUE)
                  .deadLetterExchange(DEAD_EXCHANGE)
                  .deadLetterRoutingKey("dead.abc")
                  .ttl(10000)
                  .build();
      }
  • 设置Queue中的消息最大长度

    java 复制代码
    @Bean
    public Queue normalQueue(){
        return QueueBuilder.durable(NORMAL_QUEUE)
                .deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey("dead.abc")
                .maxLength(1)
                .build();
    }

    只要Queue中已经有一个消息,如果再次发送一个消息,这个消息会变为死信!

7.3 延迟交换机

下载地址:github.com/rabbitmq/ra...

死信队列实现延迟消费时,如果延迟时间比较复杂,比较多,直接使用死信队列时,需要创建大量的队列还对应不同的时间,可以采用延迟交换机来解决这个问题。

  • 构建延迟交换机

    java 复制代码
    package com.mashibing.rabbitmqboot.config;
    
    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author zjw
     * @description
     */
    @Configuration
    public class DelayedConfig {
    
        public static final String DELAYED_EXCHANGE = "delayed-exchange";
        public static final String DELAYED_QUEUE = "delayed-queue";
        public static final String DELAYED_ROUTING_KEY = "delayed.#";
    
        @Bean
        public Exchange delayedExchange(){
            Map<String, Object> arguments = new HashMap<>();
            arguments.put("x-delayed-type","topic");
            Exchange exchange = new CustomExchange(DELAYED_EXCHANGE,"x-delayed-message",true,false,arguments);
            return exchange;
        }
    
        @Bean
        public Queue delayedQueue(){
            return QueueBuilder.durable(DELAYED_QUEUE).build();
        }
    
        @Bean
        public Binding delayedBinding(Queue delayedQueue,Exchange delayedExchange){
            return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
        }
    }
  • 发送消息

    java 复制代码
    package com.mashibing.rabbitmqboot;
    
    import com.mashibing.rabbitmqboot.config.DelayedConfig;
    import org.junit.jupiter.api.Test;
    import org.springframework.amqp.AmqpException;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.MessagePostProcessor;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    /**
     * @author zjw
     * @description
     */
    @SpringBootTest
    public class DelayedPublisherTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void publish(){
            rabbitTemplate.convertAndSend(DelayedConfig.DELAYED_EXCHANGE, "delayed.abc", "xxxx", new MessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    message.getMessageProperties().setDelay(30000);
                    return message;
                }
            });
        }
    }

八、RabbitMQ的集群

RabbitMQ的镜像模式

RabbitMQ的集群

高可用

提升RabbitMQ的效率

搭建RabbitMQ集群

  • 准备两台虚拟机(克隆)

  • 准备RabbitMQ的yml文件

    rabbitmq1:

    yml 复制代码
    version: '3.1'
    services:
      rabbitmq1:
        image: rabbitmq:3.8.5-management-alpine
        container_name: rabbitmq1
        hostname: rabbitmq1
        extra_hosts:
          - "rabbitmq1:192.168.11.32"
          - "rabbitmq2:192.168.11.33"
        environment: 
          - RABBITMQ_ERLANG_COOKIE=SDJHFGDFFS
        ports:
          - 5672:5672
          - 15672:15672
          - 4369:4369
          - 25672:25672

    rabbitmq2:

    yml 复制代码
    version: '3.1'
    services:
      rabbitmq2:
        image: rabbitmq:3.8.5-management-alpine
        container_name: rabbitmq2
        hostname: rabbitmq2
        extra_hosts:
          - "rabbitmq1:192.168.11.32"
          - "rabbitmq2:192.168.11.33"
        environment: 
          - RABBITMQ_ERLANG_COOKIE=SDJHFGDFFS
        ports:
          - 5672:5672
          - 15672:15672
          - 4369:4369
          - 25672:25672

    准备完毕之后,启动两台RabbitMQ

    启动效果
  • 让RabbitMQ服务实现join操作

    需要四个命令完成join操作

    让rabbitmq2 join rabbitmq1,需要进入到rabbitmq2的容器内部,去执行下述命令

    sh 复制代码
    rabbitmqctl stop_app
    rabbitmqctl reset 
    rabbitmqctl join_cluster rabbit@rabbitmq1
    rabbitmqctl start_app

    执行成功后:

    执行成功后
  • 设置镜像模式

    在指定的RabbitMQ服务中设置好镜像策略即可

    镜像模式

九、RabbitMQ其他内容

9.1 Headers类型Exchange

headers就是一个基于key-value的方式,让Exchange和Queue绑定的到一起的一种规则

相比Topic形式,可以采用的类型更丰富。

headers绑定方式

具体实现方式

java 复制代码
package com.mashibing.headers;

import com.mashibing.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

/**
 * @author zjw
 * @description
 */
public class Publisher {

    public static final String HEADER_EXCHANGE = "header_exchange";
    public static final String HEADER_QUEUE = "header_queue";


    @Test
    public void publish()throws  Exception{
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();


        //3. 构建交换机和队列并基于header的方式绑定
        channel.exchangeDeclare(HEADER_EXCHANGE, BuiltinExchangeType.HEADERS);
        channel.queueDeclare(HEADER_QUEUE,true,false,false,null);
        Map<String,Object> args = new HashMap<>();
        // 多个header的key-value只要可以匹配上一个就可以
        // args.put("x-match","any");
        // 多个header的key-value要求全部匹配上!
        args.put("x-match","all");
        args.put("name","jack");
        args.put("age","23");
        channel.queueBind(HEADER_QUEUE,HEADER_EXCHANGE,"",args);

        //4. 发送消息
        String msg = "header测试消息!";
        Map<String, Object> headers = new HashMap<>();
        headers.put("name","jac");
        headers.put("age","2");
        AMQP.BasicProperties props = new AMQP.BasicProperties()
                .builder()
                .headers(headers)
                .build();

        channel.basicPublish(HEADER_EXCHANGE,"",props,msg.getBytes());

        System.out.println("发送消息成功,header = " + headers);

    }
}

十、RabbitMQ实战

在掌握了SpringCloudAlibaba的应用后,再来玩!!

为了更好的理解RabbitMQ在项目中的作用,来一套实战操作。

10.1 RabbitMQ实战场景

首先模拟一个场景,电商中对应的处理方案。

模拟一个用户在电商平台下单:

  • 需要调用库存服务,扣除商品库存,扣除成功后,才可以继续往下走业务
  • 需要调用订单服务,创建订单(待支付)。
  • 还需要很多后续的处理
    • 下单时,会使用优惠券,预扣除当前用户使用的优惠券
    • 下单时,会使用用户积分顶金额,预扣除当前用户的积分
    • 创建成功后,需要通知商家,有用户下单。
    • ..................

10.2 RabbitMQ实战场景搭建

因为场景设计到了服务之间的调用。

这里需要大家提前掌握一些知识:Nacos,OpenFeign的应用层面。

1、构建聚合工程,作为父工程管理所有的模块

准备好pom.xml文件

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath />
</parent>

<groupId>com.mashibing</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
    <spring.cloud-version>Hoxton.SR12</spring.cloud-version>
    <spring.cloud.alibaba-version>2.2.7.RELEASE</spring.cloud.alibaba-version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring.cloud-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring.cloud.alibaba-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
2、构建其他六个子服务
3、从下单服务开始一次完成配置以及接口的提供

下单服务:

  • 导入依赖 org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-openfeign

  • 构建启动类 @SpringBootApplication @EnableDiscoveryClient public class PlaceOrderStarterApp {

    arduino 复制代码
        public static void main(String[] args) {
            SpringApplication.run(PlaceOrderStarterApp.class,args);
        }
    }
  • 编写配置文件 server: port: 80

    yaml 复制代码
    spring:
      application:
        name: placeorder
      cloud:
        nacos:
          discovery:
            server-addr: 114.116.226.76:8848
  • 处理问题:启动后发现,无法正常的注册到Nacos上,需要将Alibaba的版本降到2.2.6.RELEASE

其他服务的基本配置,我这里直接写好,然后大家可以去Git中找到指定提交点~

4、完成整个下单的流程
  • 下单服务接口(前置操作) @RestController public class PlaceOrderController {

    typescript 复制代码
        /**
         * 模拟用户下单操作
         * @return
         */
        @GetMapping("/po")
        public String po(){
            //1、调用库存服务扣除商品库存
    
            //2、调用订单服务,创建订单
    
            //3、调用优惠券服务,预扣除使用的优惠券
    
            //4、调用用户积分服务,预扣除用户使用的积分
    
            //5、调用商家服务,通知商家用户已下单
    
            return "place order is ok!";
        }
    
    }
  • 库存服务接口 @RestController public class ItemStockController {

    csharp 复制代码
        private static int stock = 10;
    
        @GetMapping("/decr")
        public void decr() throws InterruptedException {
            Thread.sleep(400);
            stock--;
            if(stock < 0){
                throw new RuntimeException("商品库存不足!");
            }
            System.out.println("扣减库存成功!");
        }
    }
  • 订单服务接口 @RestController public class OrderManageController {

    csharp 复制代码
        @GetMapping("create")
        public void create() throws InterruptedException {
            Thread.sleep(400);
            System.out.println("创建订单成功!");
        }
    
    }
  • 优惠券服务接口 @RestController public class CouponController {

    csharp 复制代码
        @GetMapping("/coupon")
        public void coupon() throws InterruptedException {
            Thread.sleep(400);
            System.out.println("优惠券预扣除成功!");
        }
    
    }
  • 用户积分服务接口 @RestController public class UserPointsController {

    csharp 复制代码
        @GetMapping("/up")
        public void up() throws InterruptedException {
            Thread.sleep(400);
            System.out.println("扣除用户积分成功!!");
        }
    
    }
  • 商家服务接口 @RestController public class BusinessController {

    csharp 复制代码
        @GetMapping("/notify")
        public void notifyBusiness() throws InterruptedException {
            Thread.sleep(400);
            System.out.println("通知商家成功!!");
        }
    
    }
5、完善下单接口服务调用
  • 先给启动类添加OpenFeign注解

    css 复制代码
    @EnableFeignClients
  • 给5个服务提供对应的OpenFeign接口

  • 在下单服务的Controller中实现服务的调用

    scss 复制代码
    @RestController
    public class PlaceOrderController {
    
        @Autowired
        private ItemStockClient itemStockClient;
        @Autowired
        private OrderManageClient orderManageClient;
        @Autowired
        private CouponClient couponClient;
        @Autowired
        private UserPointsClient userPointsClient;
        @Autowired
        private BusinessClient businessClient;
    
    
        /**
         * 模拟用户下单操作
         * @return
         */
        @GetMapping("/po")
        public String po(){
            long start = System.currentTimeMillis();
            //1、调用库存服务扣除商品库存
            itemStockClient.decr();
            //2、调用订单服务,创建订单
            orderManageClient.create();
            //3、调用优惠券服务,预扣除使用的优惠券
            couponClient.coupon();
            //4、调用用户积分服务,预扣除用户使用的积分
            userPointsClient.up();
            //5、调用商家服务,通知商家用户已下单
            businessClient.notifyBusiness();
    
            long end = System.currentTimeMillis();
            System.out.println(end - start);
            return "place order is ok!";
        }
    
    }

10.3 完成异步调用

因为下单功能,核心就在于扣除库存成功,以及创建订单成功。只要这两个操作么得问题,直接就可以让后续的优惠券,用户积分,通知商家等等操作实现一个异步的效果。而且基于RabbitMQ做异步之后,还可以让下单服务与其他服务做到解耦。

异步:可以让整个业务的处理速度更快,从而更快的给用户一个响应,下单是成功还是失败。

解耦:优惠券,用户积分,商家服务,无论哪个服务宕机,都不影响正常的下单流程。

1、下单服务
  • 导入依赖 org.springframework.boot spring-boot-starter-amqp

  • 编写配置文件链接RabbitMQ spring: rabbitmq: host: 114.116.226.76 port: 5672 username: rabbitmq password: rabbitmq virtual-host: rabbitmq

  • 构建交换机&队列 @Configuration public class RabbitMQConfig {

    typescript 复制代码
        // 下单服务的交换机
        public static final String PLACE_ORDER_EXCHANGE = "place_order_exchange";
        // 三个服务的Queue
        public static final String COUPON_QUEUE = "coupon_queue";
        public static final String USER_POINTS_QUEUE = "user_points_queue";
        public static final String BUSINESS_QUEUE = "business_queue";
    
    
        @Bean
        public Exchange placeOrderExchange(){
            return ExchangeBuilder.fanoutExchange(PLACE_ORDER_EXCHANGE).build();
        }
    
        @Bean
        public Queue couponQueue(){
            return QueueBuilder.durable(COUPON_QUEUE).build();
        }
        @Bean
        public Queue userPointsQueue(){
            return QueueBuilder.durable(USER_POINTS_QUEUE).build();
        }
        @Bean
        public Queue businessQueue(){
            return QueueBuilder.durable(BUSINESS_QUEUE).build();
        }
    
        @Bean
        public Binding couponBinding(Exchange placeOrderExchange,Queue couponQueue){
            return BindingBuilder.bind(couponQueue).to(placeOrderExchange).with("").noargs();
        }
        @Bean
        public Binding userPointsBinding(Exchange placeOrderExchange,Queue userPointsQueue){
            return BindingBuilder.bind(userPointsQueue).to(placeOrderExchange).with("").noargs();
        }
        @Bean
        public Binding businessBinding(Exchange placeOrderExchange,Queue businessQueue){
            return BindingBuilder.bind(businessQueue).to(placeOrderExchange).with("").noargs();
        }
    }
  • 修改下单接口Controller @RestController public class PlaceOrderController {

    java 复制代码
        @Autowired
        private ItemStockClient itemStockClient;
        @Autowired
        private OrderManageClient orderManageClient;
        @Autowired
        private RabbitTemplate rabbitTemplate;
        /**
         * 模拟用户下单操作
         * @return
         */
        @GetMapping("/po")
        public String po(){
            long start = System.currentTimeMillis();
            //1、调用库存服务扣除商品库存
            itemStockClient.decr();
            //2、调用订单服务,创建订单
            orderManageClient.create();
    
            String userAndOrderInfo = "用户信息&订单信息&优惠券信息等等............";
            // 将同步方式修改为基于RabbitMQ的异步方式
            rabbitTemplate.convertAndSend(RabbitMQConfig.PLACE_ORDER_EXCHANGE,"",userAndOrderInfo);
    
            long end = System.currentTimeMillis();
            System.out.println(end - start);
            return "place order is ok!";
        }
    
    }
2、优惠券服务
  • 导入依赖 org.springframework.boot spring-boot-starter-amqp

  • COPY配置文件 spring: rabbitmq: host: 114.116.226.76 port: 5672 username: rabbitmq password: rabbitmq virtual-host: rabbitmq listener: simple: acknowledge-mode: manual

  • COPY配置类: 复制的下单服务的RabbitMQConfig

  • 编写消费者,实现预扣除优惠券 @Component public class CouponListener {

    java 复制代码
        @RabbitListener(queues = {RabbitMQConfig.COUPON_QUEUE})
        public void consume(String msg, Channel channel, Message message) throws Exception {
            // 预扣除优惠券
            Thread.sleep(400);
            System.out.println("优惠券预扣除成功!" + msg);
            // 手动ACK
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }
    
    }
3、用户积分服务

类似优惠券服务操作!

4、商家服务

类似优惠券服务操作!

10.4 下单服务保证消息的可靠性

下单服务需要保证消息一定可以发送到RabbitMQ服务中,如果发送失败。

如果消息没有发送到Exchange或者是消息没有从Exchange路由到指定队列。

  • 可以将消息存储到数据库,基于定时任务的方式重新发送。
  • 可以直接在confirm中做重试。
  • 或者是记录error日志,通过日志的形式做重新发送。
  • ............

开始完成当前操作

1、修改配置文件
yml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
2、重新配置RabbitTemplate对象,指定confirm和return的回调处理
java 复制代码
@Configuration
public class RabbitTemplateConfig {

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        //1、new出RabbitTemplate对象
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        //2、将connectionFactory设置到RabbitTemplate对象中
        rabbitTemplate.setConnectionFactory(connectionFactory);
        //3、设置confirm回调
        rabbitTemplate.setConfirmCallback(confirmCallback());
        //4、设置return回调
        rabbitTemplate.setReturnCallback(returnCallback());
        //5、设置mandatory为true
        rabbitTemplate.setMandatory(true);
        //6、返回RabbitTemplate对象即可
        return rabbitTemplate;
    }

    public RabbitTemplate.ConfirmCallback confirmCallback(){
        return new RabbitTemplate.ConfirmCallback(){
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (correlationData == null) return;
                String msgId = correlationData.getId();
                if(ack){
                    System.out.println("消息发送到Exchange成功!! msgId = " + msgId);
                }else{
                    System.out.println("消息发送到Exchange失败!! msgId = " + msgId);
                }
            }
        };
    }

    public RabbitTemplate.ReturnCallback returnCallback(){
        return new RabbitTemplate.ReturnCallback(){
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("消息未路由到队列");
                System.out.println("return:消息为:" + new String(message.getBody()));
                System.out.println("return:交换机为:" + exchange);
                System.out.println("return:路由为:" + routingKey);
            }
        };
    }

}
3、重新完成Controller中消息的发送并且完善confirm和return的回调
3.1、需要在Controller中将correlationData和发送的消息信息绑定

准备全局的Cache

java 复制代码
public class GlobalCache {

    private static Map map = new HashMap();

    public static void set(String key,Object value){
        map.put(key,value);
    }

    public static Object get(String key){
        Object value = map.get(key);
        return value;
    }

    public static void remove(String key){
        map.remove(key);
    }

}

重新编写Controller,实现标识和消息信息的绑定

java 复制代码
@RestController
public class PlaceOrderController {

    @Autowired
    private ItemStockClient itemStockClient;
    @Autowired
    private OrderManageClient orderManageClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 模拟用户下单操作
     * @return
     */
    @GetMapping("/po")
    public String po(){
        long start = System.currentTimeMillis();
        //1、调用库存服务扣除商品库存
        itemStockClient.decr();
        //2、调用订单服务,创建订单
        orderManageClient.create();

        // 将之前的同步方式注释
        String userAndOrderInfo = "用户信息&订单信息&优惠券信息等等............";
        // 声明当前消息的id标识
        String id = UUID.randomUUID().toString();
        // 封装消息的全部信息
        Map map = new HashMap<>();
        map.put("message",userAndOrderInfo);
        map.put("exchange",RabbitMQConfig.PLACE_ORDER_EXCHANGE);
        map.put("routingKey","");
        map.put("sendTime",new Date());
        // 将id标识和消息存储到全局缓存中
        GlobalCache.set(id,map);
        // 将同步方式修改为基于RabbitMQ的异步方式
        rabbitTemplate.convertAndSend(RabbitMQConfig.PLACE_ORDER_EXCHANGE,"",userAndOrderInfo,new CorrelationData(id));


        long end = System.currentTimeMillis();
        System.out.println(end - start);
        return "place order is ok!";
    }

}
3.2、需要在confirm的回调中完成两个操作
  • 消息发送成功,删除之前绑定的消息 if(ack){ log.info("消息发送到Exchange成功!!"); GlobalCache.remove(msgId); }
  • 消息发送失败,将之前绑定的消息存储到数据库
    • 准备库表信息,存储发送失败的信息。 CREATE TABLE resend ( id varchar(255) NOT NULL, message varchar(255) NOT NULL, exchange varchar(255) NOT NULL, routing_key varchar(255) NOT NULL, send_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, send_count int(11) NOT NULL DEFAULT '0' COMMENT '最多重新发送3次', is_send int(11) NOT NULL DEFAULT '0' COMMENT '0-发送失败,1-发送成功', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    • 实现数据源和MyBatis的基本配置:......
    • 改造confirm实现 public RabbitTemplate.ConfirmCallback confirmCallback(){ return new RabbitTemplate.ConfirmCallback(){ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (correlationData == null) return; String msgId = correlationData.getId(); if(ack){ log.info("消息发送到Exchange成功!!"); GlobalCache.remove(msgId); }else{ log.error("消息发送失败!"); Map value = (Map) GlobalCache.get(msgId); // 推荐自己玩的时候,用service做增删改操作,控制事务~ resendMapper.save(value); } } }; }
4、测试效果

10.5 消费者避免重复消费问题

采用数据库的幂等表解决消费者可能存在重复消费的问题。

再真正处理消费执行业务前做一些操作,先去查看数据库中的幂等表信息:

  • 如果消息的唯一标识已经存在了,证明当前消息已经被消费,直接告辞。
  • 如果消息的唯一标识不存在,先将当前唯一标识存储到幂等表中,然后再执行消费业务。

基于用户积分服务实现幂等性操作。

1、准备幂等表
sql 复制代码
CREATE TABLE `user_points_idempotent` (
  `id` varchar(255) NOT NULL,
  `createtime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2、给用户积分服务追加连接数据库信息:
  • 导入依赖

  • 编写配置

  • 准备Mapper接口 public interface UserPointsIdempotentMapper {

    less 复制代码
        @Select("select count(1) from user_points_idempotent where id = #{id}")
        int findById(@Param("id") String id);
    
        @Insert("insert into user_points_idempotent (id) values (#{id})")
        void save(@Param("id") String id);
    
    }
3、准备消费方法
java 复制代码
@Service
@Slf4j
public class UserPointsConsumeImpl implements UserPointsConsume {

    @Resource
    private UserPointsIdempotentMapper userPointsIdempotentMapper;

    private final String ID_NAME = "spring_returned_message_correlation";


    @Override
    @Transactional
    public void consume(Message message) {
        // 获取生产者提供的CorrelationId要基于header去获取。
        String id = message.getMessageProperties().getHeader(ID_NAME);
        //1、查询幂等表是否存在当前消息标识
        int count = userPointsIdempotentMapper.findById(id);
        //2、如果存在,直接return结束
        if(count == 1){
            log.info("消息已经被消费!!!无需重复消费!");
            return;
        }
        //3、如果不存在,插入消息标识到幂等表
        userPointsIdempotentMapper.save(id);
        //4、执行消费逻辑
        // 预扣除用户积分
        try {
            Thread.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("扣除用户积分成功!!");
    }
}
4、测试功能效果

10.6 实现延迟取消订单状态

当客户端下单之后,会基于订单服务在数据库中构建一个订单信息,默认情况下,订单信息是待支付状态。

如果用户正常支付了,会将当前订单从待支付状态改为已支付/待发货状态。

如果超过一定的时间,用户没有支付,此时需要将订单状态从待支付改为已取消的状态。

基于RabbitMQ提供的死信队列来实现当前的延迟修改订单状态的功能,同时也可以采用延迟交换机插件的形式实现,But,因为当前业务中,延迟时间是统一的,不使用延迟交换机也是ok的。

1、准备订单表并修改订单服务的业务
  • 准备表结构 CREATE TABLE tb_order ( id varchar(36) NOT NULL AUTO_INCREMENT, total decimal(10,2) DEFAULT NULL, order_state int(11) DEFAULT '0' COMMENT '订单状态 0-待支付, 1-已支付,2-待发货,3-已发货,-1-已取消', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 修改订单服务,将之前模拟数据库操作,改为真实的数据库操作
    • 导入依赖 mysql mysql-connector-java 5.1.47 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 org.projectlombok lombok
    • 编写配置信息 spring: datasource: driver-class-name: org.gjt.mm.mysql.Driver url: jdbc:mysql:///rabbitmq username: root password: root
    • 启动类添加注解 @MapperScan(basePackages = "com.mashibing.mapper")
    • 实现添加操作
      • 准备Mapper接口 public interface TBOrderMapper {

        less 复制代码
            @Insert("insert into tb_order (id) values (#{id})")
            void save(@Param("id") String id);
        
        }
      • 准备Service层 @Service public class TBOrderServiceImpl implements TBOrderService {

        typescript 复制代码
            @Resource
            private TBOrderMapper orderMapper;
        
        
            @Override
            public void save() {
        	String id = UUID.randomUUID().toString();
                orderMapper.save(id);
            }
        }
      • Controller调用Service层 @RestController @Slf4j public class OrderManageController {

        java 复制代码
            @Autowired
            private TBOrderService orderService;
        
            @GetMapping("create")
            public void create() throws InterruptedException {
                orderService.save();
                log.info("创建订单成功!!");
            }
        
        }
      • 测试

2、在订单服务中准备死信队列配置
  • 导入依赖 org.springframework.boot spring-boot-starter-amqp

  • 编写配置文件 spring: rabbitmq: host: 114.116.226.76 port: 5672 username: rabbitmq password: rabbitmq virtual-host: rabbitmq listener: simple: acknowledge-mode: manual

  • 编写配置类完成死信队列的构建 @Configuration public class RabbitMQConfig {

    typescript 复制代码
        public static final String ORDER_EXCHANGE = "order_exchange";
        public static final String ORDER_QUEUE = "order_queue";
    
        public static final String DEAD_EXCHANGE = "dead_exchange";
        public static final String DEAD_QUEUE = "dead_queue";
    
        @Bean
        public Exchange orderExchange(){
            return ExchangeBuilder.fanoutExchange(ORDER_EXCHANGE).build();
        }
    
        @Bean
        public Queue orderQueue(){
            return QueueBuilder.durable(ORDER_QUEUE).deadLetterExchange(DEAD_EXCHANGE).build();
        }
    
        @Bean
        public Exchange deadExchange(){
            return ExchangeBuilder.fanoutExchange(DEAD_EXCHANGE).build();
        }
    
        @Bean
        public Queue deadQueue(){
            return QueueBuilder.durable(DEAD_QUEUE).build();
        }
    
        @Bean
        public Binding orderBinding(Exchange orderExchange,Queue orderQueue){
            return BindingBuilder.bind(orderQueue).to(orderExchange).with("").noargs();
        }
    
        @Bean
        public Binding deadBinding(Exchange deadExchange,Queue deadQueue){
            return BindingBuilder.bind(deadQueue).to(deadExchange).with("").noargs();
        }
    
    }
3、完成订单构建成功后,发送消息到死信队列

前面的准备工作,没考虑到订单的主键需要作为消息的问题,将之前的主键自增的形式,更改为UUID作为主键,方便作为消息传递。

处理了两个问题:

  • 订单表的主键,为了方便作为消息,将之前主键自增的ID,设置为了自然主键,用UUID。
  • 发送消息后,发现队列没有收到消息,定位到是忘记在配置文件追加Binding信息。

完成消息发送

java 复制代码
@Service
public class TBOrderServiceImpl implements TBOrderService {

    @Resource
    private TBOrderMapper orderMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;


    @Override
    @Transactional
    public void save() {
        // 生成主键ID
        String id = UUID.randomUUID().toString();
        // 创建订单
        orderMapper.save(id);
        // 订单构建成功~
        // 发送消息到RabbitMQ的死信队列
        rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_EXCHANGE, "", id, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置消息的生存时间为15s,当然,也可以在构建队列时,指定队列的生存时间。
                message.getMessageProperties().setExpiration("15000");
                return message;
            }
        });
    }
}
4、声明消费者消费延迟取消订单的消息
  • 声明消费者: @Component public class DelayMessageListener {

    java 复制代码
        @Autowired
        private TBOrderService orderService;
    
        @RabbitListener(queues = RabbitMQConfig.DEAD_QUEUE)
        public void consume(String id, Channel channel, Message message) throws IOException {
            //1、 调用Service实现订单状态的处理
            orderService.delayCancelOrder(id);
    
            //2、 ack的干活~
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }
    
    }
  • 完善Service业务处理 @Override @Transactional public void delayCancelOrder(String id) { //1、基于id查询订单信息。 for update int orderState = orderMapper.findOrderStateByIdForUpdate(id); //2、判断订单状态 if(orderState != 0){ log.info("订单已经支付!!"); return; } //3、修改订单状态 log.info("订单未支付,修改订单状态为已取消"); orderMapper.updateOrderStateById(-1,id); }

  • 提供Mapper与数据库交互的业务 public interface TBOrderMapper {

    less 复制代码
        @Select("select order_state from tb_order where id = #{id} for update")
        int findOrderStateByIdForUpdate(@Param("id") String id);
    
        @Update("update tb_order set order_state = #{orderState} where id = #{id}")
        void updateOrderStateById(@Param("orderState") int i, @Param("id") String id);
    }
相关推荐
小突突突1 小时前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年1 小时前
Go 语言并发编程核心与用法
开发语言·后端·golang
roman_日积跬步-终至千里1 小时前
【大数据架构-数据中台(1)】解码数据中台:从概念到认知
大数据·架构·dubbo
掘金码甲哥1 小时前
云原生算力平台的架构解读
后端
码事漫谈1 小时前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈1 小时前
现代软件开发中常用架构的系统梳理与实践指南
后端
狼爷2 小时前
Saga 分布式事务模式详解
架构
Mr.Entropy2 小时前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate
oMcLin2 小时前
如何在 Red Hat Linux 8 上实现 Kubernetes 自定义资源管理器(CRD)扩展,支持微服务架构
linux·架构·kubernetes
YDS8292 小时前
SpringCloud —— MQ的可靠性保障和延迟消息
后端·spring·spring cloud·rabbitmq