在实际开发中我们大部分情况下都是将RabbitMQ和Springbooot集成使用,下面的例子皆以此环境为例
消息的生产和消费路径很长且复杂,怎么保证消息最终被正确的消费?
上图列出了ABCDE 5个风险点,当你的消息需要确保正确送达必须要控制好这几个点
- A:确保消息被正确的发送到RabbitMQ的Exchange中
- B:防止Exchange中还未来得及放置到queue中的消息意外丢失(服务异常停止)
- C:确保消息能正常投递到queue中(未匹配上bindingKey)
- D: 防止Queue中还未被消费的消息丢失(服务异常停止)
- E:确保消息被Consumer服务正常消费(代码错误、业务逻辑异常、服务异常终止)
为解决这些问题,rabbitMQ提供了独立的配置和功能来处理各个环节
(A) 发送消息异步确认
除了异步确认还有事务、同步确认和批量同步确认3种方式,但都会极大降低效率 一般不采用
配置文件增加publisher-confirm-type 配置项
spring:
rabbitmq:
port: 5672
host: zhangfr.iok.la
username: admin
password: admin1
virtual-host: /
# 打开消息确认机制,通知消息是否到达Exchange
publisher-confirm-type: correlated
系统初始化时设置一个ConfirmCallback
@Configuration
public class RabbitInitializingBean implements InitializingBean {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void afterPropertiesSet() throws Exception {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("Confirm====消息唯一标识: {}", correlationData);
log.info("Confirm====确认状态: {}", ack);
log.info("Confirm====造成原因: {}", cause);
}
});
}
}
示例:发送两条消息,一条发送给正确的Exchange,另一条发送给一个不存在的Exchange
public void p4_send() {
User user = new User();
user.setId(1L);
CorrelationData correlationData = new CorrelationData();
correlationData.setId(user.getId().toString());
// 这里的 空字符串是一个默认的路由名
rabbitTemplate.convertAndSend("",RabbitMQConfig.QUEUE_TEST,user,correlationData);
log.info("====发送消息");
}
public void p5_send() {
User user = new User();
user.setId(2L);
CorrelationData correlationData = new CorrelationData();
correlationData.setId(user.getId().toString());
// 123是一个不存在的路由
rabbitTemplate.convertAndSend("123",RabbitMQConfig.QUEUE_TEST,user,correlationData);
}
ConfirmCallback打印日志如下图
我们可以根据发送情况进行补偿处理:比如重试、通知、撤回等等
(C) 消息失败退回
开启消息失败回退后,当消息未成功送达Queue时仍然会通知我们
先在配置文件开启失败回退配置
spring:
rabbitmq:
port: 5672
host: zhangfr.iok.la
username: admin
password: admin1
virtual-host: /
# 开启发送失败退回,确保消息到达 Queue
publisher-returns: true
设置一个ReturnCallback,路由消息到Queue时将会回调
@Configuration
public class RabbitInitializingBean implements InitializingBean {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void afterPropertiesSet() throws Exception {
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("Return====消息主体: {}", message);
log.info("Return====回复编码: {}", replyCode);
log.info("Return====回复内容: {}", replyText);
log.info("Return====交换器: {}", exchange);
log.info("Return====路由键: {}", routingKey);
}
});
}
}
示例:发送给默认路由一个 不能匹配上 Queue的消息
public void p5_send() {
User user = new User();
user.setId(2L);
CorrelationData correlationData = new CorrelationData();
correlationData.setId(user.getId().toString());
// 空字符串是一个默认的路由名
// abc不能匹配上Queue的routingKey
rabbitTemplate.convertAndSend("","abc",user,correlationData);
log.info("====发送消息 -> {}",user.toString() );
}
ReturnCallback收到异常通知如下
(E) 消费端显示确认消息
默认配置下消费者收到消息就确认消息被消费(消息到达方法入口就自动确认消费了消息),实际业务中该方法可能报错、程序中断等等。消息并未被正确消费,造成事实上的消息丢失。
对准确性要求高的业务我们就需要开启显示确认:当消费业务方法执行完成后再显示的确认消息被消费
配置如下:
spring:
rabbitmq:
port: 5672
host: zhangfr.iok.la
username: admin
password: admin1
virtual-host: /
# 开启手动确认:在收到消息处理完业务后再调用 channel.basicAck() 确认消。否则将再收到消息后自动确认,业务可能会处理失败造成数据丢失
listener:
simple:
acknowledge-mode: manual
使用示例:
@RabbitListener(queues = RabbitMQConfig.QUEUE_TEST)
public void c3(Message msg, Channel channel) throws IOException {
byte[] byteMsg = msg.getBody();
//todo 执行业务....
//业务执行完成后再显示确认
channel.basicAck(msg.getMessageProperties().getDeliveryTag(),false);
log.info("c3 确认成功 -> {}",byteMsg);
}
(E) 消费端显示确认消息-进阶
上文示例只列举了basicAck这一种方式:他的意思是肯定消息被正常消费,RabbitMQ服务端收到此消息就会删除掉这一条消息
实际上确认还包含了否定确认的场景。
- 拒绝此消息、第一个参数为消息ID ,第二个参数是 是否重试(为false时将成为死信)
@RabbitListener(queues = RabbitMQConfig.QUEUE_TEST)
public void c2(Message msg, Channel channel) throws IOException {
byte[] byteMsg = msg.getBody();
//todo 执行业务....
channel.basicReject(msg.getMessageProperties().getDeliveryTag(),true);
log.info("c2 确认失败需要重试 -> {}",byteMsg);
}
执行结果
日志应该很好的展现了问题,拒绝此次推送后,又被推送过来了。如此循环(实际上如果有多个消费者的话,消息有机会被推送到其他消费者正常消费,从而终止循环)
- 批量拒绝消息,方法同上:多了一个multiple 参数(第二个参数)
@RabbitListener(queues = RabbitMQConfig.QUEUE_TEST)
public void c3(Message msg, Channel channel) throws IOException {
byte[] byteMsg = msg.getBody();
//todo 执行业务....
channel.basicNack(msg.getMessageProperties().getDeliveryTag(),true,true);
log.info("c2 确认失败丢弃 -> {}",byteMsg);
}
- 拒绝并告知RabbitMQ尽量投递给其他消费者(如果只有一个消费者那就等同于 basicReject了 233....)
@RabbitListener(queues = RabbitMQConfig.QUEUE_TEST)
public void c1(Message msg, Channel channel) throws IOException {
byte[] byteMsg = msg.getBody();
//todo 执行业务....
//Recover: 拒绝消息
channel.basicRecover(true);
log.info("c1 确认失败重新放回 -> {}",byteMsg);
}
(B & D)这两个放在一起讲
交换机和队列创建时设置为需持久化。 这样MQ突然宕机或被关闭,下次启动时会自动恢复数据。
当然,要是服务器挂了、磁盘坏了就不顶用了,这种情况下需要镜像队列、异地多活来解决了
/**
* 创建一个Queue
* @return
*/
@Bean
public Queue QUEUE_TEST(){
//第二个参数:是否持久化
return new Queue(QUEUE_TEST,true);
}
/**
* 创建一个Exchange
* @return
*/
@Bean
public DirectExchange DIRECT_EXCHANGE_TEST(){
//第二个参数:是否持久化
return new DirectExchange(DIRECT_EXCHANGE_TEST,true,false);
}