一、开发生产端
1、配置队列、交换机并绑定
spring:
rabbitmq:
virtual-host: /myhost
port: 5672
host: localhost
username: admin
password: admin
2、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
3、配置队列、交换机并绑定
@Configuration
public class MqConfig {
public static final String MSG_QUEUE = "msg-queue";
public static final String MSG_EXCHANGE = "msg-exchange";
public static final String MSG_ROUTE_KEY = "msg.key";
/**
* 声明队列
*/
@Bean
public Queue msgQueue() {
return new Queue(MSG_QUEUE,true,false,false);
}
/**
* 声明交换机
*/
@Bean
public TopicExchange msgExchage() {
//参数2是否持久化,参数3是否自动删除
return new TopicExchange(MSG_EXCHANGE,true,false);
}
/**
* 将队列和交换机进行绑定
*/
@Bean
public Binding bindMsgQueue() {
return BindingBuilder.bind(msgQueue()).to(msgExchage()).with(MSG_ROUTE_KEY);
}
}
4、发送消息
@Component
public class MsgProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(String message) {
rabbitTemplate.convertAndSend(MqConfig.MSG_EXCHANGE,MqConfig.MSG_ROUTE_KEY,message);
}
}
二、开发消费端
同样导入依赖、配置yml
处理消息
@Component
public class MsgConsumer {
public static final String MSG_QUEUE = "msg-queue";
public static final String MSG_EXCHANGE = "msg-exchange";
//消息处理的方法
@RabbitHandler
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name=MSG_QUEUE,declare = "true",durable = "true",exclusive = "false",autoDelete = "false"),
exchange = @Exchange(name = MSG_EXCHANGE,type = ExchangeTypes.TOPIC),
key = "msg.*"
))
//@Payload表示消费者处理的消息
//@Headers注解表示接收的消息头将会被绑定到`headers`参数上
public void receiveMsg(@Payload String msg, Channel channel, @Headers Map headers) {
System.out.println("消费者处理消息:" + msg);
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
channel.basicAck(tag,false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@RabbitHandler注解:表示该方法是一个消息处理方法
bindings属性用于指定队列和交换机的绑定关系。
@RabbitListener注解:表示该类是一个消息监听器,用于监听指定的队列
value属性用于指定队列的属性,包括队列的名称、是否需要声明、是否持久化、是否排他、是否自动删除等
exchange属性用于指定交换机的名称和类型
key属性用于指定消息的路由键
三、 生产者端消息确认和回退
1、yml配置
spring:
rabbitmq:
virtual-host: /myhost
port: 5672
host: localhost
username: admin
password: admin
publisher-confirm-type: correlated
publisher-returns: true
ConfirmType | |
---|---|
NONE | 禁用发布确认模式,是默认值。 |
CORRELATED | 将消息成功发布到交换器后触发回调方法。 |
SIMPLE | 与CORRELATED相似,也会在将消息成功发布到交换器后触发回调方法。 |
2、定义消息确认回调的方法
(1)配置类添加配置
RabbitTemplate.ConfirmCallback callback = new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean isAck, String cause) {
if (!isAck) {
System.out.println("拒收的原因:" + cause);
} else {
if (correlationData != null) {
System.out.println("broker接收消息自定义ID:" + correlationData.getId());
}
}
}
};
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
RabbitTemplate template = new RabbitTemplate();
template.setConnectionFactory(factory);
template.setConfirmCallback(callback);
return template;
}
(2)修改发送消息的方法,携带附加数据
@Component
public class MsgProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(String message) {
//自定义的附加数据
CorrelationData data = new CorrelationData();
data.setId("10001");
rabbitTemplate.convertAndSend(MqConfig.MSG_EXCHANGE,MqConfig.MSG_ROUTE_KEY,message,data);
}
}
3、消息回退
在配置文件中添加 publisher-returns: true 配置消息回退
定义处理消息回退的方法
RabbitTemplate.ReturnsCallback returnsCallback = new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage msg) {
System.out.println("--------消息路由失败------------");
System.out.println("消息主体:" + msg.getMessage());
System.out.println("返回编码:" + msg.getReplyCode());
System.out.println("描述信息:" + msg.getReplyText());
System.out.println("交换机:" + msg.getExchange());
System.out.println("路由key:" + msg.getExchange());
System.out.println("------------------------------");
}
};
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
RabbitTemplate template = new RabbitTemplate();
template.setConnectionFactory(factory);
template.setConfirmCallback(callback);
template.setReturnsCallback(returnsCallback);
// true 表示消息通过交换机无法路由到队列时候,会把消息返回给生产者
// false 消息无法路由到队列就直接丢弃
template.setMandatory(true);
return template;
}
四.消息消费可靠性保障
常见情况 | 问题环节 | 解决方案 |
---|---|---|
生产者消息没到交换机 | 生产者丢失消息 | 为上面的异步监听confirm-type、publisher-returns |
交换机没有把消息路由到队列 | 生产者丢失消息 | |
RabbitMQ 宕机导致队列、队列中的消息丢失 | RabbitMQ 丢失消息 | 设置持久化将消息写出磁盘,否则RabbitMQ重启后所有队列和消息都会丢失 |
消费者消费出现异常,业务没执行 | 消费者丢失消息 |
消费者丢失消息具体处理办法:
消费者丢数据一般是因为采用了自动确认消息模式。MQ收到确认消息后会删除消息,如果这时消费者异常了,那消息就没了。使用ack机制,默认情况下自动应答,可以使用手动ack
1、修改配置
listener:
simple:
acknowledge-mode: manual #开启消费者手动确认模式 (channel.bacisAck)
2 然后在消费端代码中手动应答签收消息
如果消息消费失败,不执行消息确认代码,用channel的basicNack方法拒收
public void receiveMsg(@Payload String msg, Channel channel, @Headers Map headers) {
System.out.println("消费者处理消息:" + msg);
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
System.out.println(2/0);
//其中`tag`是消息的唯一标识,`false`表示只确认当前消息,不确认之前的所有未确认消息。
channel.basicAck(tag,false);
} catch (Exception e) {
System.out.println("签收失败");
try {
//其中`tag`是消息的唯一标识,`false`表示只拒绝当前消息,`true`表示该消息将重新进入队列等待被消费。
channel.basicNack(tag,false,true);
} catch (IOException ex) {
System.out.println("拒收失败");
}
}
}
通常的代码报错并不能因为重试而解决,可能会造成死循环
解决办法:
- 当消费失败后将此消息存到 Redis,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存;
- basicNack方法的参数3直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理
- 不启用手动 ack ,使用 SpringBoot 提供的消息重试
3 使用 SpringBoot 提供的消息重试
listener:
simple:
retry:
enabled: true
max-attempts: 3 #重试次数
注意:要抛异常,因为SpringBoot 触发重试是根据方法中发生未捕捉的异常来决定的
public void receiveMsg(@Payload String msg, Channel channel, @Headers Map headers) {
System.out.println("消费者处理消息:" + msg);
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
System.out.println(2/0);
//channel.basicAck(tag,false);
} catch (Exception e) {
System.out.println("签收失败");
//记录日志、发送邮件、保存消息到数据库,落库之前判断如果消息已经落库就不保存
throw new RuntimeException(e);
}
}
4、消息重复消费(消息幂等性)
使用手动恢复MQ解决了消息在消费者端丢失的问题,但是如果消费者处理消息成功后,由于网络波动导致手动回复MQ失败,该条消息还保存在消息队列中,由于MQ消息的重发机制,该消息会被重复消费,造成不好的后果
(1)确保消费端只执行一次:使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识
(2)允许消费端执行多次,保证数据不受影响
- 数据库唯一约束:如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号,如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入两条;
-
数据库乐观锁
五、消息转换器
1、需求描述:
前面我们发送的消息都是字符串,如果想发送对象,就要用到消息转换器。
消息转换器(Message Converter)是用于将消息在生产者和消费者之间进行序列化和反序列化的组件。在消息传递过程中,生产者将消息对象转换为字节流发送到消息队列,而消费者则将接收到的字节流转换回消息对象进行处理。
创建生产者发送消息方法
/**创建生产者发送消息方法 */
@Component
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendOrder(OrderDTO orderDTO) {
rabbitTemplate.convertAndSend(MqConfig.ORDER_EXCHANGE,MqConfig.ORDER_KEY,orderDTO);
}
}
//test
//发送消息
@Test
void testSendOrder() {
OrderDTO orderDTO = new OrderDTO();
orderDTO.setOrderSn(UUID.randomUUID().toString());
orderDTO.setUsername("user");
orderDTO.setAmount(new BigDecimal("200"));
orderDTO.setCreateDate(new Date());
orderProducer.sendOrder(orderDTO);
}
控制台查看消息:
上面采用的是JDK序列化方式,可以看出虽然获得了对象,但是得到的数据体积大,可读性差,为了解决这个问题,我们可以通过SpringAMQP的MessageConverter来处理
2、Spring的消息转换器
Spring AMQP提供了多种消息转换器(Message Converter)这些消息转换器使得消息的发送和接收可以使用不同的消息格式,如JSON、XML等,从而更灵活地处理消息数据
(1)生产端配置消息转换器
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
RabbitTemplate template = new RabbitTemplate();
template.setConnectionFactory(factory);
template.setConfirmCallback(callback);
template.setReturnsCallback(returnsCallback);
// true 表示消息通过交换机无法路由到队列时候,会把消息返回给生产者
// false 消息无法路由到队列就直接丢弃
template.setMandatory(true);
template.setMessageConverter(messageConverter());
return template;
}
(2)消费端配置消息转换器
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置消息转换器
@Configuration
public class MqConfig implements RabbitListenerConfigurer {
@Resource
private ObjectMapper objectMapper;
//将消息转换为JSON格式
public MappingJackson2MessageConverter messageConverter(){
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(objectMapper);
return converter;
}
@Bean
public MessageHandlerMethodFactory messageHandlerMethodFactory(){
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(messageConverter());
return factory;
}
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar rabbitListenerEndpointRegistrar) {
rabbitListenerEndpointRegistrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
}
}
创建消费者处理消息方法
@RabbitHandler
@RabbitListener(queues = "msg-queue")
public void receiveOrder(@Payload OrderDTO orderDTO, Channel channel, @Headers Map map){
System.out.println("Order消息处理:"+orderDTO);
Long tag = (Long)map.get(AmqpHeaders.DELIVERY_TAG);
try {
channel.basicAck(tag,false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
六、延迟队列
延时队列就是用来存放需要在指定时间内被处理的消息的队列,是死信队列的一种
应用场景: |
---|
1、订单在十分钟之内未支付则自动取消 |
2、预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议 |
3、用户发起退款,如果三天内没有得到处理则通知 |
4、用户注册成功后,如果三天内没有登陆则进行短信提醒 |
1 死信队列
死信队列(Dead Letter Queue,简称DLQ)是一种用于处理消息处理失败或被拒绝的消息的特殊队列。当消息在队列中满足一定条件时,例如消息被消费者拒绝、消息过期、消息处理超时等,这些消息将被发送到死信队列而不是直接被丢弃或忽略
2 TTL消息
TTL(Time To Live)指定消息在队列中存活的时间,超过指定的时间后如果消息还未被消费者消费,则该消息会被自动丢弃或转移到死信队列
(1)生产端配置类配置TTL消息
@Bean
public Queue ttlQueue() {
Map map = new HashMap();
map.put("x-message-ttl",5000);
return new Queue("ttl-queue",false,false,false,map);
}
@Bean
public TopicExchange ttlExchange() {
return new TopicExchange("ttl-exchange");
}
@Bean
public Binding bindTllQueue() {
return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl.*");
}
(2)创建生产者发送消息方法
public void sendMessage() {
rabbitTemplate.convertAndSend("ttl-exchange","ttl.msg","hello world");
}
3 死信交换机
(1)生产端配置类配置死信交换机
配置死信队列、交换机并把二者绑定,修改TTL队列失效时放到死信交换机中进而存到死信队列中,而不是直接销毁
@Bean
public Queue ttlQueue(){
Map map = new HashMap<>();
map.put("x-message-ttl",5000);
map.put("x-dead-letter-exchange","dead-exchange"); //死信交换机
map.put("x-dead-letter-routing-key","dead.msg"); //发送消息时携带路由key
return new Queue("ttl-queue",false,false,false,map);
}
@Bean
public TopicExchange deadExchange() {
return new TopicExchange("dead-exchange");
}
@Bean
public Queue deadQueue() {
return new Queue("dead-queue");
}
@Bean
public Binding bindDeadQueue() {
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead.#");
}
(2)接收端处理消息
@RabbitHandler
@RabbitListener(queues = {"dead-queue"})
public void receiveDeadMsg(@Payload String msg, Channel channel, @Headers Map headers) {
Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
System.out.println("处理了已经超时的消息:" + msg);
try{
channel.basicAck(tag,false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
七、延迟插件
RabbitMQ实现延迟消息的方式有两种,一种是使用死信队列,另一种是使用延迟插件。
通过安装插件,自定义交换机,让交换机拥有延迟发送消息的能力,从而实现延迟消息,相较于死信队列延迟插件只需创建一个交换机和一个队列,使用起来简单
1、延迟插件下载安装
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
将插件文件复制到RabbitMQ安装目录的plugins目录下,然后进入RabbitMQ安装目录的sbin目录下,使用如下命令启用延迟插件;
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
2 SpringBoot中实现延迟插件
(1) 开发生产者端
配置交换机、队列和绑定关系
/**
* 订单延迟插件消息队列所绑定的交换机
*/
@Bean
DirectExchange orderCancelExchange() {
return ExchangeBuilder.directExchange("order-delay-exchange")
.delayed().durable(true)
.build();
}
/**
* 订单延迟插件队列
*/
@Bean
public Queue orderCancelQueue() {
return new Queue("order-delay-queue");
}
/**
* 将订单延迟插件队列绑定到交换机
*/
@Bean
public Binding bindOrderCancelQueue() {
return BindingBuilder.bind(orderCancelQueue())
.to(orderCancelExchange()).with("delay.order.key");
}
创建发送消息方法
通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间
@Component
public class CancelOrderSender {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendMessage(Long orderId,Long delayTime) {
rabbitTemplate.convertAndSend("order-delay-exchange", "delay.order.key", orderId, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//给消息设置延迟毫秒值
message.getMessageProperties().setHeader("x-delay",delayTime);
return message;
}
});
}
}
Service层
@Service
public class OrderServiceImpl {
@Resource
private CancelOrderSender cancelOrderSender;
public void createOrder() {
System.out.println("下单后生成订单ID");
Long orderId = 1001L;
sendDelayMessageCancelOrder(orderId);
}
public void sendDelayMessageCancelOrder(Long orderId) {
//获取订单超时时间,假设为5秒
long delayTimes = 5 * 1000;
cancelOrderSender.sendMessage(orderId,delayTimes);
}
}
Controller层
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderServiceImpl orderService;
@PostMapping
public String create() {
orderService.createOrder();
return "success";
}
}
(2)、开发消费者端
Service层
@Service
public class OrderServiceImpl {
public void cancelOrder(Long orderId) {
System.out.println("查询订单编号为:" + orderId + "订单状态,如果是待支付状态,则更新为已失效");
}
}
创建处理消息的方法
@Component
public class CancelOrderReceiver {
@Autowired
private OrderServiceImpl orderService;
@RabbitHandler
@RabbitListener(queues = {"order-delay-queue"})
public void handle(@Payload Long orderId, Channel channel, @Headers Map headers) {
orderService.cancelOrder(orderId);
Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
try {
channel.basicAck(tag,false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}