模擬RabbitMq消息丟失的幾種場景

基於 springboot 2.1.4

環境準備

▶ 導入rmq依賴

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

 

▶ properties配置rmq連接參數

spring.rabbitmq.host=114.215.83.3
spring.rabbitmq.port=5672
spring.rabbitmq.username=test
spring.rabbitmq.password=123456

 

▶ rmq配置類

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class RabbitConfig {

	// 定義exchange
	public static String HELP_EXCHANGE = "help-exchange";

	// 定義key
	public static String ROUTINGKEY_TEST_KEY = "queue_test_key";

	// 定義queue
	public static final String QUEUE_TEST = "queue_test";

	/**
	 * 配置rabbitTemplate
	 * 
	 * @param connectionFactory
	 * @return
	 */
	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
	public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
		RabbitTemplate template = new RabbitTemplate(connectionFactory);
		// 使用jackson序列化
		template.setMessageConverter(new Jackson2JsonMessageConverter());
		return template;
	}

	/**
	 * 配置exchange的bean
	 * 
	 * @return
	 */
	@Bean
	public DirectExchange exchange() {
		return new DirectExchange(HELP_EXCHANGE);
	}

	/**
	 * 配置queue的bean
	 * 
	 * @return
	 */
	@Bean
	public Queue queueHelp() {
		return new Queue(QUEUE_TEST, true); // 隊列持久
	}

	/**
	 * 將exchange、key以及queue綁定
	 * 
	 * @return
	 */
	@Bean
	public Binding binding(DirectExchange exchange, Queue queueHelp) {
		return BindingBuilder.bind(queueHelp).to(exchange)
				.with(RabbitConfig.ROUTINGKEY_TEST_KEY);
	}
}

 

▶ 消息生產者

爲了方便測試,將生產者定義成controller,可以通過訪問形式發送消息到mq

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.config.RabbitConfig;
import com.mote.entity.User;

@RestController
public class Producer {

	@Autowired
	private RabbitTemplate rabbitTemplate;

	@GetMapping("/mq")
	public void mqtest() {
		try {
			// 自定義一個對象
			User user = new User();
			user.setUsername("test").setPassword("123");

			// 發送rmq
			rabbitTemplate.convertAndSend(RabbitConfig.HELP_EXCHANGE,
					RabbitConfig.ROUTINGKEY_TEST_KEY, user);

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

 

▶ 消息消費者

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mote.config.RabbitConfig;
import com.mote.entity.User;
import com.rabbitmq.client.Channel;

@Component
public class Consumer {

	ObjectMapper mapper = new ObjectMapper();

	@RabbitHandler
	@RabbitListener(queues = RabbitConfig.QUEUE_TEST)
	public void process(Channel channel, Message message) throws Exception {
		try {
			// 將消息反序列化成對象
			User user = mapper.readValue(message.getBody(), User.class);
			System.out.println(user);

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

 

基礎環境測試沒問題後,接下來開始分析可能出現消息丟失的場景

 

場景一

交換機、隊列、消息未持久化,mq重啓後會出現消息丟失

 

 

▶ 交換機持久化(默認支持)

以下是交換機的bean配置,我們進入DirectExchange的源碼

 在DirectExchange的父類AbstractExchange中,我們找到exchange的new方法,發現默認是支持持久化的

▶ 隊列持久化(默認支持)

以下是隊列的bean配置,我們進入Queue的源碼

 在Queue類中,通過queue的new過程,我們也很好發現它是默認支持持久化的

 

▶ 消息<message>持久化(默認支持)

通過convertAndSend方法發送消息,進入這個方法一探究竟

 最後在doSend方法中,我們找到消息屬性的定義

進入MessageProperties這個類,找到DEFAULT_DELIVERY_MODE屬性,初始值是2,默認消息持久化

 

場景二

生產者發出的消息第一步是投遞到交換機,這一步可能因爲網絡原因導致失敗

 

通過實現ConfirmCallback接口,監聽消息是否成功到達交換機

▶ 實現ConfirmCallback接口

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class RmqConfirmCallback implements RabbitTemplate.ConfirmCallback {

	/**
	 * correlationData:消息唯一標識 
	 * ack:確認結果 true代表成功到達交換機,反之失敗 
	 * cause:失敗原因
	 */
	@Override
	public void confirm(CorrelationData correlationData, boolean ack,
			String cause) {
		System.out.println("UUID: " + correlationData.getId());
		if (ack)
			System.out.println("消息發送交換機成功!");
		else
			System.out.println("消息發送交換機失敗!原因" + cause);

	}
}

▶ 配置開啓confirm監聽(application.properties文件中添加)

spring.rabbitmq.publisher-confirms=true

▶ 修改rmq配置類,設置自定義的ConfirmCallback

▶ 修改生產者,添加消息標識

 

接下來開始測試

正常發送測試:啓動項目、訪問接口發送消息查看控制檯打印 

非正常測試,斷開網絡,重新發送,查看打印

 

場景三

消息正常投遞到交換機後,通過路由key路由到隊列的時候出現失敗

 

通過實現ReturnCallback接口,監聽消息是否正常投遞到隊列

 

▶ 實現ReturnCallback接口

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class RmqReturnCallback implements RabbitTemplate.ReturnCallback {

	@Override
	public void returnedMessage(Message message, int replyCode,
			String replyText, String exchange, String routingKey) {
		System.out.println("消息主體 message : " + message);
		System.out.println("消息主體 message : " + replyCode);
		System.out.println("描述:" + replyText);
		System.out.println("消息使用的交換器 exchange : " + exchange);
		System.out.println("消息使用的路由鍵 routing : " + routingKey);
	}
}

▶ 修改rmq配置類,設置自定義的ReturnCallback,並開啓監聽

▶ 修改生產者,定義一個rmq不存在的Key進行測試

啓動項目,訪問生產者,查看打印

 

場景三

消費者接收消息後,準備處理的時候,消費者掛了或者處理消息的邏輯出現異常都會導致消息的丟失

 

RabbitMQ提供的ack機制,消費端消費完成要通知服務端,服務端才把消息從內存刪除

 

▶ 開啓ack手動確認(application.properties中配置)

spring.rabbitmq.listener.simple.acknowledge-mode=manual

▶ 成功確認

void basicAck(long deliveryTag, boolean multiple)

       deliveryTag:該消息的index

       multiple:是否批量. true:將一次性ack所有小於deliveryTag的消息。

消費者成功處理後,調用channel.basicAck(message.getMessageProperties().getDeliveryTag(), false)方法對消息進行確認。

▶ 失敗確認

void basicNack(long deliveryTag, boolean multiple, boolean requeue)

        deliveryTag:該消息的index。

        multiple:是否批量. true:將一次性拒絕所有小於deliveryTag的消息。

        requeue:被拒絕的是否重新入隊列。

void basicReject(long deliveryTag, boolean requeue)

        deliveryTag:該消息的index。

        requeue:被拒絕的是否重新入隊列。

channel.basicNack 與 channel.basicReject 的區別在於basicNack可以批量拒絕多條消息,而basicReject一次只能拒絕一條消息。

 

▶ 修改消費者

tip:消息失敗確認時,如果參數requeue爲true會重新放入隊列,這條消息默認排在已有消息的後面!!

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章