RabbitMQ保证消息不丢失:消息发送确认 与 消息接收确认(ACK)

默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除

如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则会立即发送,当 Message 被消费者正确接收时,就会被从 Queue 中移除

为什么会产生消息确认这个概念呢?其实rabbitmq的模式是我们熟悉的典型的观察者模式的具体实现,或者说是监听器模式可以,生产者往队列投递了一条消息,消费者从队列取出消息消费,这是很好理解的;

但是在rabbitmq中引入了exchange,即交换机这个概念,我们可以理解为一个消息的中转站或者是消息的分发集散中心,在这里,rabbitmq相比kafka或者activemq提供了更为高级的功能,就是支持消息的精确路由,消息的模糊匹配等功能,这样一来,对于整个消息从生产者到消费者最终消费到这条消息,中间的链路比起单纯的链路,生产者 —>队列—>消费者,中间多了一些环节,这也就造成了消息能否最终发送并被消费成功的不确定性,正是这种不确定性使得我们在使用的时候会关注消息到每一步的时候的状态,也就产生了消息的确认机制;


下面,我们先看一张关于rabbitmq从生产者发送消息到exchange然后到指定队列的整个流程示意图,如下所示

å¨è¿éæå¥å¾çæè¿°

通过这张示意图,相信大家可以大致了解了上述解释的意思所在,也可以看出来,消息需要确认的地方无非有3处,

1、消息是否能找到对应的exchange,即生产者的消息是否能够准确投递到指定的exchange中,如果找不到,则会被退回;

2、消息投递到exhange成功,但是没有找到合适的队列,即消息无法被路由到指定的queue中去,导致消息无法被投递和消费,也会被退回;

3、最后,消息被某个消费者消费,但是没有确认

退回这个词可以认为是程序中处理未被确认的消息的一致机制,或者说一种处理方式,在rabbitmq中可以是退回这条未被确认的消息,或者是丢弃掉可以根据业务场景具体使用;

消息发送确认

这里注意:ConfirmCallback 和 ReturnCallback的区别?

ConfirmCallback 是发送到交换机exchange失败调用的回调函数,

ReturnCallback 是消息发送到交换机exchange了,但是没有成功投递到队列失败而调用的函数,消息被退回。

  • 发送的消息怎么样才算失败或成功?如何确认?

当消息无法路由到队列时,确认消息路由失败。消息成功路由时,当需要发送的队列都发送成功后,进行确认消息,对于持久化队列意味着写入磁盘,对于镜像队列意味着所有镜像接收成功

  • ConfirmCallback - 消息发送确认

通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中

@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallback
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("消息唯一标识:"+correlationData);
        System.out.println("确认结果:"+ack);
        System.out.println("失败原因:"+cause);
    }
}

还需要在配置文件添加配置

spring:
  rabbitmq:
    publisher-confirms: true 
  • ReturnCallback - 消息发送失败返回

通过实现 ReturnCallback 接口,启动消息失败返回,比如路由不到队列时触发回调

@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("消息主体 message : "+message);
        System.out.println("消息主体 message : "+replyCode);
        System.out.println("描述:"+replyText);
        System.out.println("消息使用的交换器 exchange : "+exchange);
        System.out.println("消息使用的路由键 routing : "+routingKey);
    }
}

还需要在配置文件添加配置

spring:
  rabbitmq:
    publisher-returns: true 

 

消息接收确认

  • 消息消费者如何通知 Rabbit 消息消费成功?

1.消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
2.自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
3.如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失
4.如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者
5.如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟
消息确认模式有:
  AcknowledgeMode.NONE:自动确认
  AcknowledgeMode.AUTO:根据情况确认
  AcknowledgeMode.MANUAL:手动确认
  • 确认消息(局部方法处理消息)

默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual

或在 RabbitListenerContainerFactory 中进行开启手动 ack

@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);             //开启手动 ack
    return factory;
}

确认消息

@RabbitHandler
public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
    System.out.println(message);
    try {
        channel.basicAck(tag,false);            // 确认消息
    } catch (IOException e) {
        e.printStackTrace();
    }
}

需要注意的 basicAck 方法需要传递两个参数

deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel

multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

 

  • 手动否认、拒绝消息

发送一个 header 中包含 error 属性的消息

消费者获取消息时检查到头部包含 error 则 nack 消息

@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
    System.out.println(message);
    if (map.get("error")!= null){
        System.out.println("错误的消息");
        try {
            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
            return;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    try {
        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
    } catch (IOException e) {
        e.printStackTrace();
    }
}

此时控制台重复打印,说明该消息被 nack 后一直重新入队列然后一直重新消费

hello
错误的消息
hello
错误的消息

也可以拒绝该消息,消息会被丢弃,不会重回队列

channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息

 

  • 确认消息(全局处理消息)

自动确认涉及到一个问题就是如果在处理消息的时候抛出异常,消息处理失败,但是因为自动确认而导致 Rabbit 将该消息删除了,造成消息丢失

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");                 // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.NONE);     // NONE 代表自动确认
    container.setMessageListener((MessageListener) message -> {         //消息监听处理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //相当于自己的一些消费逻辑抛错误
        throw new NullPointerException("consumer fail");
    });
    return container;
}

手动确认消息

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL);        // 手动确认
    container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {      //消息处理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        if(message.getMessageProperties().getHeaders().get("error") == null){
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息已经确认");
        }else {
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息拒绝");
        }

    });
    return container;
}

 

AcknowledgeMode 除了 NONE 和 MANUAL 之外还有 AUTO ,它会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)

  • 如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认

  • 当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)

  • 当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认

  • 其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.AUTO);     // 根据情况确认消息
    container.setMessageListener((MessageListener) (message) -> {
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //抛出NullPointerException异常则重新入队列
        //throw new NullPointerException("消息消费失败");
        //当抛出的异常是AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且requeue=false
        //throw new AmqpRejectAndDontRequeueException("消息消费失败");
        //当抛出ImmediateAcknowledgeAmqpException异常,则消费者会被确认
        throw new ImmediateAcknowledgeAmqpException("消息消费失败");
    });
    return container;
}

 

rabbitmq消息确认机制案例

这里我们使用springboot整合rabbitmq做项目演示,项目结构大家可以自己指定,网上关于springboot整合rabbitmq的demo也很多,这里我主要贴上关键代码,来加深理解消息确认机制的过程。

  • rabbitMQ的配置文件:
server.port=8082

#rabbitmq的相关配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/test

spring.rabbitmq.connection-timeout=2000ms

#生产者确认消息  confirmListener
spring.rabbitmq.publisher-confirms=true
#消息未被消费则原封不动返回,不被处理  returnListener  和 mandatory 配合使用
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-mandatory=true

#定义消费者最多同时消费10个消息
#spring.rabbitmq.listener.simple.prefetch=10
spring.rabbitmq.listener.simple.concurrency=1
spring.rabbitmq.listener.simple.max-concurrency=5
#设置手动确认消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#支持消息重试
spring.rabbitmq.listener.simple.retry.enabled=true
  • controller层模拟发送一条消息
	/**
	 * 发送对象消息
	 * @return
	 */
	@GetMapping("/sendEmployeeMessage")
	@ResponseBody
	public String sendEmployeeMessage(){
		Employee employee = new Employee();
		employee.setAge(23);
		employee.setEmpno("007");
		employee.setName("jike");
		messageProducer.sendMessage(employee);
		return "success";
	}
  • 我们来看 上面的sendMessage 这个方法,在这个方法里面主要做了两件事,一个是发送对象消息,然后就是在发送过程中添加了消息确认的回调函数,要注意的是这里的回调函数目前跟消费者的通道是没有任何关系的,即消息最终能否成功发送到exchange上以及exchange能否将消息路由到指定的队列
@Component
public class MessageProducer {
	
	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	//消息确认机制,如果消息已经发出,但是rabbitmq并没有回应或者是拒绝接收消息了呢?就可以通过回调函数的方式将原因打印出来
	RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
		
		public void confirm(CorrelationData correlationData, boolean isack, String cause) {
			System.out.println("本次消息的唯一标识是:" + correlationData);
			System.out.println("是否存在消息拒绝接收?" + isack);
			if(isack == false){
				System.out.println("消息拒绝接收的原因是:" + cause);
			}else{
				System.out.println("消息发送成功");
			}
		}
	};
	
	//有关消息被退回来的具体描述消息
	RabbitTemplate.ReturnCallback returnCallback = new ReturnCallback() {
		
		@Override	
		public void returnedMessage(Message message, 
									int replyCode, 
									String desc, 
									String exchangeName, 
									String routeKey) {
			System.out.println("err code :" + replyCode);
			System.out.println("错误消息的描述 :" + desc);
			System.out.println("错误的交换机是 :" + exchangeName);
			System.out.println("错误的路右键是 :" + routeKey);
			
		}
	};
	
	//发送对象消息时
	/**
	 * CorrelationData  标识消息唯一性的主体对象,可以自己设定相关的参数,方便后续对某条消息做精确的定位
	 * confirmCallback  消息投递到rabbitmq是否成功的回调函数,如果不成功,我们可以在该回调函数中做相关的处理
	 * returnCallback   消息被退回的回调函数
	 * @param employee
	 */
	public void sendMessage(Employee employee){
		CorrelationData cData = new CorrelationData(employee.getEmpno() + "-" + new Date().getTime());
		rabbitTemplate.setConfirmCallback(confirmCallback);
		rabbitTemplate.setReturnCallback(returnCallback);
		rabbitTemplate.convertAndSend("springboot-exchange", "hr.employee",employee,cData);
	}
	
}
  • 消费者一端代码,实际开发中,消费端的项目可能会在其他的工程中,这个并不会影响使用,
@Component
public class HandlerOrderMessage {
	
	@RabbitListener(queues="java_queue")
	@RabbitHandler
	public void handleEmployeeMsg(@Payload Employee employee,Channel channel,
								  @Headers Map<String, Object> headers){
		
		System.out.println("消费者开始接收员工消息 =================");
		System.out.println(employee.getName());
		Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
		try {
			channel.basicAck(tag, false);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
}
  • 在我们启动项目之前,还有一个很重要的配置,想必大家也很快就想到了,就是config的配置文件,在整合rabbitmq中,我们需要提前准备一个bean的class,用于在项目启动时候初始化相关的队列以及设置队列和exchange进行绑定的相关代码,这里为了模拟出效果,我们先不提供这个配置类,而且在rabbitmq控制台也不提前创建队列看看会有什么效果呢?启动项目后,浏览器输入,http://localhost:8082/map/sendEmployeeMessage,我们看一下控制台的输出,

å¨è¿éæå¥å¾çæè¿°

可以肯定的是这条消息发送失败了,失败的原因是什么呢?我们在看看后面的日志,意思是在/test这个虚拟的virtualhost下面没有找到这个交换机

å¨è¿éæå¥å¾çæè¿°

从这里我们可以印证示意图中所说的第一点,然后我们手动创建上这个exchange,但是并不做springboot-exchange和队列的绑定,然后再次访问,http://localhost:8082/map/sendEmployeeMessage,看看控制台的答应结果,

å¨è¿éæå¥å¾çæè¿°

这里消息走到了returnCallback 这个回调函数里面,意思就是消息被退回来了,按照上面的分析就是消息发送到了exchange,但是exchange没有找到合适的队列进行投递,因此被退回了,注意的是

"发送成功"是消息发送到exchange这个里面发送成功了,这个发送成功的回到函数是,confirmCallback,而是否能够发送到队列成功的回调函数是,returnCallback,注意区分开

基本上到这里,就把消息确认机制的基本原理讲完了,实际工作中,我们可以继续进行后续的处理,比如消息发送失败了该如何处理,如何第一时间反馈到开发人员进行问题的排查,都可以在回调函数里面做一些处理的。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值