文章目录
- 1. RabbitMQ小结
- 2. 引入依赖
- 3. RabbitMQ配置
- 4. 消息生产者与消费者
- 4.1 生产者:发送字符串、实体类
- 4.2 生产者:发送Message对象封装后的消息
- 4.2.1 发送org.springframework.messaging.Message消息
- 4.2.2 发送org.springframework.amqp.core.Message消息
- 4.3 消费者手动ack确认
- 4.4 死信队列处理拒绝签收的消息
1. RabbitMQ小结
下述代码主要实现:
- 创建了消息转换器使用Jackson2, 这样消息在RabbitMQ的管理页面中就可以看到json结构的数据;
- 配置了RabbitAdmin,主要用来创建交换机、队列、路由键;
- 创建三种类型交换机:Direct、Fanout、Topic,同时创建多个队列与其绑定;
- 配置了rabbitTemplate生产者的confirm与returns消息确认回调(注意配置yml文件)
- 生产者发送的消息体:
(1)直接发送字符串String、对象Object
(2)消息体使用org.springframework.messaging.Message 封装发送;
(3)消息体使用org.springframework.amqp.core.Message 封装发送 - 配置了rabbitTemplate消费者的手动ack确认
- 死信队列处理消息未签收的消息
2. 引入依赖
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3. RabbitMQ配置
SpringBoot启动自动创建Queue、Exchange、Binding
server:
port: 8080
spring:
#给项目来个名字
application:
name: rabbitmq-provider
#配置rabbitMq 服务器
rabbitmq:
host: ip
port: port
username: name
password: password
publisher-returns: true
#如果没有抵达队列,以异步发送优先回调returns确认
template:
mandatory: true
publisher-confirm-type: simple
import com.qs.parentchild2common.entity.constant.RabbitConfigConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@Configuration
public class RabbitMQConfig {
@Autowired
private RabbitAdmin rabbitAdmin;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 消息转换器修改
* 有了这项配置后, 消息在rabbitMq的管理页面就可以显示看到消息的json数据,否则是序列化的数据
*
* @return
*/
@Bean
public MessageConverter jackJsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 配置rabbitAdmin
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
//只有设置为 true, spring才会加载RabbitAdmin这个类
rabbitAdmin.setAutoStartup(true);
return rabbitAdmin;
}
/**
* 队列、交换机的创建
*/
@Bean
public void createExchangeQueue() {
//direct类型交换机连接三个队列
createDirectExchangeQueue();
//fanout类型交换机
createFanoutExchangeQueue();
//topic类型交换机
createTopicExchangeQueue();
}
/**
* 配置rabbitTemplate消息确认回调
*/
@PostConstruct
private void initRabbitTemplate() {
/**
* 消息发送到交换机Exchange失败时, 回调
*/
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("确认ack:{} 消息correlationDataID:{}消息发送到交换机成功", ack, correlationData.getId());
return;
}
log.error("确认ack:{} 消息correlationDataID:{}发送到交换机失败 case:{}", ack, correlationData.getId(), cause);
}
});
/**
* 消息从交换机Exchange-->队列Queue失败时, 优先回调
*/
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
Integer replyCode = returned.getReplyCode();
String replyText = returned.getReplyText();
String exchange = returned.getExchange();
String routingKey = returned.getRoutingKey();
Message message = returned.getMessage();
log.error("correlationDataID:{}发送到队列失败 replyCode:{} replyText:{} exchange:{} routingKey:{}",
message.getMessageProperties().getHeaders().get("spring_returned_message_correlation"),
replyCode, replyText, exchange, routingKey);
}
});
}
void createDirectExchangeQueue() {
DirectExchange directExchange = directExchange();
Queue queue1 = queue1();
Queue queue2 = queue2();
Queue queue3 = queue3();
Binding binding1 = BindingBuilder.bind(queue1).to(directExchange).with("queue1Key");
Binding binding2 = BindingBuilder.bind(queue2).to(directExchange).with("queue1Key");
Binding binding3 = BindingBuilder.bind(queue3).to(directExchange).with("queue3Key");
rabbitAdmin.declareExchange(directExchange);
rabbitAdmin.declareQueue(queue1);
rabbitAdmin.declareQueue(queue2);
rabbitAdmin.declareQueue(queue3);
rabbitAdmin.declareBinding(binding1);
rabbitAdmin.declareBinding(binding2);
rabbitAdmin.declareBinding(binding3);
}
void createFanoutExchangeQueue() {
FanoutExchange fanoutExchange = fanoutExchange();
Queue queue4 = queue4();
Queue queue5 = queue5();
Queue queue6 = queue6();
Binding binding4 = BindingBuilder.bind(queue4).to(fanoutExchange);
Binding binding5 = BindingBuilder.bind(queue5).to(fanoutExchange);
Binding binding6 = BindingBuilder.bind(queue6).to(fanoutExchange);
rabbitAdmin.declareExchange(fanoutExchange);
rabbitAdmin.declareQueue(queue4);
rabbitAdmin.declareQueue(queue5);
rabbitAdmin.declareQueue(queue6);
rabbitAdmin.declareBinding(binding4);
rabbitAdmin.declareBinding(binding5);
rabbitAdmin.declareBinding(binding6);
}
void createTopicExchangeQueue() {
TopicExchange topicExchange = topicExchange();
Queue queue4 = queue4();
Queue queue5 = queue5();
Queue queue6 = queue6();
Binding binding4 = BindingBuilder.bind(queue4).to(topicExchange).with(RabbitConfigConstant.QUS_ROUTING_TOPIC_KEY_1);
Binding binding5 = BindingBuilder.bind(queue5).to(topicExchange).with(RabbitConfigConstant.QUS_ROUTING_TOPIC_KEY_2);
Binding binding6 = BindingBuilder.bind(queue6).to(topicExchange).with(RabbitConfigConstant.QUS_ROUTING_TOPIC_KEY_3);
rabbitAdmin.declareExchange(topicExchange);
rabbitAdmin.declareQueue(queue4);
rabbitAdmin.declareQueue(queue5);
rabbitAdmin.declareQueue(queue6);
rabbitAdmin.declareBinding(binding4);
rabbitAdmin.declareBinding(binding5);
rabbitAdmin.declareBinding(binding6);
}
/**
* 队列
* durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
* exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
* autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
*
* @return
*/
private Queue queue1() {
return new Queue(RabbitConfigConstant.QUS_QUEUE_1, true);
}
private Queue queue2() {
return new Queue(RabbitConfigConstant.QUS_QUEUE_2, true);
}
private Queue queue3() {
return new Queue(RabbitConfigConstant.QUS_QUEUE_3, true);
}
private Queue queue4() {
return new Queue(RabbitConfigConstant.QUS_QUEUE_4, true);
}
private Queue queue5() {
return new Queue(RabbitConfigConstant.QUS_QUEUE_5, true);
}
private Queue queue6() {
return new Queue(RabbitConfigConstant.QUS_QUEUE_6, true);
}
//交换机
private DirectExchange directExchange() {
return new DirectExchange(RabbitConfigConstant.DIRECT_EXCHANGE, true, false);
}
private FanoutExchange fanoutExchange() {
return new FanoutExchange(RabbitConfigConstant.FANOUT_EXCHANGE, true, false);
}
private TopicExchange topicExchange() {
return new TopicExchange(RabbitConfigConstant.TOPIC_EXCHANGE, true, false);
}
}
4. 消息生产者与消费者
4.1 生产者:发送字符串、实体类
- 通用的消息推送封装
import com.qs.parentchild2common.entity.constant.RabbitConfigConstant;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author QuS
* @date 2021/6/25 13:46
*/
@Service
public class RabbitMQProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @param object 消息体
*/
public void sendMessageToMQ(Object object, String msgId) {
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(RabbitConfigConstant.TOPIC_EXCHANGE, "123.goods", object, correlationData);
}
}
- 消息发送测试方法
@RequestMapping("/sendTopicMessage")
public String sendTopicMessage(Integer num) {
RabbitMQBO rabbitMQBO;
UserBO userBO;
for (int i = 0; i < num; i++) {
String id = "ID" + i;
if (i % 2 == 0) {
rabbitMQBO = new RabbitMQBO();
rabbitMQBO.setId(i);
rabbitMQBO.setUuid(UUID.randomUUID().toString());
rabbitMQBO.setMsg("这是生产的消息" + i);
rabbitMQProducer.sendMessageToMQ(rabbitMQBO, id);
}
if (i % 2 == 1) {
userBO = new UserBO();
userBO.setAge(20 + i);
userBO.setPassword("password" + i);
userBO.setUsername("小明" + i);
rabbitMQProducer.sendMessageToMQ(userBO, id);
}
rabbitMQProducer.sendMessageToMQ("测试字符串" + i, id);
}
return "ok";
}
- 消费者
注意消费者也需要配置和生产者相同的消息转换器:Jackson2JsonMessageConverter()
import com.alibaba.fastjson.JSONObject;
import com.qs.parentchild2common.entity.bo.RabbitMQBO;
import com.qs.parentchild2common.entity.bo.UserBO;
import com.qs.parentchild2common.entity.constant.RabbitConfigConstant;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* @author QuS
* @date 2021/6/22 14:20
*/
@Component
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = RabbitConfigConstant.TOPIC_EXCHANGE, type = ExchangeTypes.TOPIC),
value = @Queue(value = RabbitConfigConstant.QUS_QUEUE_4, durable = "true"),
key = "#.goods"
))
public class RabbitMQConsumer1 {
/**
* 如果发送端使用的是Message,接收端使用@RabbitHandler注解接受Message会报错
*
* @param
* @param channel
* @throws IOException
*/
@RabbitHandler
public void process1(@Payload RabbitMQBO rabbitMQBO, @Headers Map<String, Object> headers, Channel channel) throws IOException {
System.out.println("消费者_1: RabbitMQBO: " + JSONObject.toJSONString(rabbitMQBO) + " Header: " + JSONObject.toJSONString(headers));
}
@RabbitHandler
public void process2(@Payload String str, @Headers Map<String, Object> headers) {
System.out.println("消费者_2: String: " + JSONObject.toJSONString(str) + " Header: " + JSONObject.toJSONString(headers));
}
@RabbitHandler
public void process3(@Payload UserBO userBO, @Headers Map<String, Object> headers) {
System.out.println("消费者_3: UserBO: " + JSONObject.toJSONString(userBO) + " Header:" + JSONObject.toJSONString(headers));
}
}
- 输出结果
生产者回调输出
2021-06-25 14:31:25.924 INFO 21992 --- [nectionFactory2] c.q.p.config.RabbitMQConfig : 确认ack:true 消息correlationDataID:ID0消息发送到交换机成功
2021-06-25 14:31:25.924 INFO 21992 --- [nectionFactory1] c.q.p.config.RabbitMQConfig : 确认ack:true 消息correlationDataID:ID0消息发送到交换机成功
2021-06-25 14:31:25.926 INFO 21992 --- [nectionFactory1] c.q.p.config.RabbitMQConfig : 确认ack:true 消息
......
消费者输出
消费者_1: RabbitMQBO: {"id":0,"msg":"这是生产的消息0","uuid":"8b8c385c-d0d6-4176-9ffc-7421d8215b6c"} Header: {"amqp_receivedDeliveryMode":"PERSISTENT","amqp_receivedExchange":"qus_topic_exchange","amqp_deliveryTag":1,"amqp_consumerQueue":"qus-queue-4","amqp_redelivered":false,"amqp_receivedRoutingKey":"123.goods","amqp_contentEncoding":"UTF-8","spring_listener_return_correlation":"449d172c-89be-455e-9d4a-107967a067e3","spring_returned_message_correlation":"ID0","id":"f7701b20-565c-2487-5223-eb66bfdb3d0c","amqp_consumerTag":"amq.ctag-hLAmFEcm6YIyp-hXlicXqA","amqp_lastInBatch":false,"contentType":"application/json","__TypeId__":"com.qs.parentchild2common.entity.bo.RabbitMQBO","timestamp":1624602705686}
消费者_2: String: "测试字符串0" Header: {"amqp_receivedDeliveryMode":"PERSISTENT","amqp_receivedExchange":"qus_topic_exchange","amqp_deliveryTag":2,"amqp_consumerQueue":"qus-queue-4","amqp_redelivered":false,"amqp_receivedRoutingKey":"123.goods","amqp_contentEncoding":"UTF-8","spring_listener_return_correlation":"449d172c-89be-455e-9d4a-107967a067e3","spring_returned_message_correlation":"ID0","id":"caccf01c-6cfa-adcf-e11a-6030caa42251","amqp_consumerTag":"amq.ctag-hLAmFEcm6YIyp-hXlicXqA","amqp_lastInBatch":false,"contentType":"application/json","__TypeId__":"java.lang.String","timestamp":1624602705738}
消费者_3: UserBO: {"age":21,"password":"password1","username":"小明1"} Header:{"amqp_receivedDeliveryMode":"PERSISTENT","amqp_receivedExchange":"qus_topic_exchange","amqp_deliveryTag":3,"amqp_consumerQueue":"qus-queue-4","amqp_redelivered":false,"amqp_receivedRoutingKey":"123.goods","amqp_contentEncoding":"UTF-8","spring_listener_return_correlation":"449d172c-89be-455e-9d4a-107967a067e3","spring_returned_message_correlation":"ID1","id":"fa5b8cdb-d092-0e58-8c28-712cae04078f","amqp_consumerTag":"amq.ctag-hLAmFEcm6YIyp-hXlicXqA","amqp_lastInBatch":false,"contentType":"application/json","__TypeId__":"com.qs.parentchild2common.entity.bo.UserBO","timestamp":1624602705741}
......
- 小结
(1)@RabbitListener可以提前声明queue、exchange、routingKey的绑定关系,如果RabbitMQ中不存在,在启动消费者时会自动创建
(2)@RabbitListener配合@RabbitHandler使用时,可以通过指定不同类型的消息类型,从而编写不同的方法去获取消息体
(3)消息如果没有抵达队列,会以异步发送优先回调returns确认
(4)上述已经实现了消息–>RabbitMQ的可靠性投递,接下来会说消费者的ack手动确认
4.2 生产者:发送Message对象封装后的消息
4.2.1 发送org.springframework.messaging.Message消息
注意:封装该类型消息发送时,RabbitTemplate的消息转换器使用Jackson2JsonMessageConverter会报错,当我注释掉该消息转换器,使用默认的消息转换器时,正常运行
- 通用的消息封装
import com.qs.parentchild2common.entity.constant.RabbitConfigConstant;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Service;
/**
* @author QuS
* @date 2021/6/25 13:46
*/
@Service
public class RabbitMQProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @param message 消息体
*/
public void sendMessageToMQ(Message message, String msgId) {
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(RabbitConfigConstant.TOPIC_EXCHANGE, "order.123", message, correlationData);
}
}
- 生产者代码
@RequestMapping("/sendMessage1")
public String sendMessage(Integer num) {
RabbitMQBO rabbitMQBO;
UserBO userBO;
for (int i = 0; i < num; i++) {
String id = "ID" + i;
Message message;
if (i % 2 == 0) {
rabbitMQBO = new RabbitMQBO();
rabbitMQBO.setId(i);
rabbitMQBO.setUuid(UUID.randomUUID().toString());
rabbitMQBO.setMsg("这是生产的消息" + i);
Map<String, Object> headers = new HashMap<>();
headers.put("testId", "testId" + i);
message = new GenericMessage(rabbitMQBO, headers);
rabbitMQProducer.sendMessageToMQ(message, id);
}
}
return "ok";
}
- 消费者代码
import com.alibaba.fastjson.JSONObject;
import com.qs.parentchild2common.entity.constant.RabbitConfigConstant;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author QuS
* @date 2021/6/28 14:49
*/
@Component
public class RabbitMQConsumer2 {
@RabbitListener(queues = {RabbitConfigConstant.QUS_QUEUE_5})
private void consumeMessage(Message message, Channel channel) throws IOException {
Object payload = message.getPayload();
System.out.println("payload:>> " + JSONObject.toJSONString(payload));
MessageHeaders headers = message.getHeaders();
long deliveryTag = (long) headers.get("amqp_deliveryTag");
System.out.println("deliveryTag:>> " + deliveryTag + " testId:>> " + headers.get("testId"));
System.out.println("headers:>> " + JSONObject.toJSONString(headers));
channel.basicAck(deliveryTag, false);
}
}
- 小结
(1)org.springframework.messaging.Message作为消息体发消息时,消息转换器使用Jackson2JsonMessageConverter会报错,使用默认的消息转换器不会报错
(2)使用该消息体,可以自定义拓展封装Header
4.2.2 发送org.springframework.amqp.core.Message消息
该消息体的封装需要转换为字节数组,消费端也需要由字节数组转换为对应的类型,个人不太喜欢,感兴趣的伙伴可以研究使用
4.3 消费者手动ack确认
默认情况下,消费端是自动ack确认,如果消费端处理出现异常,外加自动ack确认,会导致处理异常的消息丢失,所以我们可改成手动ack,在程序异常时,对消息进行处理,防止消息丢失。
- 配置文件修改,开启手动ack
spring:
application:
name: rabbitmq-consumer1
rabbitmq:
host: host
port: port
username: username
password: password
listener:
simple:
acknowledge-mode: manual
- 代码手动ack
channel.basicAck(deliveryTag, false);
@RabbitListener(queues = {RabbitConfigConstant.QUS_QUEUE_5})
private void consumeMessage(Message message, Channel channel) {
try {
Object payload = message.getPayload();
System.out.println("payload:>> " + JSONObject.toJSONString(payload));
MessageHeaders headers = message.getHeaders();
long deliveryTag = (long) headers.get("amqp_deliveryTag");
System.out.println("deliveryTag:>> " + deliveryTag + " testId:>> " + headers.get("testId"));
System.out.println("headers:>> " + JSONObject.toJSONString(headers));
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
//异常处理
log.error("出现异常:{}", e.toString());
channel.basicNack(deliveryTag, false, false);
}
}
4.4 死信队列处理拒绝签收的消息
消费者消费过程出现异常,nack后进入死信队列
- 进入死信队列的过程
生产者 --> 发送消息 --> 业务交换机 --> 队列 --> 成为死信消息 --> 死信交换机 --> 死信队列 --> 监听死信队列的消费者
1. 正常业务消息被投递到正常业务的Exchange,该Exchange根据路由键将消息路由到绑定的正常队列。
2. 正常业务队列中的消息变成了死信消息之后,会被自动投递到该队列绑定的死信交换机上(并带上配置的路由键,如果没有指定死信消息的路由键,则默认继承该消息在正常业务时设定的路由键)。
3. 死信交换机收到消息后,将消息根据路由规则路由到指定的死信队列。
4. 消息到达死信队列后,可监听该死信队列,处理死信消息。
- 消息成为死信消息的场景
(1)消息被(reject 或 nack ) 并且 requeue = false,即消息被消费者拒绝签收,并且重新入队为false;
(2)消息过期,过了ttl存活时间;
(3)队列设置了x-max-length最大消息数量且当前队列中的消息已经达到了这个数量,再次投递,消息将被挤掉,被挤掉的是最靠近被消费那一端的消息。 - 主要给业务队列添加"x-dead-letter-exchange" 和 "x-dead-letter-routing-key"参数去指定其死信交换机和死信路由键
- 业务队列绑定死信队列代码示例
@Slf4j
@Configuration
public class RabbitMQConfig {
@Autowired
private RabbitAdmin rabbitAdmin;
/**
* 队列、交换机的创建
*/
@Bean
public void createExchangeQueue() {
//死信交换机、死信队列
createDeadLetterExchangeQueue();
}
void createDeadLetterExchangeQueue() {
DirectExchange deadLetterExchange = deadLetterExchange();
Queue deadLetterQueue1 = deadLetterQueue1();
Binding binding = BindingBuilder.bind(deadLetterQueue1).to(deadLetterExchange).with(RabbitConfigConstant.DEAD_LETTER_QUEUE1_ROUTING_KEY);
rabbitAdmin.declareExchange(deadLetterExchange);
rabbitAdmin.declareQueue(deadLetterQueue1);
rabbitAdmin.declareBinding(binding);
}
//业务队列添加死信交换机参数
private Queue queue1() {
Queue queue1 = QueueBuilder.durable(RabbitConfigConstant.QUS_QUEUE_1)
.withArgument("x-dead-letter-exchange", RabbitConfigConstant.DEAD_LETTER_DIRECT_EXCHANGE)
.withArgument("x-dead-letter-routing-key", RabbitConfigConstant.DEAD_LETTER_QUEUE1_ROUTING_KEY)
.build();
return queue1;
}
//死信队列
private Queue deadLetterQueue1() {
return new Queue(RabbitConfigConstant.DEAD_LETTER_QUEUE_1, true);
}
//死信交换机
private DirectExchange deadLetterExchange() {
return new DirectExchange(RabbitConfigConstant.DEAD_LETTER_DIRECT_EXCHANGE, true, false);
}
}
- 下图说明qus-queue-1队列有对应的死信交换机和死信路由键