SpringBoot2.0集成RabbitMQ3.7.16示例Demo

准备工作

第一步:在SpringBoot的pom.xml中引入AMQP高级消息队列协议的依赖。



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

 

第二步:配置系统配置文件(根据需要)

 rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirms: true # 开启发送确认
    publisher-returns: true # 开启发送失败退回
    listener:
      direct:
        acknowledge-mode: manual      # 开启ACK
        retry:
          enabled: true # 消费者端的重试
      simple:
        retry:
          enabled: true # 消费者端的重试
        auto-startup: true  # 启动时自动启动容器
        default-requeue-rejected: true  # 投递失败时是否重新排队
        acknowledge-mode: manual      # 开启ACK
    template:
      reply-timeout: 10000 # 超时时间
      retry:
        enabled: true  # RabbitTemplate(生产端)实现重试
        initial-interval: 1000  # 第一次与第二次发布消息的时间间隔
        max-attempts: 3 # 尝试发布消息的最大数量
        max-interval: 10000  # 尝试发布消息的最大时间间隔
        multiplier: 1.0  # 上一次尝试时间间隔的乘数

 

第三步:配置Rabbitmq类(RabbitmqConfig)

ConnectionFactory为Connection的制造工厂。连接工厂,通过RabbitTemplate进行转发消息,接受信息。

开启ACK手动确认机制

要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者进行处理。

@Configuration
public class RabbitmqConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger("programLog");

    @Value("${spring.rabbitmq.host}")
    private String addresses;
    @Value("${spring.rabbitmq.port}")
    private int port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;
    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;
    @Value("${spring.rabbitmq.publisher-confirms}")
    private boolean publisherConfirms;
    @Value("${spring.rabbitmq.publisher-returns}")
    private boolean publisherReturns;

    @Autowired
    private QueueConfig queueConfig;

    /**
     * @Description: 连接工厂
     */
    @Bean
    public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(addresses, port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualHost);
        // 如果要进行消息发送确认回调,则这里必须要设置为true
        connectionFactory.setPublisherConfirms(publisherConfirms);
        // 如果要进行消息发送失败退回,则这里必须要设置为true
        connectionFactory.setPublisherReturns(publisherReturns);
        return connectionFactory;
    }

    /**
     * 因为要设置回调类,所以应是prototype类型
     */
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate template = new RabbitTemplate(this.connectionFactory());
        return template;
    }

    /**
     * 定义消息转换实例转化成 JSON 传输
     * 传输实体就可以不用实现序列化
     */
    @Bean
    public MessageConverter integrationEventMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public SimpleMessageListenerContainer messageContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
        container.setQueues(queueConfig.getQueueOne(), queueConfig.getQueueTwo(), queueConfig.getQueueThree(), queueConfig.getQueueFour());
        //将channel暴露给listener才能手动确认,AcknowledgeMode.MANUAL时必须为ture
        container.setExposeListenerChannel(true);
        //消费者的最大数量,并发消费的时候需要设置,且>=concurrentConsumers
        container.setMaxConcurrentConsumers(10);
        //消费者的最小数量
        container.setConcurrentConsumers(10);
        //在单个请求中处理的消息个数,他应该大于等于事务数量
        container.setPrefetchCount(1);
        //开启ACK手动确认机制
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
            try {
                // 通过basic.qos方法设置1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message
                channel.basicQos(1);
                LOGGER.info("ACK消费端接收到消息:" + message.getMessageProperties() + ":" + new String(message.getBody()));
                LOGGER.info("当前使用路由key:" + message.getMessageProperties().getReceivedRoutingKey());
                // deliveryTag:消息传送的次数,发布的每一条消息都会获得一个唯一的deliveryTag
                // multiple:批量确认标志,如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行确认; 如果值为false,则只对当前收到的消息进行确认
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e) {
                e.printStackTrace();
                if (message.getMessageProperties().getRedelivered()) {
                    LOGGER.info("消息已重复处理失败,拒绝再次接收...");
                    // deliveryTag:消息传送的次数,发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
                    // multiple:批量确认标志。如果值为true,包含本条消息在内的、所有比该消息deliveryTag值小的消息都被拒绝了(除了已经被 ack 的以外);如果值为false,只拒绝三条消息
                    // requeue:如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息
                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
                } else {
                    LOGGER.info("消息即将再次返回队列处理...");
                    // deliveryTag:消息传送的次数,发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
                    // requeue:如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
                }
            }
        });
        return container;
    }

}

第四步:配置生产者发送消息(RabbitSender)

方法实现RabbitTemplate.ConfirmCallback和RabbitTemplate.ReturnCallback的ack回调函数接口

  • 实现消息发送到RabbitMQ交换器后接收ack回调,如果消息发送确认失败就进行重试
  • 实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调,发送失败回调
  • 发送消息,convertAndSend 异步发送,消息是否发送成功用ConfirmCallback和ReturnCallback回调函数类确认
/**
 * 生产者
 */
@Service
public class RabbitSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private static Logger logger = LoggerFactory.getLogger("programLog");

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 实现RabbitTemplate.ConfirmCallback和RabbitTemplate.ReturnCallback的回调函数接口
     */

    /**
     * 实现消息发送到RabbitMQ交换器后接收ack回调,如果消息发送确认失败就进行重试
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("消息发送成功,消息ID:{}", correlationData.getId());
        } else {
            logger.info("消息发送失败,消息ID:{}", correlationData.getId());
        }
    }

    /**
     * 实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调,发送失败回调
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        logger.error("消息发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}", replyCode, replyText, exchange, routingKey, new String(message.getBody()));
    }

    /**
     * convertAndSend 异步发送,消息是否发送成功用ConfirmCallback和ReturnCallback回调函数类确认
     * 发送MQ消息
     */
    public void sendMessage(String exchangeName, String routingKey, Object message) {
        rabbitTemplate.convertAndSend(exchangeName, routingKey, message, new CorrelationData(UUID.randomUUID().toString()));
    }

}

 

 

第五步:配置消费队列Queue类(QueueConfig)
Queue是RabbitMQ的内部对象,用于存储消息。RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费。多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。

@Configuration
public class QueueConfig {

    /**
     * 队列的名字
     */
    private static final String QUEUE_ONE = "QUEUE_ONE";
    private static final String QUEUE_TWO = "QUEUE_TWO";
    private static final String QUEUE_THREE = "QUEUE_THREE";
    private static final String QUEUE_FOUR = "QUEUE_FOUR";

    /**
     * DIRECT_QUEUE   队列名字
     * durable="true" 是否持久化 rabbitmq 重启的时候不需要创建新的队列,保证绝大部分情况下RabbitMQ消息不会丢失
     * auto-delete    表示消息队列没有在使用时将被自动删除 默认是false
     * exclusive      表示该消息队列是否只在当前 connection 生效,默认是false
     */
    @Bean(name = QUEUE_ONE)
    public Queue getQueueOne() {
        return new Queue(QUEUE_ONE, true, false, false);
    }

    @Bean(name = QUEUE_TWO)
    public Queue getQueueTwo() {
        return new Queue(QUEUE_TWO, true, false, false);
    }

    @Bean(name = QUEUE_THREE)
    public Queue getQueueThree() {
        return new Queue(QUEUE_THREE, true, false, false);
    }

    @Bean(name = QUEUE_FOUR)
    public Queue getQueueFour() {
        return new Queue(QUEUE_FOUR, true, false, false);
    }

}

第六步:配置消费者MessageListener

确认消息已经消费成功。拒绝当前消息,并把消息返回原队列重新排队消费。

/**
 * 消费者
 */

@Component
public class MessageListener {

    private static Logger LOGGER = LoggerFactory.getLogger("programLog");

    @RabbitListener(queues = "QUEUE_ONE")
    @RabbitHandler
    public void queueOneMessage(String sendMessage, Channel channel, Message message) throws IOException {
        messageBasicAck(sendMessage, channel, message);
    }

    @RabbitListener(queues = "QUEUE_TWO")
    @RabbitHandler
    public void queueTwoMessage(String sendMessage, Channel channel, Message message) throws IOException {
        messageBasicAck(sendMessage, channel, message);
    }

    @RabbitListener(queues = "QUEUE_THREE")
    @RabbitHandler
    public void queueThreeMessage(String sendMessage, Channel channel, Message message) throws IOException {
        messageBasicAck(sendMessage, channel, message);
    }

    @RabbitListener(queues = "QUEUE_FOUR")
    @RabbitHandler
    public void queueFourMessage(String sendMessage, Channel channel, Message message) throws IOException {
        messageBasicAck(sendMessage, channel, message);
    }

    public void messageBasicAck(String sendMessage, Channel channel, Message message) throws IOException {
        try {
            Assert.notNull(sendMessage, "sendMessage 消息体不能为NULL");
            LOGGER.info("处理MQ消息");
            // 通过basic.qos方法设置prefetch_count=1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message
            channel.basicQos(1);
            LOGGER.info("Consumer {} Message :" + message);
            // 确认消息已经消费成功
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (IOException e) {
            LOGGER.error("MQ消息处理异常,消息ID:{},消息体:{}", message.getMessageProperties().getCorrelationId(), sendMessage, e);
            // 拒绝当前消息,并把消息返回原队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}

使用示例:

RabbitMQ常用的Exchange Type(交换机,消息转发器)有fanout、direct、topic、headers这四种。

Fanout

fanout类型的Exchange路由它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。

绑定规则:

SpringBoot2.0集成RabbitMQ3.7.16示例Demo_第1张图片

生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。

Fanout交换机绑定队列

创建交换机:所有发送到该Exchange的消息路由到所有与它绑定的Queue中

消息队列绑定到交换机:fanout类型的Exchange会无视binding key,将消息路由到所有绑定到该Exchange的Queue。

@Configuration
public class FanoutExchangeBindingQueue {

    private static final String FANOUT_EXCHANGE = "FANOUT_EXCHANGE"; // fanout交换机

    /**
     * 所有发送到该Exchange的消息路由到所有与它绑定的Queue中
     */
    @Bean(name = FANOUT_EXCHANGE)
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE, true, false);
    }

    /**
     * 将fanout队列和交换机进行绑定
     * fanout类型的Exchange会无视binding key,将消息路由到所有绑定到该Exchange的Queue
     */
    @Bean
    public Binding fanoutExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("FANOUT_EXCHANGE") FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(queueOne).to(fanoutExchange);
    }

    @Bean
    public Binding fanoutExchangeBindingQueueTwo(@Qualifier("QUEUE_TWO") Queue queueTwo, @Qualifier("FANOUT_EXCHANGE") FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(queueTwo).to(fanoutExchange);
    }

}

消息生成控制:注意交换机名称

@RequestMapping(value = "/fanoutSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void fanoutSend() {
     String message = "fanout 发送消息";
     rabbitSender.sendMessage("FANOUT_EXCHANGE", "", message);
}

 

Direct

direct类型的Exchange路由规则把消息路由到那些binding key与routing key完全匹配的Queue中

绑定规则:

SpringBoot2.0集成RabbitMQ3.7.16示例Demo_第2张图片

routingKey=” routingKey.one”发送消息到Exchange,则消息会路由到Queue1和Queue2;如果我们以routingKey=”a”或routingKey=”b”来发送消息,则消息只会路由到Queue2;如果以其他routingKey发送消息,则消息不会路由到这两个Queue中。

声明交换机和rountintkey: routing key,可绑定交换机转发消息到指定Queue

创建交换机:所有发送到该Exchange的消息路由到所有与它绑定的Queue中

消息队列绑定到交换机:把消息路由到那些binding key与routing key完全匹配的Queue中。

@Configuration
public class DirectExchangeBindingQueue {

    private static final String DIRECT_EXCHANGE = "DIRECT_EXCHANGE";    // direct交换机

    private static final String DIRECT_KEY = "routingKey.one";        // routing key,可绑定交换机转发消息到指定Queue


    /**
     * 所有发送到该Exchange的消息路由到所有与它绑定的Queue中
     */
    @Bean(name = DIRECT_EXCHANGE)
    public DirectExchange directExchange() {
        return new DirectExchange(DIRECT_EXCHANGE, true, false);
    }

    /**
     * 将direct队列和交换机进行绑定
     * 把消息路由到那些binding key与routing key完全匹配的Queue中
     */
    @Bean
    public Binding directExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("DIRECT_EXCHANGE") DirectExchange directExchange) {
        return BindingBuilder.bind(queueOne).to(directExchange).with(DIRECT_KEY);
    }

}

 

 

消息生成控制:注意交换机名称和rountingkey

 

@RequestMapping(value = "/directSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void directSend() {
    String message = "direct 发送消息";
    rabbitSender.sendMessage("DIRECT_EXCHANGE", "routingKey.one", message);
}

Topic

它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同:

  • routing key为一个句点号“. ”分隔的字符串(“. ”分隔开的每一段独立的字符串称为一个单词)
  • binding key与routing key一样也是句点号“. ”分隔的字符串
  • binding key存在两种特殊字符“*”与“#”,做模糊匹配,“*”匹配一个单词,“#”匹配多个/0个单词

绑定规则:

SpringBoot2.0集成RabbitMQ3.7.16示例Demo_第3张图片

routingKey=”lazy.brown.topic”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2;routingKey=”lazy.rabbit”的消息将会被丢弃,因为它们没有匹配任何routingKey。

声明交换机和rountintkey

将队列和topic交换机进行绑定

@Configuration
public class TopicExchangeBindingQueue {

    private static final String TOPIC_EXCHANGE = "TOPIC_EXCHANGE"; // topic交换机

    // 模糊匹配,“*”匹配一个单词,“#”匹配多个/0个单词
    private static final String TOPIC_KEY_ONE = "routingKey.#";
    private static final String TOPIC_KEY_TWO = "#.topic";
    private static final String TOPIC_KEY_THREE = "#";

    /**
     * 将消息路由到binding key与routing key相匹配的Queue中,匹配规则与direct有所不同
     * binding key存在两种特殊字符“*”与“#”,做模糊匹配,“*”匹配一个单词,“#”匹配多个/0个单词
     */
    @Bean(name = TOPIC_EXCHANGE)
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE, true, false);
    }

    /**
     * 将topic队列和交换机进行绑定
     */
    @Bean
    public Binding topicExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("TOPIC_EXCHANGE") TopicExchange topicExchange) {
        return BindingBuilder.bind(queueOne).to(topicExchange).with(TOPIC_KEY_ONE);
    }

    @Bean
    public Binding topicExchangeBindingQueueTwo(@Qualifier("QUEUE_TWO") Queue queueTwo , @Qualifier("TOPIC_EXCHANGE") TopicExchange topicExchange) {
        return BindingBuilder.bind(queueTwo).to(topicExchange).with(TOPIC_KEY_TWO);
    }

    @Bean
    public Binding topicExchangeBindingQueueThree(@Qualifier("QUEUE_THREE") Queue queueThree, @Qualifier("TOPIC_EXCHANGE") TopicExchange topicExchange) {
        return BindingBuilder.bind(queueThree).to(topicExchange).with(TOPIC_KEY_THREE);
    }

}

 

 

注意rountingkey的赋值:

第一条信息只能到第一消息队列排队消费

第二条信息只能到第二消息队列排队消费

三条信息都可以到第三消息队列排队消费

@RequestMapping(value = "/topicSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void topicSend() {
     String message = "topic 发送消息";
     rabbitSender.sendMessage("TOPIC_EXCHANGE", "rountingKey.key", message);
     rabbitSender.sendMessage("TOPIC_EXCHANGE", "key.topic", message);
     rabbitSender.sendMessage("TOPIC_EXCHANGE", "Key", message);
}

headers

headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。

创建交换机:对比消息的键值对是否完全匹配Queue与Exchange绑定时指定的键值对。

绑定消息队列

  • whereAll:当消息生产者传到Exchange中的headers中的键值对所有都符合(所有Map或所有Key)要求时,才启用该队列。
  •  whereAny:只要消息生产者传到Exchange中的headers中的键值对有至少一个(至少一个Map或至少一个Key)符合要求时,就启用该队列。
@Configuration
public class HeaderExchangeBindingQueue {

    private static final String HEADERS_EXCHANGE = "HEADERS_EXCHANGE"; // header交换机

    /**
     * 不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配
     * 对比消息的键值对是否完全匹配Queue与Exchange绑定时指定的键值对
     */
    @Bean(name = HEADERS_EXCHANGE)
    public HeadersExchange headersExchange() {
        return new HeadersExchange(HEADERS_EXCHANGE, true, false);
    }

    /**
     * 将headers队列和交换机进行绑定
     * whereAll:当消息生产者传到Exchange中的headers中的键值对所有都符合(所有Map或所有Key)要求时,才启用该队列
     */
    @Bean
    public Binding headersExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("HEADERS_EXCHANGE") HeadersExchange headersExchange) {
        Map map = new HashMap<>();
        map.put("headers1", "value1");
        map.put("headers2", "value2");
        return BindingBuilder.bind(queueOne).to(headersExchange).whereAll(map).match();
    }

    /**
     * whereAny:只要消息生产者传到Exchange中的headers中的键值对有至少一个(至少一个Map或至少一个Key)符合要求时,就启用该队列
     */
    @Bean
    public Binding headersExchangeBindingQueueTwo(@Qualifier("QUEUE_TWO") Queue queueTwo, @Qualifier("HEADERS_EXCHANGE") HeadersExchange headersExchange) {
        Map map = new HashMap<>();
        map.put("headers1", "value1");
        map.put("headers2", "value2");
        return BindingBuilder.bind(queueTwo).to(headersExchange).whereAny(map).match();
    }

}

 

生产消息控制

 

@RequestMapping(value = "/headersSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void headersSend() {
    String msg = "headers 发送消息";
    MessageProperties properties = new MessageProperties();
    properties.setHeader("headers1", "value1");
    properties.setHeader("headers2", "value2");
    Message message = new Message(msg.getBytes(), properties);
    rabbitSender.sendMessage("HEADERS_EXCHANGE", "", message);
}

参考资料:SO JSON在线解析《我为什么要选择RabbitMQ ,RabbitMQ简介,各种MQ选型对比》

版权所属:SO JSON在线解析

原文地址:https://www.sojson.com/blog/48.html

转载时必须以链接形式注明原始出处及本声明。

你可能感兴趣的:(RabbitMQ,&,Erlang)