RabbitMQ常见问题解决方案

上一篇博客记录了一下RabbitMQ的服务搭建和简单入门,但是光这些还远远不够。
要想将RabbitMQ用于生产中,需要考虑和解决很多问题。

目录

  • 消息转换器

  • 生产者如何确保消息发送不丢失?

  • 消费者如何防止消息丢失?

  • 消息预取

  • 死信交换机


消息转换器

原生的RabbitMQ只能发送字节数组,与SpringBoot整合后,Spring允许开发者发送一个对象,原因在于Spring对消息的发送和接收进行了一些处理。

默认的消息转换器为:SimpleMessageConverter,转换大致逻辑如下:

  • 如果请求的contentType是以text打头,则将消息转为String。转之前判断是否给定字符集,如果没有给定则以默认字符集UTF-8转换。
  • 如果contentType等于application/x-java-serialized-object则将消息进行Java序列化传输。
  • 如果都不满足以上条件,则不进行转换,原样传输。

自定义消息转换器

如果有必要,也可以选择自己实现消息转换器。
创建类实现MessageConverter接口,生产者实现toMessage方法,消费者实现fromMessage方法。

public class MyMessageConverter implements MessageConverter {

	//生产者发送转换
	@Override
	public Message toMessage(Object o, MessageProperties messageProperties) throws MessageConversionException {
		//使用FastJson
		Message message = new Message(JSONObject.toJSONBytes(o), messageProperties);
		return message;
	}

	//消费者接收转换
	@Override
	public Object fromMessage(Message message) throws MessageConversionException {
		return JSONObject.parse(message.getBody());
	}
}

template设置自定义转换器

//设置自定义消息转换器
template.setMessageConverter(new MyMessageConverter());

生产者如何确保消息发送不丢失?

消息发送成功分两种:

  • 消息到达Broker
  • 消息被成功路由到Queue

一般从业务角度来说,只有消息被路由到Queue中,才算真正的发送成功。

针对这两种情况,RabbitMQ提供了两种回调:ConfirmCallback和ReturnCallback。

ConfirmCallback

ConfirmCallback针对的是“消息是否到达Broker”,至于消息是否被路由到Queue它是不管的。

当消息发送到Broker中,RabbitMQ会发出一个响应,告诉生产者自己已经接受到了消息。

代码实现

开启发送者确认

//开启 发送者确认
connectionFactory.setPublisherConfirms(true);

编写消息发送确认回调类

/**
 * @Author: pch
 * @Date: 2020/1/11 11:06
 * @Description: email消息发送方确认回调
 */
public class EmailConfirmCallback implements RabbitTemplate.ConfirmCallback {

	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		//发送消息时没传递correlationData,这里就为null
		System.err.println("消息附带标识:" + correlationData);
		//消息是否到达broker
		System.err.println("消息是否到达broker:" + ack);
		//消息发送失败的原因(RabbitMQ服务宕机等)
		System.err.println("失败原因:"+cause);
		
		if (!ack) {
			//消息发送失败,写入到数据库,等待后续处理
		}
	}
}

template设置发送确认回调

//开启 发送者确认
template.setConfirmCallback(new EmailConfirmCallback());

用户注册接口,注册成功后发送邮件业务代码

@PostMapping("register")
public Object register(String name, String email) {
	//保存注册信息逻辑...
	System.out.println(name + "-注册成功,开始发送email消息...");

	//发送消息到队列
	Map map = new HashMap<>(2);
	map.put("name", name);
	map.put("email", email);

	//发送消息时,可以携带一个CorrelationData保存业务主键
	CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString().toUpperCase());
	rabbitTemplate.convertAndSend("emailExchange", "email_error", JSONObject.toJSONString(map),correlationData);

	return name + "-注册成功";
}

发出请求,控制台如下图所示:
RabbitMQ常见问题解决方案_第1张图片

显示消息已经到达Broker,但是消息是否被路由到Queue,这里就无法判断了,需要使用ReturnCallback。

ReturnCallback

ReturnCallback回调针对的是消息是否成功被路由到队列,一般与ConfirmCallback配合使用。

如果消息到达Broker但是没有被路由到Queue,则会触发ReturnCallback。例如:Exchange没有绑定Queue。
反之,则不会触发ReturnCallback。

mandatory

RabbitMQ接收到消息进行路由时,会根据mandatory来进行不同的操作,mandatory默认为false。

如果mandatory为false,当消息无法被路由时,RabbitMQ会直接丢弃该消息。
mandatory为true,消息无法被路由时,RabbitMQ会调用Basic.Return命令将消息返回给生产者。

代码实现

开启发送者确认

//开启 发送者确认
connectionFactory.setPublisherConfirms(true);

编写消息失败回调类

/**
 * @Author: pch
 * @Date: 2020/1/11 12:46
 * @Description: email消息失败回调
 */
public class EmailReturnCallback implements RabbitTemplate.ReturnCallback {
	@Override
	public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
		System.err.println("消息内容:" + new String(message.getBody()));
		System.err.println("响应状态码:" + replyCode);
		System.err.println("响应内容:" + replyText);
		System.err.println("exchange:" + exchange);
		System.err.println("rou![在这里插入图片描述](https://img-blog.csdnimg.cn/20200111130632936.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMyMDk5ODMz,size_16,color_FFFFFF,t_70)tingKey:" + routingKey);
		
		//消息没有被路由到Queue,写入数据库,待后续处理...
	}
}

开启mandatory模式,并设置回调实例

//开启mandatory模式(开启失败回调)
template.setMandatory(true);
template.setReturnCallback(new EmailReturnCallback());

发送消息时,设置错误的routingKey,控制台显示如下:
RabbitMQ常见问题解决方案_第2张图片

设置正确的routingKey,则不会触发回调。

备用交换机

声明交换机时,可以设置一个属性:alternate-exchange来为其设置一个备用交换机。
当消息无法被路由到Queue时,RabbitMQ则会将消息交给备用交换机路由,备用交换机一般声明为FANOUT类型确保消息一定会被路由到Queue。

需要注意的是:即使消息在主交换机中没有路由成功,只要在备用交换机中路由成功,也不会触发ReturnCallback回调。
声明备用交换机后,只有当主交换机和备用交换机都无法成功路由时才会触发ReturnCallback回调。

总结

ConfirmCallback针对消息没有到达Broker的回调处理,ReturnCallback针对消息到达Broker但是没路由到Queue的回调处理。
一般将两者配合使用,可以保证消息发送100%不丢失。

RabbitMQ也支持事务,也可以做到消息发送不丢失,但是开启事务后性能严重下降,不建议使用。

消费者如何防止消息丢失?

除了生产者要保证消息100%发送外,消费者也必须确保消息不丢失,这样才能最终确保消息不丢失。

RabbitMQ的消息确认模式:

  • AcknowledgeMode.NONE
  • AcknowledgeMode.MANUAL
  • AcknowledgeMode.AUTO

默认为自动确认,即消息被消费者取走后Queue就会将其删除,不管其是否消费成功。

如果对数据的要求不高,如:日志记录,哪怕丢失一部分日志也无所谓,则可以使用自动确认,这样可以保证最好的性能。

但是如果对数据要求很高,则必须改为:手动确认。

消息手动确认

在手动确认模式下,消息被消费者取走之后,Queue不会将其删除,而是将消息的状态改为Unacked待确认,消费者获取到消息进行消费时,可能成功也可能失败。

消费成功时,则通知RabbitMQ,将消息从Queue中删除。

消费失败时,有两种选择,一是通知RabbitMQ将消息放回队列,交给其他消费者消费、二是认为消息是废数据,让RabbitMQ直接丢弃即可。

不管是否消费成功,都必须进行消息确认,否则消息会一直处于Unacked状态,堆积在Queue中。

消费者获取消息后宕机,消息会丢失吗?

不会,手动确认模式下,即使消费者获取到消息,Queue也不会将其删除,只是将消息的状态改为Unacked待确认。
消费者宕机后,连接就会断开,RabbitMQ检测到连接断开后,会将消息状态改为Ready分发给其他消费者。

虽然不会丢失消息,但是会带来另外一个问题:消息重复消费

例如:消费者获取消息成功消费后,在消息确认之前服务突然宕机,RabbitMQ会认为消息没有被成功消费,会分发给其他消费者,导致消息被重复消费,可以通过“消息幂等性”解决,后面会介绍。

开启消息手动确认,也可通过yml方式

@Bean("simpleContainerFactory")
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory){
	SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
	factory.setConnectionFactory(connectionFactory);
	//消息手动确认
	factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
	return factory;
}

消费者:消息的确认和退回

@Component
public class EmailConsumer {

	//监听的队列名称
	@RabbitListener(queues = "emailQueue", containerFactory = "simpleContainerFactory")
	public void consumer(Map map, Message message, Channel channel) throws Exception {
		long deliveryTag = message.getMessageProperties().getDeliveryTag();
		System.out.println("消费者,消息ID:" + deliveryTag);

		if (sendEmail(map.get("name"), map.get("email"))) {
			//邮件发送成功,确认消息	消息ID	是否批量确认
			channel.basicAck(deliveryTag, false);

		}else {
			//消息退回 		消息ID      是否批量确认    是否重回队列
			channel.basicNack(deliveryTag, false, true);

			//消息退回,只能单条 建议用basicNack
			//channel.basicReject(deliveryTag, true);
		}
	}

	//发送邮件
	private boolean sendEmail(String name, String email){
		System.out.println("发送邮件:" + email);
		//编写业务逻辑....

		return true;
	}
}

消息预取

聊消息预取之前,先说一说RabbitMQ的消息分发机制。

默认情况下,RabbitMQ会以最快的速度,将消息以轮询的方式全部分发给消费者,尽管消费者还来不及处理。
这样可以保证RabbitMQ本身不会因为消息堆积而影响性能,但是对消费者而言却不太友好。

举个例子:Queue中有100个消息,同时启动两个消费者,RabbitMQ会立即给每个消费者分发50个消息,假设消费者A性能很强,1S就能消费完50个消息,而消费者B性能弱,需要10S才能消费完。这时消费者A就会很空闲,消费者B就会很忙碌,没有充分利用A的性能。

而消息预取则可以根据消费者的实际情况来进行设置,开启消息预取后,RabbitMQ不会直接分发所有的消息,而是根据给定的预取数量来分发,当消费者全部处理完毕后,RabbitMQ才会进行下一轮的消息分发。

例如:Queue中有100个消息,消费者消费1个消息耗时1S,消息预取设为1,则RabbitMQ每秒分发一个消息,如下图所示:
RabbitMQ常见问题解决方案_第3张图片

开启消息预取后,还可以对消息进行批量确认,从而进一步提升性能。

设置消息预取的数量

@Bean("simpleContainerFactory")
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory){
	SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
	factory.setConnectionFactory(connectionFactory);
	//消息手动确认
	factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
	//消息预取数量
	factory.setPrefetchCount(100);
	return factory;
}

消息批量确认

//deliveryTag当前消息的标识,RabbitMQ会自动将一组消息确认
channel.basicAck(deliveryTag, true);

消息预取的数量需要设置一个合适的值,太小性能差但是数据可靠性高,太大性能高但是数据可靠性差。

消息重复消费

开启消息预取后,可能存在“消息重复消费”的问题。
批量获取100条消息,成功消费了99条,在消费最后一条时系统宕机,RabbitMQ会将100条消息重新分发给其他消费者。

RabbitMQ没有提供解决方案,需要在业务中自己解决,常见的解决方案:

  • 数据库表加字段区分消费标记。
  • 使用Redis来保存消费成功的标识。

死信交换机

在声明Queue时,可以给其设置一个“死信交换机”(dead-letter-exchange),当消息出现如下情况时,RabbitMQ会将消息重新发送到死信交换机上进行重新路由。

  • 消息被退回(channel.basicNack)
  • 消息过期(TTL)
  • 消息数量(x-max-length)超过队列最大限制而被删除
  • 消息总大小(x-max-length-bytes)超过队列最大限制而被删除

当消息出现以上状况时,如果不希望消息被RabbitMQ丢弃,就可以通过设置一个死信交换机来保存这些数据。

死信交换机的作用是:当消息不能被消费者正确消费时,将其路由到另外一个队列,等待重试或者人工干预。

声明队列,设置死信交换机,消息过期后会发送到deadExchange重新路由到队列。

@Bean
public Queue queue(){
	Map map = new HashMap<>();
	//设置过期时间
	map.put("x-message-ttl", 1000);
	//设置死信交换机
	map.put("x-dead-letter-exchange", "deadExchange");
	//死信交换机路由时新的routingKey
	map.put("x-dead-letter-routing-key", "dead.key");
	return new Queue("emailQueue", true, false, false, map);
}

消息退回,也是如此。

//消息退回 如果设置了死信交换机,则会被发送到死信交换机重新路由
channel.basicNack(deliveryTag,false,false);

避免死循环

使用RabbitMQ时需要特别注意避免死循环,消息始终无法被正确消费,但是RabbitMQ不断发送消息给消费者,造成死循环,例如:

  • 消费者只有一个时,进行消息回退且让消息重回Queue
  • Queue的死信交换机设为原交换机

你可能感兴趣的:(java,java,后端)