在SpringBoot中使用RabbitMQ

开始之前

RabbitMQ涉及的重要的概念

Channel 信道。消息的读写操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话
Message 消息。应用程序和MQ服务之间传送的数据,消息可以非常简单,也可以很复杂。有Properties和body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;body就是消息体内容。
Exchange 交换器,一个MQ服务器可以有一个或多个交换器。应用程序通过交换器,按照一定路由规则将消息写入MQ的一个或多个队列中。其中,如果交换器路由不到队列,可以将消息返回给生产者,或者直接丢弃。RabbitMQ中的交换器类型有:Direct、topic、fanout、headers。
Binding 绑定。交换器和队列之间的虚拟映射规则,意思是将交换器和队列绑定在一起。在建立这个映射关系,需要由Routing Key去标识。
RoutingKey 路由键。交换器在接受到生产者发送的消息后,会根据RoutingKey去决定将消息发送到某个队列上。命名通常由一个"."分割的字符串,如"com.rabbitmq"。
Queue 队列,一个MQ服务可以有一个或多个队列。用来保存消息和供消费者读取消息。
push模式 rabbitMQ消费消息的模式之一。指MQ服务主动将消息推送给消费者程序,前提是消费者程序按照规定监听了MQ。
pull模式 rabbitMQ消费消息的模式之一。指消费者程序主动获取消息,假如消费者程序不需要使用MQ中的数据时,则不获取。

 

 

 

 

 

 

 

 

 

 

 

引入jar包


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

配置

# MQ连接地址
spring.rabbitmq.addresses=127.0.0.1
# MQ连接端口
spring.rabbitmq.port=5672
# 账号
spring.rabbitmq.username=guest
# 密码
spring.rabbitmq.password=guest

springboot-rabbitMQ提供了一个封装好的操作对象RabbitTemplate。

@Bean
public RabbitTemplate initrRabbitTemplate(ConnectionFactory connectionFactory) {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
    return rabbitTemplate;
}

编写生产者(provider)及消费者(consumer,push模式和pull模式)

1.Direct交换器类型(Direct Exchange)

a.配置

/**
 * 

直连型交换机(exchange)配置 * */ @Configuration public class DirectRabbitConfig { /**交换机:可以创建多个交换机.所以声明了三个**/ public static final String Exchange_A = "Exchange_A"; public static final String Exchange_B = "Exchange_B"; public static final String Exchange_C = "Exchange_C"; /**队列:一个MQ服务可以创建多个队列.所以声明了三个**/ public static final String Queue_A = "Queue_A"; public static final String Queue_B = "Queue_B"; public static final String Queue_C = "Queue_C"; // 生产者将消息发送给交换器的时候,会发送一个routingKey,用来指定路由规则,这样交换机就知道把消息发送到哪一个队列. // 通常路由键为一个"."分割的字符,比如:com.rabbitmq // 以下代码就是指定路由和队列绑定规则的 // return BindingBuilder.bind(队列).to(交换器).with(routingKey); // 一个交换机可以绑定多个消息队列,也就是消息通过一个交换机,可以分发到不同的队列当中去 /**路由键:交换机和队列之间需要路由键绑定,所以声明了三个**/ public static final String RoutingKey_A = "RoutingKey_A"; public static final String RoutingKey_B = "RoutingKey_B"; public static final String RoutingKey_C = "RoutingKey_C"; // 初始化队列 @Bean public Queue initQueueA() { // 构造函数中。第一个参数:指定队列的名称;第二个参数:是否持久化消息 return new Queue(Queue_A, true); } @Bean public Queue initQueueB() { return new Queue(Queue_B, true); } @Bean public Queue initQueueC() { return new Queue(Queue_C, true); } // 初始化一个 directExchange @Bean public DirectExchange initDirectExchange() { // 构造函数中.第一个参数:指定交换机;第二个参数:是否持久化;第三个参数:是否自动删除 return new DirectExchange(Exchange_A, true, false); } // 绑定交换器和队列 一个交换机可以绑定多个消息队列,也就是消息通过一个交换机,可以分发到不同的队列当中去。 @Bean public Binding initBindingA() { return BindingBuilder.bind(initQueueA()).to(initDirectExchange()).with(RoutingKey_A); } @Bean public Binding initBindingB() { return BindingBuilder.bind(initQueueB()).to(initDirectExchange()).with(RoutingKey_B); } @Bean public Binding initBindingC() { return BindingBuilder.bind(initQueueC()).to(initDirectExchange()).with(RoutingKey_C); } }

b.生产消息

@RunWith(SpringRunner.class)
@SpringBootTest
public class FirstRabbitMQTest {

	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	/**
	 * Direct 生产消息
	 * */
	@Test
	public void method() {
		Map map = new HashMap<>();
		map.put("name", "zepal");
		map.put("age", "18");
		map.put("gender", "男");
		map.put("timestamp", System.currentTimeMillis());
		String json = JSONObject.toJSONString(map);
		// 将消息发送到交换机(Exchange_A),指定路由键(RoutingKey_B)
		// 由DirectRabbitConfig配置中的绑定关系,可以确定消息会通过Exchange_A发送到Queue_B队列中
		rabbitTemplate.convertAndSend(DirectRabbitConfig.Exchange_A, DirectRabbitConfig.RoutingKey_B, json);
		System.out.println("ok");
	}
	
}

成功运行后,可以在RabbitMQ管理后台成功看到此条消息。

c.push模式消费

@Service
@RabbitListener(queues = DirectRabbitConfig.Queue_B)
public class DirectReceiver {

	/**
	 * 

除此之外,还可以创建多个消费者监听同一队列. *

如果多个消费者监听同一队列.会以轮询的方式对消息进行消费,而且不存在重复消费。 * @param message 从MQ中获取的信息.message参数类型需要和生产者投递的类型一致.如果生产者投递的类型是Map,那么message的类型也需要时Map,不能是String * */ @RabbitHandler public void process(String message) { System.out.println("DirectReceiver消费者收到消息 : " + message); } }

启动项目或项目处于运行中,会输出结果:

DirectReceiver消费者收到消息  : {"gender":"男","name":"zepal","age":"18","timestamp":1595830452670}

d.pull模式消费(注:如果在一个系统中测试,需要把push模式的消费者注释掉,不然启动单元测试会被push模式的消费者把消息消费掉之后,让下面的代码产生异常)

/**
	 * 

pull模式 手动拉取消息 * @throws IOException * @throws ClassNotFoundException * */ @Test public void methodD() throws IOException { // 获取一个MQ连接 Connection connection = connectionFactory.createConnection(); // 获取一个信道 Channel channel = connection.createChannel(false); // 参数1:表示从哪个队列中获取.参数2:是否自动确认 GetResponse getResponse = channel.basicGet(DirectRabbitConfig.Queue_B, false); byte[] body = getResponse.getBody(); // 消息体body转string String json = new String(body, "UTF-8"); System.out.println(json); // 转其他对象参考。这里的转换类型需要根据生产者投递类型决定 // ByteArrayInputStream in = new ByteArrayInputStream(body); // ObjectInputStream sIn = new ObjectInputStream(in); // Map map = (Map) sIn.readObject(); // 手动确认消息已正确消费,可以从队列中移除 channel.basicAck(getResponse.getEnvelope().getDeliveryTag(),false); }

运行单元测试会输出以下结果:

{"gender":"男","name":"zepal","age":"18","timestamp":1595831508608}

2.Topic交换器类型(Topic Exchange)

a.配置

/**
 * 

主体型交换机(Topic Exchange)配置 * */ @Configuration public class TopicRabbitConfig { /**声明一个交换机**/ public static final String Topic_Exchange = "Topic_Exchange"; /**声明一个队列**/ public static final String Topic_Queue = "Topic_Queue"; /**声明一个路由键**/ public static final String Topic_RoutingKey = "topic.#"; /** *

初始化交换机 * */ @Bean public TopicExchange initTopicExchange() { TopicExchange topicExchange = new TopicExchange(Topic_Exchange, true, false); return topicExchange; } /** *

初始化队列 * */ @Bean public Queue initQueue() { Queue queue = new Queue(Topic_Queue); return queue; } /** *

绑定交换机和队列 * */ @Bean public Binding initBinding() { return BindingBuilder.bind(initQueue()).to(initTopicExchange()).with(Topic_RoutingKey); } }

b.生产消息

/**
	 * Topic Exchange 生产者
	 * */
	@Test
	public void topicProviderTest() {
		Map map = new HashMap<>();
		map.put("name", "zepal");
		map.put("age", "18");
		map.put("gender", "男");
		map.put("timestamp", System.currentTimeMillis());
		String json = JSONObject.toJSONString(map);
		// 在配置中的路由键"topic.#"
		rabbitTemplate.convertAndSend(TopicRabbitConfig.Topic_Exchange, "topic.zepal", json);
		System.out.println("ok");
	}

执行成功后,可以在管理后台看到此条消息记录,如下。

说明

Topic Exchange和Direct Exchange。消息投递和消费过程差不多,但是Topic Exchange的特点是,它的RoutingKey在绑定过程中有一定规则。

*:英文星号。用来匹配一个单词,必须出现。示例:路由键声明为topic.*,那么生产消息时,推送的路由键能匹配的有,topic.xxx。topic/topic.xxx.yyy/xxx.topic不能被匹配

#:英文井号。用来匹配任意数量的单词(0个或多个)。示例:路由键声明为topic.#,那么生产消息时,发送到MQ的路由键可以匹配的有topic.xxx/topic.xxx.yyy/topic。

*和#还可以声明在最前面,比如:*.topic.*/#.topic.#/*.topic.#

c.消费消息

Topic Exchange的消费消息方式和Direct Exchange类似,区别就是Topic Exchange消费监听的队列也支持通配。通配规则和生产者一样。(如果把RoutingKey写死,就成了直连型交换器(Direct Exchange),直连型交换机名称的由来估计就是因为固定的RoutingKey,直接连接交换机与队列)。

3.Fanout交换器类型(Fanout Exchange)

扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
a.配置

/**
 * 

扇型交换机(Fanout Exchange)配置 * */ @Configuration public class FanoutRabbitConfig { /**声明一个交换机**/ public static final String Fanout_Exchange = "Fanout_Exchange"; /**声明一个队列**/ public static final String Fanout_Queue = "Fanout_Queue"; /** *

初始化一个交换机 * */ @Bean public FanoutExchange initFanoutExchange() { FanoutExchange fanoutExchange = new FanoutExchange(Fanout_Exchange); return fanoutExchange; } /** *

初始化一个队列 * */ @Bean public Queue initQueue() { Queue queue = new Queue(Fanout_Queue); return queue; } /** *

初始化绑定关系。Fanout Exchange不需要路由键绑定. *

同样一个交换机可以绑定多个队列,那么往一个交换机发送消息,多个队列都会收到 * */ @Bean public Binding initBinding() { return BindingBuilder.bind(initQueue()).to(initFanoutExchange()); } }

b.生产消息

/**
	 * Fanout Exchange 生产者
	 * */
	@Test
	public void fanoutProviderTest() {
		Map map = new HashMap<>();
		map.put("name", "zepal");
		map.put("age", "18");
		map.put("gender", "男");
		map.put("timestamp", System.currentTimeMillis());
		String json = JSONObject.toJSONString(map);
		// 不用路由键置null
		rabbitTemplate.convertAndSend(FanoutRabbitConfig.Fanout_Exchange, null, json);
		System.out.println("ok");
	}

c.消费消息
和Direct消费消息一样。

从处理过程上讲,处理速度:Fanout > Direct > Topic

消息确认机制

不管是在投递消息还是在消费消息的过程,都有可能因某些原因导致失败。所以RabbitMQ提供了消息确认机制。

1.投递消息确认

在配置文件中加入以下配置

# 投递确认:确认消息已发送到交换机(Exchange)
spring.rabbitmq.publisher-confirms=true
# 投递确认:确认消息已发送到队列(Queue)
spring.rabbitmq.publisher-returns=true

重新配置RabbitTemplate

@Bean
	public RabbitTemplate initrRabbitTemplate(ConnectionFactory connectionFactory) {
		RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
		// 设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
		rabbitTemplate.setMandatory(true);
		rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
			
			@Override
			public void confirm(CorrelationData correlationData, boolean ack, String cause) {
				System.out.println("ConfirmCallback(数据:correlationData) : " + correlationData);
                System.out.println("ConfirmCallback(确认情况:ack) : " + ack);
                System.out.println("ConfirmCallback(原因:cause) : " + cause);
                // TODO 业务扩展
			}
		});
		rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("ReturnCallback(消息:message):     " + message);
                System.out.println("ReturnCallback(回应码:replyCode):     " + replyCode);
                System.out.println("ReturnCallback(回应信息:replyText):     " + replyText);
                System.out.println("ReturnCallback(交换机:exchange):     " + exchange);
                System.out.println("ReturnCallback(路由键:routingKey):     " + routingKey);
                // TODO 业务扩展
            }
        });
		return rabbitTemplate;
	}

在重新配置RabbitTemplate过程中,加入了两个回调函数。

在消息投递过程中,大致可以分为以下几种情况:

a.消息投递到MQ服务,但是相关的交换机不存在或交换机和队列都不存在;

b.消息投递到MQ服务,交换器存在,但是队列不存在;

c.消息投递到MQ服务,交换机和队列都存在,消息投递成功;

在分别模拟以上3种场景过程中,不难发现以下情况:

a.交换机不存在,只调用ConfirmCallback函数,此时boolean ack参数输出false;

b.交换机存在,队列不存在,ConfirmCallback和ReturnCallback都被调用,此时boolean ack参数同样输出false,在ReturnCallback会输出其它相关信息;

c.当消息投递成功,只调用ConfirmCallback函数,此时boolean ack参数输出true。

2.消费消息确认

编写手动确认的消费者

@Service
public class AckReceiver implements ChannelAwareMessageListener {

	private static final Logger logger = LoggerFactory.getLogger(AckReceiver.class);
	
	@Override
	public void onMessage(Message message, Channel channel) throws Exception {
		// 消息在MQ中的唯一标识
		long deliveryTag = message.getMessageProperties().getDeliveryTag();
		try {
			byte[] body = message.getBody();// 获取消息体
			String json = new String(body, "UTF-8");
			System.out.println(json);// TODO 消费消息逻辑
			channel.basicAck(deliveryTag, true);// 手动确认消息
			
			// 如果要操作多个队列:1.可以创建多个多个消费者2.根据在当前消费者根据队列的不同做不同的操作,如下:
			// 获取队列
//			String consumerQueue = message.getMessageProperties().getConsumerQueue();
//			if("myQueueName".equals(consumerQueue)) {
//				// TODO 消费逻辑
//			}
			
		} catch (Exception e) {
			channel.basicReject(deliveryTag, false);// 拒绝消息
			// 除了basicReject();方法之外,还有basicNack()方法用于拒绝消息
			logger.error("消息者:AckReceiver消费失败。", e);
		}
		
	}
}

将上面的消费者加入监听

@Autowired
	private AckReceiver ackReceiver;
	
	/**
	 * 

手动确认 * */ @Bean public SimpleMessageListenerContainer initSimpleMessageListenerContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息 //设置一个队列:这是前面创建的扇型交换器中的队列 container.setQueueNames(FanoutRabbitConfig.Fanout_Queue); //如果同时设置多个如下: 前提是队列都是必须已经创建存在的,如果要操作多个队列进行手动确认,就需要配置多个 // container.setQueueNames("myQueueName1","myQueueName2","myQueueName3"); container.setMessageListener(ackReceiver); return container; }

在上面使用到的消息确认方法。详细说明如下:

Channel.basicAck(deliveryTag, boolean); 用于确认消息;表明已经正常消费消息。参数1:消息在队列中的唯一标识。

Channel.basicReject(deliveryTag, boolean);用于否定确认。一次只能拒绝一条消息。参数1:消息在队列中的唯一标识;参数2:true表示当前消息会重新回到队列中去,false表示直接丢掉消息。

Channel.basicNack(deliveryTag, boolean, boolean);用于否定确认。一次可以拒绝多条消息。参数1:消息在队列中的唯一标识;参数2:如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认;参数3:是否重新回到队列。

 

========================结束线================================

 

 

 

 

你可能感兴趣的:(rabbitmq使用笔记,rabbitmq,springboot,springboot-mq)