spring boot整合rabbitmq

第一部分:RabbitMQ介绍

RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。RabbitMQ主要是用来实现应用程序的异步和解耦,同时也能起到消息缓冲,消息分发的作用。当生产者大量产生数据时,消费者无法快速消费,那么需要一个中间层。保存这个数据。

AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。

RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

1.1 几个重要概念

一般的消息中间件的工作流程可以用消费者模式来表示。即消费者不断向消息队列发送消息,生产者不断从消息队列中取出消息进行消费,如下图所示:
spring boot整合rabbitmq_第1张图片

对于rabbitMQ来说,除了最基本的消费者,消息队列,生产者之外,还引进了一个新的模块,即交换机(Exchange)。它使得生产者和消息队列之间进行了隔绝,生产者只需要将消息发给交换机,有交换机根据一定的调度策略,将消息发送给相应的消息队列。RabbitMQ的 工作流程如下:
spring boot整合rabbitmq_第2张图片

至此,RabbitMQ的几个重要概念就出来了:
1.生产者:生产消息的应用程序
2.消费者:消费消息的应用程序
3.消息:一串二进制数据流
4.队列:消息的暂存/存储区
5.交换机:消息的中转站,用于接收分发消息, 有 fanout,direct,topic,headers四种类型
6.路由(绑定关系):将消息队列和交换机进行绑定(BindingKey),交换机才能将消息发送到指定队列。

下面重点介绍一下交换机:

1.Direct类型的交换机

Direct是RabbitMQ默认的交换机模式,也是最简单的模式.即创建消息队列的时候,指定一个BindingKey.当发送者发送消息的时候,指定对应的Key.当Key和消息队列的BindingKey一致的时候,消息将会被发送到该消息队列中.
spring boot整合rabbitmq_第3张图片

2.Topic类型的交换机

topic转发信息主要是依据通配符,队列和交换机的绑定主要是依据一种模式(通配符+字符串),而当发送消息的时候,只有指定的Key和该模式相匹配的时候,消息才会被发送到该消息队列中.
spring boot整合rabbitmq_第4张图片

3.Fanout类型的交换机

Fanout是路由广播的形式,将会把消息发给绑定它的全部队列,即便设置了key,也会被忽略.
spring boot整合rabbitmq_第5张图片

4.Headers类型的交换机

headers 也是根据规则匹配, 相较于 direct 和 topic 固定地使用 routing_key , headers 则是一个自定义匹配规则的类型. 在队列与交换器绑定时, 会设定一组键值对规则, 消息中也包括一组键值对( headers 属性), 当这些键值对有一对, 或全部匹配时, 消息被投送到对应队列.

至此,概念部分基本就完了。注意本地安装RabbitMQ的话,需要先安装Erlang,注意Erlang和rabbitMQ的版本不要差太多,否则rabbitMQ启动会报错(我安装的是RabbitMQ 3.6.5, Erlang 18.1)。

第二部分:spring boot 集成RabbitMQ

2.1 添加pom依赖

    
	    org.springframework.boot
	    spring-boot-starter-amqp
	 

2.2 application.properties中添加配置信息

spring.application.name=spirng-boot-rabbitmq-sender
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

#并发消费者的初始化值
spring.rabbitmq.listener.concurrency=1
#并发消费者的最大值
spring.rabbitmq.listener.max-concurrency=1
#每个消费者每次监听时可拉取处理的消息数量
spring.rabbitmq.listener.prefetch=1 

下面的配置,如果不是业务必须,都可以不配置,spring-amqp中都有默认配置

2.3 配置ConnectionFactory

这里的ConnectionFactory指的是spring-rabbit包下面的ConnectionFactory接口

@Configuration

public class MqProducerConfig {

    @Autowired

    @Bean

    public ConnectionFactory amqpConnectionFactory(ConnectionListener connectionListener,

                                                  RecoveryListener recoveryListener,

                                                  ChannelListener channelListener) {

        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();

        connectionFactory.setAddresses("localhost:5672");

        connectionFactory.setUsername("guest");

        connectionFactory.setPassword("guest");

        connectionFactory.setVirtualHost("/");

        connectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);

        connectionFactory.setChannelCacheSize(25);

        connectionFactory.setChannelCheckoutTimeout(0);

        connectionFactory.setPublisherReturns(false);

        connectionFactory.setPublisherConfirms(false);

        return connectionFactory;

    }

}

这里主要是配置一些连接信息,都比较容易理解,下面几个可能稍微解释一下:

setCacheMode:设置缓存模式,共有两种,CHANNEL和CONNECTION模式。

CHANNEL模式,程序运行期间ConnectionFactory会维护着一个Connection,所有的操作都会使用这个Connection,但一个Connection中可以有多个Channel,操作rabbitmq之前都必须先获取到一个Channel,否则就会阻塞(可以通过setChannelCheckoutTimeout()设置等待时间),这些Channel会被缓存(缓存的数量可以通过setChannelCacheSize()设置);

CONNECTION模式,这个模式下允许创建多个Connection,会缓存一定数量的Connection,每个Connection中同样会缓存一些Channel,除了可以有多个Connection,其它都跟CHANNEL模式一样

setPublisherReturns、setPublisherConfirms:producer端的消息确认机制(confirm和return),设为true后开启相应的机制,后文详述(3.1)

2.4 配置rabbitTemplate

这里主要是生产者发送消息时的一些配置信息。

@Bean
    public RabbitTemplate rabbitTemplate() {
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(@Nullable CorrelationData correlationData, boolean b, @Nullable String s) {
                if (b){
                    logger.error("消息发送至交换机成功:correlationData({}),ack({}),cause({})",correlationData,b,s);
                } else {
                    logger.error("消息发送至交换机失败:correlationData({}),ack({}),cause({})",correlationData,b,s);
                }
            }
        });

        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int i, String s, String s1, String s2) {
                logger.error("交换机无法将信息发送至队列:exchange({}),route({}),replyCode({}),replyText({}),message:{}",s1,s2,i,s,message);
            }
        });

        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        rabbitTemplate.setRetryTemplate(retryTemplate());
        return rabbitTemplate;
    }
    

这里,主要设置了发送消息时的转换机制(setMessageConverter),以及消息是否顺利到达队列的反馈机制。

setReturnCallback、setConfirmCallback:return和confirm机制的回调接口,后文详述(3.1)。
setMandatory:设为true使ReturnCallback生效。
setRetryTemplate:重试机制,后文详述(3.2)

2.5 配置RabbitListenerContainerFactory

这个类,主要是消费者接受信息时需要注意的一些点,消费者使用@RabbitListener注解接收信息,每一个注解的方法都会由这个RabbitListenerContainerFactory创建一个MessageListenerContainer,负责接收消息。

@Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory,connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setConcurrentConsumers(environment.getProperty("spring.rabbitmq.listener.concurrency",int.class));
        factory.setMaxConcurrentConsumers(environment.getProperty("spring.rabbitmq.listener.max-concurrency",int.class));
        factory.setPrefetchCount(environment.getProperty("spring.rabbitmq.listener.prefetch",int.class));
        factory.setDefaultRequeueRejected(true);
        factory.setErrorHandler(new ErrorHandler() {
            @Override
            public void handleError(Throwable throwable) {
                logger.error("接收消息异常:",throwable);
            }
        });
        return factory;
    }

factoryConfigurer.configure:设置spring-amqp的ConnectionFactory。

setMessageConverter:设置消费者端的消息转换器。

setAcknowledgeMode:设置consumer端的应答模式,共有三种:NONE、AUTO、MANUAL。

NONE,无应答,这种模式下rabbitmq默认consumer能正确处理所有发出的消息,所以不管消息有没有被consumer收到,有没有正确处理都不会恢复;

AUTO,由Container自动应答,正确处理发出ack信息,处理失败发出nack信息,rabbitmq发出消息后将会等待consumer端的应答,只有收到ack确认信息才会把消息清除掉,收到nack信息的处理办法由setDefaultRequeueRejected()方法设置,所以在这种模式下,发生错误的消息是可以恢复的。

MANUAL,基本同AUTO模式,区别是需要人为调用方法给应答。

setConcurrentConsumers:设置每个MessageListenerContainer将会创建的Consumer的最小数量,默认是1个。

setMaxConcurrentConsumers:设置每个MessageListenerContainer将会创建的Consumer的最大数量,默认等于最小数量。

setPrefetchCount:设置每次请求发送给每个Consumer的消息数量。

setDefaultRequeueRejected:设置当rabbitmq收到nack/reject确认信息时的处理方式,设为true,扔回queue头部,设为false,丢弃。

setErrorHandler:实现ErrorHandler接口设置进去,所有未catch的异常都会由ErrorHandler处理。

至此,一些准备配置基本都已经配置完毕,剩下的就是按照Rabbit MQ的工作流程,配置生产者,消费者,交换机,队列以及绑定关系。

2.6 配置消息队列

@Bean
    public Queue firstQueue(){
        return new Queue("first-queue",true);
    }

这里只是简单创建一个Queue。
name:队列名称
durable:持久化 rabbitmq重启的时候不需要创建新的队列)。

2.7 配置交换机

@Bean
    public DirectExchange directExchange(){
        DirectExchange exchange = new DirectExchange("directExchange1",true, false);
        return exchange;
    }

这里只配置了最简单的Direct类型的交换机,参数分别为:
name:交换机名字
durable:持久化 rabbitmq重启的时候不需要创建新的交换机
auto-delete:表示交换机没有在使用时将被自动删除 默认是false

2.8 配置绑定关系

@Bean
    public Binding bindingOne(){
        return BindingBuilder.bind(queueConfig.firstQueue()).to(exchangeConfig.directExchange()).with("test111");
    }

BindingBuilder.bind(队列).to(交换机).with(路由键(routing-key))。

2.9 配置生产者

@Component
public class Sender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg){
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend("directExchange1","test111",msg,correlationData);
    }
}

可以看到,生产者利用rabbitTemplate发送消息,参数分为为:
exchange:交换机名称
routingKey:路由键
msg:发送的消息
CorrelationData:消息的一个唯一标识,当消息发送失败时,可以根据这个标识找回消息做后续处理(见2.4 的confirmCallBack

2.10 配置消费者

@Component
public class Receiver {

    @RabbitListener(queues = "first-queue", containerFactory = "multiListenerContainer")
    public void handleMessage(Message msg, Channel channel) throws Exception{
        String o =new String(msg.getBody(),"UTF-8");
        Book book = JSONObject.parseObject(o, Book.class);
        channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
        System.out.println("接受到消息:"+ book.getAuthor());
    }
}

@RabbitListener:监听器,两个参数分布为监听的队列(需要注意的是,该队列必须已经有了绑定关系,否则会报错),以及RabbitListenerContainerFactory(见2.5)。当队列中有消息时,会触发该注解修饰的方法。具体实现过程见:@RabbitListener实现过程

方法中有两个参数,一个是Message,一个是Channel。

Message:Spring AMQP 对消息的封装,其 byte[] body 属性代表消息内容,MessageProperties messageProperties 属性代表消息属性。
Channel:RabbitMQ的连接通道。

当设置消费者手工确认消息时(见2.5 setAcknowledgeMode),消费者可以根据自己实际的业务需求确定是否成功消费信息,并且利用Channel的方法反馈回去。
注意2.5 setDefaultRequeueRejected 方法,设置了消息拒绝后,要不丢弃,要不扔回队列头。扔回队列头时,下一次消费还会是这条消息,所以这边可以先确认消息,再把消息发送到队列,这样就到队列尾了。

至此,配置的部分已经全部讲完,一个简单的消息队列也已经建立完成!

第三部分:一些改进

3.1生产者发送消息反馈机制

现在有一个问题,生产者向消息队列发送完消息后,它怎么知道消息有没有顺利的到达消息队列里面。如果没有到达,问题出现在哪里,怎么补救?

前面已经说过,生产者发送消息时,是通过rabbitTemplate来发送的。我们在配置rabbitTemplate的Bean的时候,可以指定两个callback:ReturnCallbackConfirmCallBack

rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String s) {
                //TODO SOMETHING
            }
        });

        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                //TODO SOMETHING
            }
        });

ConfirmCallback:每一条发出的消息都会调用ConfirmCallback;
ReturnCallback:只有在消息进入exchange但没有进入queue时才会调用。

如果消息没有到exchange,则confirm回调,ack=false  
如果消息到达exchange,则confirm回调,ack=true  
exchange到queue成功,则不回调return  
exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)

现在,我们已经知道了生产者发送消息何时反馈给我们,但是出问题后我们还没有补救措施。returnCallBack的回调信息中有很明显的Message以及交换机、路由等信息,我们可以直接再次尝试发送,但是confirmCallBack没有这些信息,我们该怎么处理呢?

通过观察我们发现,RabbitTemplate的convertAndSend方法中,有参数CorrelationData ,ConfirmCallBack中也有该参数,通过测试发现,对于同一条消息,它们是一致的。这下问题就有了解决方法了!

rabbitTemplate.convertAndSend(String exchange, String routingKey, Object object, @Nullable CorrelationData correlationData) throws AmqpException;

在发送信息的时候,我们可以自定义一个消息类,将发送的消息的信息(消息体,交换机,路由等)和CorrelationData绑定到一起。再继承一个ConcurrentHashMap 将关联关系保存到内存中。

public class MessageWithId {
    private String id;
    private String exchangeName;
    private String rountingKey;
    private Object msg;
  }

@Component
public class SendMessageMap extends ConcurrentHashMap{}

发送消息的时候,同时将关联关系保存一下。

@Component
public class Sender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private SendMessageMap hashMap;

    public void send(Object msg){
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        MessageWithId message = new MessageWithId();
        message.setExchangeName("directExchange1");
        message.setId(correlationData.getId());
        message.setMsg(msg);
        message.setRountingKey("test111");
        hashMap.put(correlationData.getId(),message);
        rabbitTemplate.convertAndSend("directExchange1","test111",msg,correlationData);
    }
}

ConfirmCallBack再根据回掉信息,以及关联关系,对消息进行相应处理。

rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String s) {
                if (ack){
                    logger.error("消息发送至交换机成功:correlationData({}),ack({}),cause({})",correlationData,b,s);
                    //成功则从内存中移除关联关系
                    hashMap.remove(correlationData.getId());
                    logger.error("消息移除成功!");
                } else {
                    logger.error("消息发送至交换机失败:correlationData({}),ack({}),cause({})",correlationData,b,s);
                    //失败则设置一个最大重试次数重新发送!
                    rabbitTemplate.convertAndSend();
                }
            }
        });

现在看起来好像是没什么问题了,但是直接在callBack中使用rabbitTemplate,会导致死锁!

解决方案也很简单,将CallBack剥离出来就行了,内部通过构造函数将rabbitTemplate引进来。

public class ConfirmCallBack implements RabbitTemplate.ConfirmCallback {

    private Logger logger = LoggerFactory.getLogger(ConfirmCallBack.class);

    private RabbitTemplate rabbitTemplate;

    private SendMessageMap hashMap;

    public ConfirmCallBack(RabbitTemplate rabbitTemplate, SendMessageMap hashMap) {
        this.hashMap = hashMap;
        this.rabbitTemplate = rabbitTemplate;
    }

    @Override
    public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String s) {
        try {
            MessageWithId message = (MessageWithId) hashMap.get(correlationData.getId());
            if (ack){
                logger.error("消息发送至交换机成功:correlationData({}),ack({}),cause({})",correlationData,ack,s);
                hashMap.remove(correlationData.getId());
            } else {
                logger.error("消息发送至交换机失败:correlationData({}),ack({}),cause({})",correlationData,ack,s);
                rabbitTemplate.convertAndSend("directExchange1",message.getRountingKey(),message.getMsg(),new CorrelationData(message.getId()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后rabbiteTemplate传进去参数。
rabbitTemplate.setConfirmCallback(new ConfirmCallBack(rabbitTemplate, hashMap));

将交换机名称故意写错,试一下能否成功。

2019-05-16 15:57:06.796 ERROR 8868 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory       : Channel shutdown: channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'directExchange2' in vhost '/', class-id=60, method-id=40)
2019-05-16 15:57:09.684 ERROR 8868 --- [nectionFactory1] c.r.sender.callBack.ConfirmCallBack      : 消息发送至交换机失败:correlationData(CorrelationData [id=1a63d944-d64d-4757-be91-88d2e9aea7d1]),ack(false),cause(channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'directExchange2' in vhost '/', class-id=60, method-id=40))
2019-05-16 15:57:13.107 ERROR 8868 --- [ 127.0.0.1:5672] c.r.sender.callBack.ConfirmCallBack      : 消息发送至交换机成功:correlationData(CorrelationData [id=1a63d944-d64d-4757-be91-88d2e9aea7d1]),ack(true),cause(null)

可以看到,大功告成!其实这里只是演示了一个最简单的处理方案。其他方案比如,设置最大重试次数,依然失败记录信息,清除内存中的数据。或者将失败的信息保存起来,利用调度重新发送等等。

另外,这里主要写了ConfirmCallBack,另一个ReturnCallBack也是一样的处理方案。

3.2 消息发送过程中,rabbitMQ断开连接怎么办?

在2.4将配置rabbitTemplate的时候,加入了重试机制,重试机制针对的是网络不稳定导致的连接中断的问题,而不是消息的重发。

rabbitTemplate.setRetryTemplate(retryTemplate());

@Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(Integer.MAX_VALUE);
        retryTemplate.setRetryPolicy(simpleRetryPolicy);
        return retryTemplate;
    }

连接断开的时候,生产者消息发不出去,就会触发3.1我们讲的东西,这个时候,我们可以把发送失败的消息存起来,利用调度去定时跑这些任务。

3.3 消费者对消息的处理

首先,对rabbitMQ消费过程不是很清楚的话,可以参考spring-rabbit消费过程解析 以及 @RabbitListener实现过程

在2.5我们讲到,消费者端对消息有三种应答方式,这里我们选择人工处理(AcknowledgeMode.MANUAL)。原因是可以防止rabbitMQ消息丢失。

MANUAL模式下,Broker对消费者拿走的消息,会处于unack(未应答)模式,只有当消费者端手工应答ack的时候,才会从消息队列里面删除。

如果由于程序异常,或者消费者自己拒绝应答(basicNack)消息,该消息会按照我们2.5配置的策略扔回队列头部或者丢弃。
针对多个消费者消费同一个队列时(多个@RabbitListener),被丢弃的消息可能会被其他消费者捕获到。
针对单一消费者,扔回头部的消息又会被自己捕捉到,从而照成死循环。
解决方案有:
1.设置最大重试次数,超过后记录详细信息,返回消费成功让消息队列删除该消息。
2.引入类似死信队列,将nack的消息放入该队列,不影响正常队列消费。

3.4 死信队列

DLX, Dead-Letter-Exchange。
将一个queue设置了x-dead-letter-exchange及x-dead-letter-routing-key两个参数后,这个queue里丢弃的消息将会进入dead letter exchange,并route到相应的queue里去。
利用DLX, 当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。消息变成死信一向有一下几种情况:

  • 消息被拒绝(basic.reject/ basic.nack)并且requeue=false

  • 消息TTL过期

  • 队列达到最大长度

DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列,可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ 3.0以前支持的immediate参数(可以参考RabbitMQ之mandatory和immediate)的功能。

最后的话

作者也是第一次在实际项目中使用rabbitMQ,可能有很多地方表述不清晰,或者有误,希望大家指正!

参考链接

SpringBoot整合RabbitMQ之 典型应用场景实战一

springboot学习笔记-6 springboot整合RabbitMQ

Springboot整合一之Springboot整合RabbitMQ

Spring整合rabbitmq实践(一):基础使用配置

Spring整合rabbitmq实践(二):扩展功能

你可能感兴趣的:(spring,boot,rabbitMQ,入门)