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