在本文中直接进行介绍消息的生产和消费过程;
工程的开发基于Springboot和RabbitMQ
RabbitMQ交换机的模式为:Direct Mode
文章目录
在进行消息生产和消费之前需要在SpringBoot工程的pom.xml文件中,引入如下所示的依赖:
<!-- rabbitmq依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
1、消息生产端
1.1、配置文件
在application.properties文件中进行相关的配置,具体配置内容如下:
server.port=8889
# RabbitMQ producer configuration
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
# set timeout
spring.rabbitmq.connection-timeout=15000 # 连接超时时间(单位:ms)0表示无穷大,不超时
# RabbitMQ 生产端消息确认机制设置
# 确认消息已经发送到队列中
spring.rabbitmq.publisher-returns=true
# 确认消息已经发送到交换机中
spring.rabbitmq.publisher-confirm-type=correlated
# 热部署
spring.devtools.restart.enabled=true
1.2、交换机、消息队列配置文件
在RabbitMQConfig.java文件中进行交换机、队列的初始化配置,如下所示:
@Configuration
public class RabbitMQConfig {
private static final String EXCHANGE_NAME = "exchange_demo";
private static final String QUEUE_NAME = "queue_demo";
private static final String ROUTE_KEY = "routeKey_demo";
@Bean
public DirectExchange createDirectExchange() {
return new DirectExchange(EXCHANGE_NAME, true, false);
}
//name:交换机名称
//durable: 是否持久化
//autoDelete: 有过消费者,并且所有的消费者都取消订阅则自动删除
//DirectExchange(String name, boolean durable, boolean autoDelete)
@Bean
public Queue createQueue() {
return new Queue(QUEUE_NAME, true, false, false, null);
}
// name: 队列名称
// durable: 是否持久化
// exclusive: 是否只能在本次的连接中访问,一般为false
// autoDelete: 没有消费者的时候是否自动删除
// arguments: 参数配置
//Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
@Bean
public Binding createBinding() {
return BindingBuilder.bind(createQueue()).to(createDirectExchange()).with(ROUTE_KEY);
}
}
1.2.1 消息的持久化
如果要实现消息的持久化则exchange
、queue
和message
都要进行持久化,缺一不可,关于exchange
、queue
的持久化如上述的设置即可,至于message的持久化,可以来看一下源码:
rabbitTemplate.convertAndSend(...)
// 来看一下convertAndSend的实现方法
@Override
public void convertAndSend(String exchange, String routingKey, final Object object,
@Nullable CorrelationData correlationData) throws AmqpException {
send(exchange, routingKey, convertMessageIfNecessary(object), correlationData);
}
// 看下convertMessageIfNecessary方法
protected Message convertMessageIfNecessary(final Object object) {
if (object instanceof Message) {
return (Message) object;
}
return getRequiredMessageConverter().toMessage(object, new MessageProperties());
}
// 接下来看下toMessage方法中的MessageProperties类对象参数,在MessageProperties类中有一个属性
// MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;
// DEFAULT_DELIVERY_MODE默认的模式为持久化的,所以消息的持久化不需要用户来设置
1.3、消息的发送
在service层中进行消息的发送操作:
@Service
public class MessageService implements IMessageService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendMessage(Message message) {
String msgId = UUID.randomUUID().toString();
// CorrelationData :data to correlate publisher confirms. (在进行消息确认时必须要有)
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend("exchange_demo", "routeKey_demo", message, correlationData);
System.out.println("Send Message :消息已经发出!");
}
}
1.4、消息实体类
@Data
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String msgData;
}
1.5、生产端消息确认
实现生产端的消息确认需要添加如下配置文件,在application.properties中的配置入1.1所示;
@Configuration
public class MessageAckConfig {
@Autowired
private ConnectionFactory connectionFactory;
@Bean
public RabbitTemplate createRabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
/**
* 当mandatory标志位设置为true时 如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息
* 那么broker会调用basic.return方法将消息返还给生产者 当mandatory设置为false时,出现上述情况broker会直接将消息丢弃
* (必须进行配置,否则不能调用setReturnCallback方法)
*/
rabbitTemplate.setMandatory(true);
// 消息确认,一般消息成功与否都会触发该方法
rabbitTemplate.setConfirmCallback(new ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.err.println("ConfirmCallback 相关数据:" + correlationData);
System.err.println("ConfirmCallback 确认情况:" + ack);
System.err.println("ConfirmCallback 原因:" + cause);
}
});
// 消息已经推送到交换机但是找不到队列,就会触发ReturnCallback,进行消息的回退
rabbitTemplate.setReturnCallback(new ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange,
String routingKey) {
System.err.println("ReturnCallback: " + "消息:" + message);
System.err.println("ReturnCallback: " + "回应码:" + replyCode);
System.err.println("ReturnCallback: " + "回应信息:" + replyText);
System.err.println("ReturnCallback: " + "交换机:" + exchange);
System.err.println("ReturnCallback: " + "路由键:" + routingKey);
}
});
return rabbitTemplate;
}
}
参考博客:https://blog.csdn.net/qq_35387940/article/details/100514134
消息成功发送到队列上:
ConfirmCallback 相关数据:CorrelationData [id=cf378f0b-2ad9-499d-bc21-a18e07e5db26]
ConfirmCallback 确认情况:true
ConfirmCallback 原因:null
消息发送到broker时,找到exchange,而没有找到queue:
ReturnCallback: 消息:(Body:'[B@1b6abdf6(byte[111])' MessageProperties [headers={spring_returned_message_correlation=fe5a3286-0a31-41b2-9dc2-5c4ba9bd2d6e}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
ReturnCallback: 回应码:312
ReturnCallback: 回应信息:NO_ROUTE
ReturnCallback: 交换机:new_exchange
ReturnCallback: 路由键:routeKey_demo
ConfirmCallback 相关数据:CorrelationData [id=fe5a3286-0a31-41b2-9dc2-5c4ba9bd2d6e]
ConfirmCallback 确认情况:true
ConfirmCallback 原因:null
1.6、死信队列
实现死信队列只需在队列初始化时,在参数中添加一些配置,然后初始化一个死信队列即可;对1.2中的配置文件进行更改:
@Configuration
public class RabbitMQConfig {
private static final String EXCHANGE_NAME = "exchange_demo";
private static final String QUEUE_NAME = "queue_demo";
private static final String ROUTE_KEY = "routeKey_demo";
// Dead Letter
private static final String EXCHANGE_DL_NAME = "exchange_dl_timeout";
private static final String QUEUE_DL_NAME = "queue_dl_timeout";
private static final String ROUTE_KEY_DL = "routeKey_dl_timeout";
@Bean
public DirectExchange createDirectExchange() {
return new DirectExchange(EXCHANGE_NAME, true, false);
}
@Bean
public Queue createQueue() {
Map<String, Object> map = new HashMap<String, Object>();
// Set max-message-length of queue.
map.put("x-max-length", 30);
// Set time-to-live of all of messages in queue.(unit:ms)
map.put("x-message-ttl", 6000);
// Set dead-letter-exchange of queue.
map.put("x-dead-letter-exchange", EXCHANGE_DL_NAME);
// 必须配置路由,否则消息不会切换到死信交换器
// Set dead-letter-routing-key
map.put("x-dead-letter-routing-key", ROUTE_KEY_DL);
return new Queue(QUEUE_NAME, true, false, false, map);
}
@Bean
public Binding createBinding() {
return BindingBuilder.bind(createQueue()).to(createDirectExchange()).with(ROUTE_KEY);
}
/*********** Dead letter **************/
@Bean
public DirectExchange createDLDirectExchange() {
return new DirectExchange(EXCHANGE_DL_NAME, true, false);
}
// Create dead letter queue.
@Bean
public Queue createDLQueue() {
return new Queue(QUEUE_DL_NAME, true, false, false, null);
}
@Bean
public Binding createDLBinding() {
return BindingBuilder.bind(createDLQueue()).to(createDLDirectExchange()).with(ROUTE_KEY_DL);
}
}
死信队列的消费端与一般的没什么区别,本文就不赘述了。
死信相关概念:
- 死信是指无法被正常消费掉的消息,而存放这类消息的队列即称为消息队列;
- 出现死信的三点原因:
a、消息过了设定的time-to-live
时间;
b、消息队列达到了最大的长度(x-max-length
);
c、消息的消费者拒绝了消息的接收,同时将channel.basicReject(deliveryTag,requeue)
方法中的requeue
参数设置为false;
2、消息消费端
2.1、消费端配置文件
server.port=8888
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
# 初始化的消费者数量
spring.rabbitmq.listener.simple.concurrency=5
# 最大的消费者数量
spring.rabbitmq.listener.simple.max-concurrency=10
# 每个消息费者每次处理的信息个数
spring.rabbitmq.listener.simple.prefetch=1
# 如果配置文件中配置消息手动确认,需要将以下两个都设置为manual模式(手动消息确认模式),否则,就会出现unknown delivery tag 1的错误
# 消息确认模式有三种:NONE:自动确认;AUTO:根据情况确认;MANUAL:手动确认
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 热部署
spring.devtools.restart.enabled=true
2.2、配置文件及实现消费及消息确认
实现消费;
@Configuration
@Slf4j
public class MQListenerConfig {
/**
* @param message 消息内容
* @param channel
* @param headers
* @throws IOException
*/
@RabbitListener(bindings = @QueueBinding(//
exchange = @Exchange("exchange_demo"), //
value = @Queue("queue_demo"), //
key = "routeKey_demo"//
))
@RabbitHandler
public void receiveMessage(@Payload Message message, Channel channel, @Headers Map<String, Object> headers)
throws IOException {
System.err.println(message);
long deliveryTag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
/********** 消费端的消息确认 ************/
// 消息确认,false:只是对当前信息的确认
try {
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
log.error("MQListenerConfig exception message" + e.toString());
//错误示范: channel.basicNack(deliveryTag, false, true);
//正确示范: 可以将消息放入到死信队列中
channel.basicNack(deliveryTag, false, false);
}
// 重新将消息放回到队列中;true:重新将消息放回到消息队列中;false:消息放到私信队列中
// channel.basicNack(deliveryTag, false, true);
// 拒绝消息; true:重新将消息放回到消息队列中
// channel.basicReject(deliveryTag, true);
}
}
Note
接下来介绍下消费端的消息确认
,当消费者(订阅)注册后,消息将被 RabbitMQ 使用basic.deliver
方法推送;该方法带有一个deliveryTag
,deliveryTag
是一个单调增长的正整数,唯一地标识了一个通道(channel)上的消息交付,同时消息的确认必须和接收使用同一个channel
。
上述的配置中使用了
channel.basicNack
方法将message重新放回到消息队列中,同时将方法的requeue
参数设置为true
,这种方式message会被放置到消息队列的头部,从而来触发消息的再次消费,但是会导致一个问题,就是重试的死循环,所以不要在catch方法这样使用basicNack
方法。如果在catch中使用channel.basicNack
方法,可以将requeue
参数设置为false
,将消息发送到死信队列
中。
2.3、Message消費端限流
RabbitMQ消費端进行限流主要是使用到channel的一个方法:basicQos
,有三个重载方法,如下:
/*
* @param prefetchSize 接收消息的最大Size,以字节为单位,如果为0表示没有限制
* @param prefetchCount 一次处理消息的最大数量
* @param global false:只是对当前的消费者,true:对整个channel有效
*/
void basicQos(int prefetchSize, int prefetchCount, boolean global)
void basicQos(int prefetchCount, boolean global)
void basicQos(int prefetchCount)
接下来可以对2.2中消息消费逻辑进行修改:
@Configuration
@Slf4j
public class MQListenerConfig {
// ... 省略
@RabbitHandler
public void receiveMessage(@Payload Message message, Channel channel, @Headers Map<String, Object> headers)
throws IOException {
System.err.println(message);
long deliveryTag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
/* 这里设置prefetchCount值为2,意味着消费端每次能最多能接收2个Message,
在这2个Message没有被确认之前(即:使用了basicQos就必须使用basicAck进行消息确认),消费客户端不会再接收新的Message
*/
channel.basicQos(0,2,false);
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
log.error("MQListenerConfig exception message" + e.toString());
channel.basicNack(deliveryTag, false, false);
}
}
}