Bootstrap

Spring Boot与RabbitMQ的整合

一、快速使用

1.1 引入依赖

在pom.xml中引入下述依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<!--<version>2.7.6</version>-->
		<!--<version>3.1.6</version>-->
		<version>2.1.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.bc</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencyManagement>
		<dependencies>
			<!-- 整合Spring Boot-->
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>2.1.3.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<!-- 整合Spring Cloud -->
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Greenwich.SR3</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<!-- 整合Spring Cloud Alibaba -->
			<dependency>
				<groupId>com.alibaba.cloud</groupId>
				<artifactId>spring-cloud-alibaba-dependencies</artifactId>
				<version>2.1.2.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>6.0.1.Final</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.0</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.8.25</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

1.2 项目配置文件

在resources目录下新建一个名为application.yml的文件: 

server:
  port: 10086

spring:
  application:
    name: demo
  rabbitmq:
    addresses: 127.0.0.1:5672
    username: admin
    password: admin
    publisher-confirms: true
    virtual-host: /
    listener:
      simple:
        prefetch: 1

1.3 创建消费者接收消息

package com.example.demo.message;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class MessageCustomer {

    @RabbitListener(queuesToDeclare = @Queue("myQueue"))
    public void receive(String content) {
        System.out.println("消费者消费消息:" + content);
    }
}

@RabbitListener(queuesToDeclare = @Queue("myQueue "))有下述三个作用:

  • queuesToDeclare能够实现当myQueue队列不存在的时候,自动创建
  • queuesToDeclare:后面的参数需要的是数组类型使用@Queue创建
  • 让该监听关联到所声明的队列(myQueue)中

1.4 创建生产者发送消息

package com.example.demo.controller;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/mq")
public class RabbitMQController {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @RequestMapping("/sendMessageToQueue")
    public void sendMessageToQueue() {
        amqpTemplate.convertAndSend("myQueue", "{'code':'200','msg':'测试数据发送'}");
    }
}

1.5 测试结果 

调用上述发送数据接口,可以看到消费者消费了数据:

消费者消费消息:{'code':'200','msg':'测试数据发送'}

二、创建Exchange(交换机)绑定Queue

我们要实现如果是小米的手机就发送到mobile_xiaomi_queue队列中,如果是华为的手机就发送到mobile_huawei_queue队列中,主要就是通过RoutingKey(mobile_xiaomi_key、mobile_huawei_key)来实现,消费者只有一个,即一个消费者从两个不同的队列中消费消息

2.1 创建常量

package com.example.demo.constant;

public interface MobileConstant {

    String MOBILE_EXCHANGE = "mobile_exchange";


    String MOBILE_XIAOMI_KEY = "mobile_xiaomi_key";
    String MOBILE_XIAOMI_QUEUE = "mobile_xiaomi_queue";

    String MOBILE_HUAWEI_KEY = "mobile_huawei_key";
    String MOBILE_HUAWEI_QUEUE = "mobile_huawei_queue";
}

2.2 创建消费者接收消息 

package com.example.demo.message;

import com.example.demo.constant.MobileConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class MessageCustomer {

    @RabbitListener(bindings = {@QueueBinding(exchange = @Exchange(MobileConstant.MOBILE_EXCHANGE), key = MobileConstant.MOBILE_HUAWEI_KEY, value = @Queue(MobileConstant.MOBILE_HUAWEI_QUEUE)),
            @QueueBinding(exchange = @Exchange(MobileConstant.MOBILE_EXCHANGE), key = MobileConstant.MOBILE_XIAOMI_KEY, value = @Queue(MobileConstant.MOBILE_XIAOMI_QUEUE))})
    public void process1(String message) {
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("消费者消费消息:" + message);
    }
}

2.3 创建生产者发送消息

package com.example.demo.controller;

import com.example.demo.constant.MobileConstant;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/mq")
public class RabbitMQController {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @RequestMapping("/sendMessageToExchange")
    public void sendMessageToExchange() {
        for (int i = 1; i <= 6; i++) {
            if (i <= 3) {
                amqpTemplate.convertAndSend(MobileConstant.MOBILE_EXCHANGE, MobileConstant.MOBILE_HUAWEI_KEY, "{'code':'200','msg':'华为'}");
            } else {
                amqpTemplate.convertAndSend(MobileConstant.MOBILE_EXCHANGE, MobileConstant.MOBILE_XIAOMI_KEY, "{'code':'200','msg':'小米'}");
            }
        }
    }
}

2.4 测试结果  

调用上述发送数据接口,分别往两个队列中发送数据,可以看到当两个队列中的数据都存在时,数据的消费是轮询处理。

消费者消费消息:{'code':'200','msg':'华为'}
消费者消费消息:{'code':'200','msg':'小米'}
消费者消费消息:{'code':'200','msg':'华为'}
消费者消费消息:{'code':'200','msg':'小米'}
消费者消费消息:{'code':'200','msg':'华为'}
消费者消费消息:{'code':'200','msg':'小米'}

三、消费并发与限流

默认情况下,一个listener对应着一个consumer,如果想要对于多个consumer,可以采用下述的方法实现。 

在yml配置文件中做下述的配置:

server:
  port: 10086

spring:
  application:
    name: demo
  rabbitmq:
    addresses: 127.0.0.1:5672
    username: admin
    password: admin
    publisher-confirms: true
    virtual-host: /
    listener:
      simple:
        prefetch: 1
        concurrency: 2 #消费端的监听个数(即被@RabbitListener注解所声明的监听者开启几个线程去处理数据)
        max-concurrency: 5 #消费端的监听最大数

此处concurrency和max-concurrency属于全局的配置,如果我们没有给@RabbitListener注解的concurrency属性进行赋值,那么这个配置就会生效。 若全局配置prefetch=1,concurrency=2,即每个监听者会开启2个线程去消费消息,每个线程都会抓取1个消息到内存中。max-concurrency表示每个监听者最大能启动的线程数。

  • 当所监听的队列中消息过多时,当前已有的线程无法及时进行消费,监听者就会逐步启动新的线程来处理,直达达到能启动的最大线程数(max-concurrency)
  • 当队列中的消息全部消费完以后,监听者就会逐步销毁空闲的线程,只保留默认配置的线程数。

@RabbitListener注解中关于消费并发的配置示例如下,表示默认开启2个线程,最大线程数为3

@RabbitListener(bindings = @QueueBinding(exchange = @Exchange(MobileConstant.MOBILE_EXCHANGE),
        key = MobileConstant.MOBILE_XIAOMI_KEY, value = @Queue(MobileConstant.MOBILE_XIAOMI_QUEUE)), concurrency = "2-3")
public void process(String message) {
    try {
        Thread.sleep(1000);
    } catch (Exception e) {
        e.printStackTrace();
    }
    log.info("消费者消费消息:" + message);
}

建议:一般情况下,一个listener对应一个consumer是够用的,只是针对部分场景,才需要考虑一对多。 

如果单位时间内,consumer到达的消息太多,也可能会把消费者压垮,因此可以通过配置 prefetch 来进行限流,即限制每一个消费者每次从队列中预获取(未消费)保存到内存中的数据条数。

spring:
  application:
    name: demo
  rabbitmq:
    addresses: 127.0.0.1:5672
    username: admin
    password: admin
    publisher-confirms: true
    virtual-host: /
    listener:
      simple:
        prefetch: 1

四、发布确认模式

4.1 发布确认原理

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置 basic.ack 的multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。

confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

4.2 发布确认配置

发布确认默认是没有开启的,我们可以在yml配置文件做下述配置进行开启:

server:
  port: 10086

spring:
  application:
    name: demo
  rabbitmq:
    addresses: 127.0.0.1:5672
    username: admin
    password: admin
    publisher-confirms: true
    virtual-host: /

publisher-confirms表示生产者是否开启异步确认机制,true表示开启

;