Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq

前言

本节主要涉及到Rabbitmq在SpringBoot的API使用,基本是上一节的一个扩展,也会简单的介绍一下七种模型和Header Exchange的使用,不过使用场景从原生变成了SpringBoot封装。
对于七种模型和Header Exchange的介绍会很简单提及,如果想详细了解,可以看一下上一节内容。

一 Hello Word

在这里插入图片描述
Hello Word 模型采用默认Exchange,以Queue Name作为RoutingKey的方式进行消息发布。

具体实现如下

1.1 配置文件

spring:
  application:
    name: Rabbitmq-AMQP
  rabbitmq:
    host: ****** # 这里填写ip
    port: 5672
    virtual-host: /test  # 虚拟主机
    username: admin
    password: admin

1.2 Consumer

/**
 * @author heshuai
 * @title: HelloWordConsumer
 * @description: HelloWord模型 消息消费者
 *              这里需要介绍几个注解
 *                  @Component: Spring IOC注解,注入当前对象实例到ICO容器
 *                  @RabbitListener: Rabbitmq监听器,监听Queue来接受消息,这个注解中包含了常用的接受消息时所涉及到的方法
 *                              id:唯一标识该监听器,可以自动生成,无需特殊设置
 *                              containerFactory:容器工厂,生成RabbitListener容器所用,一般使用默认工厂即可
 *                              queues:所监听Queue的名称,可以为数组,即监听多个Queue;注:所指定Queue必须存在,或者在其它位置开始声明对应的bean
 *                              queuesToDeclare:声明并监听该Queue,可以使用@Queue()去描述Queue
 *                              exclusive:true,单一消息中独占Queue,并且要求并发量为1;false:默认
 *                              bindings: 绑定Exchange和Queue,@QueueBinding可以使用这个注解去定义QueueBinding对象
 *                   @Queue: 定义一个Queue对象,这个注解在queuesToDeclare属性中可以使用
 *                              在这个对象中,可以定义一个常规的Queue的所有配置,这个和之前Simple-module中的内容基本一致
 * @date 2021年01月29日 11:17
 */
@Component
@RabbitListener(queuesToDeclare = @Queue("helloWord"))
public class HelloWordConsumer {
    /**
     *  @RabbitListener 可以使用在类上或者是方法上,如果使用在类上,需要指定类中的一个方法作为接受消息的方法,
     *                  所以这里使用了@RabbitHandler
     *  可以使用Object去接收消息,也可以使用String去接收:如果使用Object,那么接受的是Message对象,里面包含消息体、消息头等参数信息(不可以使用Message对象去直接接受)
     *  如果使用String去接收,则只是接收到消息体
     * @param message
     */
    /*
    @RabbitHandler
    public void receive(Object message){
        System.out.println("收到消息如下:");
        System.out.println(new String(((Message)message).getBody()));
    }*/

    @RabbitHandler
    public void receive(String message){
        System.out.println("收到消息如下:");
        System.out.println(message);
    }
}

1.3 Producer

@SpringBootTest(classes = SpringbootModuleApplication.class)
class SpringbootModuleApplicationTests {
    /**
     * RabbitTemplate
     * SpringFramework 提供一个类似于RedisTemplate、RestTemplate这些的一个高度封装的模板,
     * 就是对于amqp-client的封装(没看过底层、猜测)
     * 简单介绍一下功能
     *      除了可以send、receive,还对这两个重载了很多方法,而且提供了更多如:convertAndSend、convertSendAndReceive这些更加深入的封装方法,
     *      基本上也就是对于发送消息和接受消息的各种角度的封装了;
     *      这个类中还包含一些特殊的默认值设置,比如:encoding、exchange、routingKey、defaultReceiveQueue,可以更加浓缩代码;
     *      这里有个必须设置的配置项就是connectionFactory,不过这个也是可以通过application.yml直接配置的,没什么区别
     *      除了这些以外还有一个事务的设置(setChannelTransacted()),这个就不深入了
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * HelloWord 模型 消息发布
     */
    @Test
    void helloWord() {
        /**
         * 为了介绍这个方法,使用这个方法重载最多参数的方法
         * exchange:交换机名字,“”就是默认交换机
         * routingKey:路由Key, 使用AMQP中的默认交换机,默认与所有的queue相绑定,queue名就是他们的绑定key
         * object:发送的消息内容,因为convertAndSend方法对这个进行了封装,所以可以直接发送对象,不局限byte数组了
         * CorrelationData:这个应该是用于发布者确认模型中所需要用到的参数
         */
        rabbitTemplate.convertAndSend("","helloWord",String.format("我是一个简简单单的HelloWord\n你好!这个世界!\n愿世间没有病痛\n"), (CorrelationData)null);
    }
}

二、WorkQueue

Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第1张图片
当多个Consumer监听同一个Queue时,即是所谓的WorkQueue,Rabbitmq会采用轮询的方式将Queue的消息发送给C1、C2,不会造成同一条消息的重复消费!

具体实现如下

1. Consumer

/**
 * @author heshuai
 * @title: WorkQueueConsumer
 * @description: 工作模型——WorkQueue
 *                  因为需要在一个类中写多个监听处理,所以将@RaabitListener直接写到方法上机课
 * @date 2021年02月02日 11:05
 */
@Component
public class WorkQueueConsumer {
    /**
     * @RabbitListener: 可以直接添加到方法上,表示该方法就是Rabbitmq监听器处理函数
     */
    @RabbitListener(queuesToDeclare = @Queue(name = "workQueue"))
    public void workQueue1(String message){
        System.out.println("workQueue1收到消息如下:");
        System.out.println(message);
    }

    @RabbitListener(queuesToDeclare = @Queue(name = "workQueue"))
    public void workQueue2(String message){
        System.out.println("workQueue2收到消息如下:");
        System.out.println(message);
    }

}

2. Provider

    /**
     * WorkQueue 模型 消息发布
     */
    @Test
    void workQueue(){
        for (int i=0; i<10; i++){
            rabbitTemplate.convertAndSend("workQueue",String.format("这是第"+i+"条祝福\n"));
        }
    }

三 Publish/Subscribe

Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第2张图片

订阅发布模式:官方的解释是一次性发布消息给多个Consumer,这里的Consumer可以理解成Queue,因为Consumer都是监听相应的Queue的!
如果需要实现一次性这个关键的话,那么必须使用AMQP协议中的Exchange概念,前两个模式使用的都是默认的Exchange,在下面这些模式中将去主动声明使用Exchange来进行转发Message。
具体实现如下

1. Consumer

/**
 * @author heshuai
 * @title: SubscribeConsumer
 * @description: 发布订阅模式: 这个模式是基于AMQP协议中Exchange概念,所以等下主要是看Exchange
 *              @Component: 交给Spring管理
 * @date 2021年02月08日 16:45
 */
@Component
public class SubscribeConsumer {
    /**
     * 接受消息方法:
     *           @RabbitListener: bindings属性代表绑定Exchange和Queue,并且定义他们的RoutingKey
     *           @Exchange: 定义一个Exchange,根据它的模式定义,如果这个Exchange不存在则主动创建一个Exchange,type类型默认Direct。
     *                      并且在这个注解中支持x-delayed-message类型,默认 false
     * @param message
     */
    @RabbitListener(bindings = @QueueBinding(value = @Queue, // 生成一个临时Queue
                                            exchange = @Exchange(name = "PubScrExchange", type = ExchangeTypes.FANOUT),
                                            key = {"publish", "subscribe"}))
    public void receive(String message) {
        System.out.println("收到消息如下:");
        System.out.println(message);
    }
}

2. Provider

    /**
     * Publish/Subscribe模式
     */
    @Test
    void publishExchange(){
        rabbitTemplate.convertAndSend("PubScrExchange","publish","我是Publish");
        rabbitTemplate.convertAndSend("PubScrExchange","subscribe","我是Subscribe");
    }

四、Routing

Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第3张图片
Routing模式也称之为路由模式,上一个模式是使用的Exchange的Fanout类型,将消息默认广播给所有相关联的Queue,在这个模式中就是有选择的将消息发送给Queue。
也就是Exchange的Direct类型,通过RoutingKey的方法来区分发送给哪些Queue。

具体实现如下

1. Consumer

/**
 * @author heshuai
 * @title: RoutingConsumer
 * @description: Routing模式,根据RoutingKey来选择性地将消息发送到某些Queue
 * @date 2021年02月09日 9:46
 */
@Component
public class RoutingConsumer {

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
                                            exchange = @Exchange(name = "DIRECT_ROUTING"), // 默认类型Direct
                                            key = {"info","warn"}))
    public void receive1(String message) {
        System.out.printf("receive1 收到消息如下:\n %s \n",message);
    }

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
            exchange = @Exchange(name = "DIRECT_ROUTING"), // 默认类型Direct
            key = {"info"}))
    public void receive2(String message) {
        System.out.printf("receive2 收到消息如下:\n %s \n",message);
    }
    @RabbitListener(bindings = @QueueBinding(value = @Queue,
            exchange = @Exchange(name = "DIRECT_ROUTING"), // 默认类型Direct
            key = {"error"}))
    public void receive3(String message) {
        System.out.printf("receive3 收到消息如下:\n %s \n",message);
    }
}

2. Provider

    /**
     * Routing模式
     */
    @Test
    void RoutingExchange() throws InterruptedException {
        rabbitTemplate.convertAndSend("DIRECT_ROUTING","info","我是info");
        rabbitTemplate.convertAndSend("DIRECT_ROUTING","warn","我是warn");
        rabbitTemplate.convertAndSend("DIRECT_ROUTING","error","我是error");
        TimeUnit.SECONDS.sleep(1L);
    }

五、Topics

Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第4张图片
Topics模式即是Exchange为topic,基于模式来匹配路由,这个模式中有两个通配符,*#,其中*代表一个单词,#代表零个或多个单词。

具体实现如下

1. Consumer

/**
 * @author heshuai
 * @title: TopicsConsumer
 * @description: Topics模式,模型匹配模式
 * @date 2021年02月09日 10:06
 */
@Component
public class TopicsConsumer {

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
                                            exchange = @Exchange(name = "TOPIC_TOPICS",type = ExchangeTypes.TOPIC),
                                            key = {"log.*"}))
    public void receive1(String message){
        System.out.printf("receive1 收到消息如下\n %s \n",message);
    }

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
            exchange = @Exchange(name = "TOPIC_TOPICS",type = ExchangeTypes.TOPIC),
            key = {"log.#"}))
    public void receive2(String message){
        System.out.printf("receive2 收到消息如下\n %s \n",message);
    }

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
            exchange = @Exchange(name = "TOPIC_TOPICS",type = ExchangeTypes.TOPIC),
            key = {"log.info"}))
    public void receive3(String message){
        System.out.printf("receive3 收到消息如下\n %s \n",message);
    }
}

2. Provider

    /**
     * Topics模式
     */
    @Test
    void TopicsExchange() throws InterruptedException {
        rabbitTemplate.convertAndSend("TOPIC_TOPICS","log.info","我是log.info");
        rabbitTemplate.convertAndSend("TOPIC_TOPICS","log.warn.system","我是log.warn.system");
        rabbitTemplate.convertAndSend("TOPIC_TOPICS","log.error","我是log.error");
        TimeUnit.SECONDS.sleep(1L);
    }

六、RPC

Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第5张图片

RPC模式上一节已经写的非常清晰了,使用Rabbitmq来实现RPC模式,实现发送与接收请求。

这部分内容参考自Spring官网

具体实现如下

1. Consumer

/**
 * @author heshuai
 * @title: RPCServer
 * @description: 在RPC模式中,消息的接受处理者一般也称之为服务端,接受处理响应请求的。
 * @date 2021年02月09日 10:22
 */
@Component
public class RPCServer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     *
     * @param task
     * @param channel
     * @throws IOException
     */
    @RabbitListener(bindings = @QueueBinding(value = @Queue,
                                            exchange = @Exchange(value = "RPCSERVER", type = ExchangeTypes.TOPIC),
                                            key = {"task.#"}))
    public void process(String taskMessage, Message task, Channel channel) throws IOException {
        try {
            // 获取任务
            System.out.printf("收到任务如下:\n %s \n",taskMessage);
            // 模拟处理任务
            taskMessage +=",该任务已被处理";
            // 得到CorrelationId
            String correlationId = task.getMessageProperties().getCorrelationId();
            // 得到回调Queue
            String replyTo = task.getMessageProperties().getReplyTo();
            MessageProperties messageProperties = new MessageProperties();
            messageProperties.setCorrelationId(correlationId);
            // 定义Message;
            Message replyMessage = new Message(taskMessage.getBytes(),messageProperties);
            // 回复Response结果
            rabbitTemplate.send(replyTo, replyMessage);
            // 手动确认消息
            channel.basicAck(task.getMessageProperties().getDeliveryTag(),false);
        }catch (IOException e){
            channel.basicNack(task.getMessageProperties().getDeliveryTag(),false,true);
            throw e;
        }

    }
}

2. Provider

    /**
     * RPC 模式
     */
    @Test
    void RPCClient() throws InterruptedException {
        // convertSendAndReceive: 发送请求并等待相应结果,同步
        // 在这个demo中 因为server发送回来的response是byte[]类型,所以这个回复也是byte[]
        Object response = rabbitTemplate.convertSendAndReceive("RPCSERVER","task.info","我是一个简简单单的任务");
        System.out.printf("收到回复如下:\n %s \n",new String((byte[]) response));
        TimeUnit.SECONDS.sleep(1L);
    }

注: 这里真的是非常非常简单的一个的demo了,用在生产下是远远不够的,只是辅助于理解Rabbitmq中的RPC模式,就像是本博客标题一样,初步学习,在文档中还有很多优化的实现,如异步处理返回消息,自定义MessageConvert,container容器等。

补充: 如果上面例子有看不懂的,可以留言,建议看一下上一节博客,其实这个和直接使用amqp-client没有太多区别,只是RabbitTemplate封装了消息转换,默认创建了correlationId和返回response 的临时queue来接受消息。就解释到这吧

七、Publisher Confirms

官方解释:可靠的消息发布

意思就是确保Client端的每一次消息都是可以确保到达Broker。

1.配置文件

spring:
  application:
    name: Rabbitmq-AMQP
  rabbitmq:
    host: 59.110.41.57
    port: 5672
    virtual-host: /test  # 虚拟主机
    username: admin
    password: admin
    publisher-returns: true # 是否激活发布者返回
    publisher-confirm-type: correlated  # 消息发布确认类型

2. Provider

/**
     * Publisher Confirms 模式
     */
    @Test
    void PublisherConfirmsClient() throws InterruptedException, TimeoutException, ExecutionException {
        // 为当前rabbitTemplate实例设置一个唯一的ConfirmCallback发布者确认异步回调方法
        this.rabbitTemplate.setConfirmCallback((CorrelationData correlationData, boolean ack,  String cause) ->{
            // 打印参数
            System.out.printf("\n CorrelationData:%s, \t cause:%s \n", JSON.toJSONString(correlationData),cause);
            if(ack){
                System.out.println("已确认");
            }else{
                System.out.println("消息未确认,需记录未确认原因及重新发送消息");
            }
        });
        String messageBody = "模拟发布者确认";
        Message message = new Message(messageBody.getBytes(),null);
        ReturnedMessage returned = new ReturnedMessage(message,1,null,null,null);
        // 发布者返回的数据,可以在这里记录发送的消息体,以备后续逻辑处理
        CorrelationData correlationData = new CorrelationData();
        correlationData.setReturned(returned);
        // 发送消息
        this.rabbitTemplate.convertAndSend("helloWord", (Object) "模拟发布者确认",correlationData);
        // 也可以不设置统一回调方法,而是使用这种简单的属性future来获取是否发布成功
        System.out.println(correlationData.getFuture().get(200,TimeUnit.MILLISECONDS).isAck());
        System.out.println(correlationData.getFuture().get(200,TimeUnit.MILLISECONDS).getReason());
        TimeUnit.SECONDS.sleep(1L);
    }

这里提供了两种方式,其实都是异步进行的,一种是统一为rabbitTemplate实例设置一个回调方法,等待发布确认返回后执行回调方法;第二种是通过CorrelationData 中的future属性来简单获取到是否发布成功及发布失败原因。

能力有限,这块感觉写的并不是很好,但是这个春节还有其他的学习计划,不深入了,有感兴趣的可以看一下Spring AMQP文档,以后我工作中用到或者有时间了也会复盘这部分内容的,到时候再来完善这篇文章。

八、Exchange的Header类型

这里就没有图了,使用Header类型其实和Direct类型类似,都是直接匹配,不过一个是关联路由key,一个是设置关联参数,这个设置对应的关联参数有一个需要注意的特殊值,就是x-match,它是用来定义Header的匹配形式,有两个值,分别是all(默认)、any,如果有多对k-v的参数的话,
x-match值为all,那么必须匹配所有k-v参数才可以路由到这个queue;
x-match值为any,那么只需要匹配其中任意一个k-v参数就可以路由到这个queue了。

大概就是上边这个意思,这个api还是很简单的,没有太多的注意点

具体实现如下

1. Consumer


/**
 * @author heshuai
 * @title: HeaderConsumer
 * @description: Exchange Header类型
 * @date 2021年02月12日 16:49
 */
@Component
public class HeaderConsumer {
    /**
     * arguments: Queue和Exchange绑定时所涉及到的参数,一般就是Exchange类型为Header才会用到
     * @Argument: 就是专门设置相对应的参数的,k-v的形式,
     * 在这个类型中,有一个特殊的参数就是x-match,它是用来定义Header的匹配形式,有两个值,分别是all(默认)、any,如果有多对k-v的参数的话,
     * 并且x-match值为all,那么必须匹配所有k-v参数才可以路由到这个queue;
     * x-match值为any,那么只需要匹配其中任意一个k-v参数就可以路由到这个queue了
     */
    @RabbitListener(bindings = @QueueBinding(value = @Queue,
                                            exchange = @Exchange(name = "EXCHANGE_HEADER", type = ExchangeTypes.HEADERS),
                                            arguments = {@Argument(name = "x-match", value = "all"),
                                                        @Argument(name = "key", value = "log.error"),
                                                        @Argument(name = "flag", value = "error")}))
    public void receive1(String message){
        System.out.printf("receive1 收到消息: %s \n",message);
    }

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
            exchange = @Exchange(name = "EXCHANGE_HEADER", type = ExchangeTypes.HEADERS),
            arguments = {@Argument(name = "x-match", value = "any"),
                    @Argument(name = "key", value = "log.info"),
                    @Argument(name = "flag", value = "log")}))
    public void receive2(String message){
        System.out.printf("receive2 收到消息: %s \n",message);
    }

    @RabbitListener(bindings = @QueueBinding(value = @Queue,
            exchange = @Exchange(name = "EXCHANGE_HEADER", type = ExchangeTypes.HEADERS),
            arguments = {@Argument(name = "x-match", value = "all"),
                    @Argument(name = "key", value = "info.error"),
                    @Argument(name = "flag", value = "info")}))
    public void receive3(String message){
        System.out.printf("receive3 收到消息: %s \n",message);
    }

}

2. Provider

 /**
     * Exchange Header类型
     */
    @Test
    void exchangeHeader() throws InterruptedException {
        this.rabbitTemplate.convertAndSend("EXCHANGE_HEADER","","key=info.error,flag=error消息",
                (Message message) ->{
                    MessageProperties messageProperties = message.getMessageProperties();
                    messageProperties.setHeader("key","info.error");
                    messageProperties.setHeader("flag","info");
                    return message;
                });
        this.rabbitTemplate.convertAndSend("EXCHANGE_HEADER","","key=我是瞎写的,flag=log消息",
                (Message message) ->{
                    MessageProperties messageProperties = message.getMessageProperties();
                    messageProperties.setHeader("key","我是瞎写的");
                    messageProperties.setHeader("flag","log");
                    return message;
                });
        this.rabbitTemplate.convertAndSend("EXCHANGE_HEADER","","key=log.error,flag=info消息",
                (Message message) ->{
                    MessageProperties messageProperties = message.getMessageProperties();
                    messageProperties.setHeader("key","log.error");
                    messageProperties.setHeader("flag","error");
                    return message;
                });
        TimeUnit.SECONDS.sleep(1L);
    }

九、Rabbitmq应用场景

这里简单介绍Rabbitmq的经典应用场景。

1 异步处理

举一个网上典型的例子吧
用户注册系统,一般这种系统需要在用户注册成功后给用户发送短信或者邮件,传统是有两种方式去解决,分别是串行、并行。
串行顾名思义就是一条线的去执行,在用户发起注册请求后,系统完成将注册信息录入到数据库中,这个时候其实已经完成了用户注册了,但是因为有额外的任务就是发送短信、邮件,只有这两步骤完成后才能返回给用户。这无疑让用户进行了无意义的等待,如下图:
Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第6张图片

并行方式就是将发送邮件和短信同时进行,这样就可以减少一个50ms的时间,但是这种方式其实也是有一些无意义的等待
Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第7张图片
使用broker消息中间件的方式异步处理,其实就是让用户不进行无意义的等待,写入数据库后立即返回。在返回前向broker发送一个请求即可
Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第8张图片
这样大大增加了效率。

2. 应用解耦

在正常的微服务中,一个系统是由多个子服务组成,可能完成一个业务逻辑就会经过很多的服务逻辑,这个时候这些服务之间就会强耦合在一起,比如A服务调用B服务,那么B服务因为一个特殊的原因失败(注:也可降级处理),就会导致A服务也随之失败!
那么在一些特殊的业务中,B服务不是必要的业务流程,或者不是需要立即执行的业务逻辑,那么可以允许B服务失败,A服务成功执行。后续再重新执行B服务这种情况
Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第9张图片
下面这种就是通过MQ进行的服务解耦
Rabbitmq消息中间件初步学习——第二节SpringBoot集成Rabbitmq_第10张图片
订单系统只需要将任务发送给了 消息队列即可任务时执行成功了,那么就可以给用户返回成功!这个时候哪怕库存系统执行失败,也不会应用最后的程序效果,因为当处理库存系统所遇到的错误,那么就可以重新将错误的任务从消息队列中订阅到,并且执行!

3. 流量削峰

这个主要针对于高并发的时候,在服务调用的时候,防止A服务调用B服务时太多的请求导致B服务负担不了,而使用消息中间件起到一个缓冲、限流的作用!

结语

这章也是非常基础的API应用,有些地方写的也不够好,主要也是因为目前没有对应的应用场景,以后工作中如果用到再来补充,感觉不管是Rabbitmq文档还是Spring文档,都是很清晰的!
下一节就是Rabbitmq 的集群部署!!!

你可能感兴趣的:(Rabbitmq,rabbitmq,中间件)