基於 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會重新放入隊列,這條消息默認排在已有消息的後面!!