在实际开发中我们大部分情况下都是将RabbitMQ和Springbooot集成使用,下面的例子皆以此环境为例

消息的生产和消费路径很长且复杂,怎么保证消息最终被正确的消费?

java rabbitmq消费完确定 rabbitmq确保消费成功_User

上图列出了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打印日志如下图

java rabbitmq消费完确定 rabbitmq确保消费成功_User_02

我们可以根据发送情况进行补偿处理:比如重试、通知、撤回等等

(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收到异常通知如下

java rabbitmq消费完确定 rabbitmq确保消费成功_ide_03

(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);
}

执行结果

java rabbitmq消费完确定 rabbitmq确保消费成功_ide_04

日志应该很好的展现了问题,拒绝此次推送后,又被推送过来了。如此循环(实际上如果有多个消费者的话,消息有机会被推送到其他消费者正常消费,从而终止循环)

  • 批量拒绝消息,方法同上:多了一个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);
}