消息可靠性投递
故障概述
- 故障情况1:消息没有发送到消息队列
- 解决方式1:在生产者端分别针对交换机和队列来进行确认,当没能成功发送到消息队列服务器上,则尝试重新发送。
- 解决方式2:为目标交换机指定备份交换机,当目标交换机投递失败时,把消息投递到备份交换机
- 故障情况2:消息队列服务器宕机导致内存中消息丢失
- 解决方式:将消息持久化(已经默认实现)
- 故障情况3:消费端宕机或抛出异常导致消息没被成功消费
- 解决方式:消费端消费成功后,给服务器返回ACK消息,之后消息队列服务器才会删除该消息。当消费端消费信息失败,就给服务端返回NACK信息,同时把消费恢复为待消费的状态,这样就可以再次取回消息(需要消费端接口支持幂等性)
生产者端消息确认机制
- 在配置文件中添加相应的配置项
- 在配置类中声明相应回调函数
- 为RabbitTemplate设置增强
配置文件
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: CORRELATED # 交换机的确认
publisher-returns: true # 队列的确认
配置类
@Configuration
@Slf4j
public class RabbitConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
// 从容器中取出rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// 初始化rabbitTemplate后将下面两个回调添加进去
// 该注解可以指定在对象创建后立即执行,在spring中使用时,可以保证对象在初始化之后执行该方法
@PostConstruct
public void initRabbitTemplate() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
// 消息发送到交换机无论成功与否都会执行该方法
// 三个参数分别描述:相关数据,是否接收成功,接收失败的原因
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
log.info("相关数据" + correlationData);
log.info("交换机是否接收成功" + b);
if(!b){log.info("交换机接受失败的原因" + s);}
}
// 只有当发送到队列失败才会回调该方法
// 该参数用以描述相关信息
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息主体" + new String(returnedMessage.getMessage().getBody()));
log.info("应答码" + returnedMessage.getReplyCode());
log.info("应答码描述" + returnedMessage.getReplyText());
log.info("消息使用的交换机"+returnedMessage.getExchange());
log.info("消息使用的路由键"+returnedMessage.getRoutingKey());
}
}
测试
@SpringBootTest
class ConfirmProducerApplicationTests {
// 需要提前创建该队列
public static final String EXCHANGE_DIRECT = "exchange.direct.order";
public static final String Routing_KEY = "order";
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void testSendMessage() {
rabbitTemplate.convertAndSend(EXCHANGE_DIRECT, Routing_KEY, "Hello World");
}
}
备份交换机
若目标交换机出现故障,导致生产者消息不能被成功消费消息。此时,我们通常采用备份交换机来进行故障转移,备份交换机可以绑定另外一个队列,让另一个消费者对其进行监听,用以记录错误日志方便后续人员排查
备份交换机的创建可以直接在rabbitmq面板中进行。该交换机类型必须是fanout(从目标交换机将消息转到备份交换机时不带路由键)
目标交换机的创建需要在备份交换机之后进行,因为指定备份交换只能在创建时通过点击Add Alternate exchange,并且填写备份交换机名进行。
消费者端确认机制
- 在配置文件中将消息确认模式改为手动确认
- 在消费者监听器添加相应调用
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual # 将消息确认模式改为手动
消费者
@Component
@Slf4j
public class MyMessageListener{
public static final String QUEUE_NAME = "queue.order";
@RabbitListener(queues = {QUEUE_NAME})
public void processMessage(String dataString, Message message, Channel channel) throws IOException {
// 获取当前消息的deliveryTag
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try{
log.info("消费端 消息内容" + dataString);
// 返回ACK信息
// 第一个参数为deliveryTag,可以看作是每条消息的id(就算是广播的消息也是全局唯一)
// 第二个参数若为true时,则对此消息还有之前的全部批量操作,若为false则只会对当前消息进行操作
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
// 获取当前消息是否是重复投递的
Boolean redelivered = message.getMessageProperties().getRedelivered();
if(redelivered){channel.basicNack(deliveryTag,false,false);}
// 返回NACK消息
// 前两个参数同basicAck方法,第三个参数用于标定该消息是否要重新放回队列
channel.basicNack(deliveryTag,false,true);
throw new RuntimeException(e);
}
}
}
特殊队列
死信队列
- 死信
- 当一个消息无法被消费,则成为了死信。
- 死信产生的原因
- 拒绝
- 消费者拒绝接收消息(basicNack/basicReject),并且不把消息重新放入原目标队列(requeue=false)
- 溢出
- 队列中消息数量达到限制,根据先进先出原则,队列中最早的消息会变成死信
- 超时
- 消息达到超时时间未被消费
- 拒绝
- 死信的处理方式
- 丢弃
- 对于不重要的消息直接丢弃,不做处理
- 入库
- 把死信写入数据库,之后进行处理
- 监听
- 消息变成死信后进入死信队列,并专门设置消费端监听死信队列,做后续处理
- 丢弃
要使用死信队列,需要先创建死信队列,再创建正常队列,并且还需要在创建正常队列时指定死信交换机,队列长度,过期时间等信息
备份交换机和死信队列看起来相似,但是前者的设计目的是防止消息因路由失败而丢失,作用在路由阶段。后者设计目的是处理消费失败或延迟的消息,作用在消费阶段。
延迟队列
在rabbitMQ中要使用延迟队列,可以通过死信队列(给正常队列设置超时时间,时间一到就进入死信队列被消费),同时也可以使用插件https://github.com/rabbitmq/rabbitmq-delayed-message-exchange