模拟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会重新放入队列,这条消息默认排在已有消息的后面!!

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值