一.如何保证消息的可靠性?
下面是RabbitMq消息投递的过程
1.生产者消息丢失
- 客户端连接失败的情况,导致消息丢失。
- 生产者把消息发送到exchange交换机时,会导致消息丢失。
解决办法:
(1)客户端连接失败:我们可以配置连接失败的重连机制(注意这个配置只是连接失败的重试,如果消息异常是不会进行重连机制的)。
spring:
rabbitmq:
connection-timeout: 1s #设置Mq的连接超时时间
template:
retry:
enabled: true #开启超时重试机制
initial-interval: 1000ms #失败后的初始等待时间
multiplier: 1 #失败后下次等待时长的倍数,下次等待时长=initial-interval*multiplier
max-attempts: 3 #最大重试次数
(2)生产者把消息发送到exchange交换机失败:我们可以开启生产者确认机制,生产者Comfirm和Return两种机制。
confirm机制:broker接受到消息后发送一个确认消息给生产者,生产者接收后确认消息是否成功发送。
return机制:生产者发送消息将Mandatory(可以理解为开启return的一个开关)设置为true,如果消息无法路由到任何队列,RabbitMQ会发送一个return消息给生产者,生产者接收并处理这个消息。
注意:开启后当消息正确路由到队列时,生产者是不会接收到return的任何通知
spring:
rabbitmq:
#确保消息未被队列接收返回
publisher-returns: true
#发布消息成功到交换机后触发回调的方法
publisher-confirm-type: correlated
@Component
public class MqProductCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
/**
* @param correlationData 对象内部只有一个id属性,用来表示当前消息的唯一性
* @param ack 消息投递到broker的状态,true成功,false失败
* @param cause 投递失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack)
System.out.println("消息投递收到确认,correlationData=" + correlationData.getId());
if (!ack)
System.out.println("消息ID="+correlationData.getId()+"投递失败,失败原因:"+cause);
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("消息返回结果:"+ JSONUtil.toJsonStr(returnedMessage));
}
}
(3)开启事务的机制:生产者发送消息之前开启事务 channel.txSelect,成功channel.txCommit,失败channel.txRollback。不建议开启事务机制,它是阻塞的一个过程,Mq的性能会严重下降。
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost"); // 设置RabbitMQ服务器地址
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 开启事务
channel.txSelect();
try {
// 发送消息
// ...
// 提交事务
channel.txCommit();
} catch (Exception e) {
// 发生异常时回滚事务
channel.txRollback();
e.printStackTrace();
}
// 关闭连接和通道
channel.close();
connection.close();
}
(4)根据自己的业务我们还可以发送的每个消息都在数据库中做好记录,根据数据库中的数据状态来定期将失败的消息在次发送一遍,相当于日志记录。
总结:
1.由于网络波动,在配置文件中配置重连机制。
2.如果其他原因导致的消息丢失,RabbitMq有生产者确机制。
3.可以开启事务,来保证消息的可靠性,但是不建议这么做,性能会下降。
4.可以根据自己的业务来进行拓展,比如创建日志数据库表,失败定期发送等(生产者消费者都进行记录)。
5.上边的ConfirmCallback,ReturnsCallback会影响性能,根据自己的业务进行合理判断。
2.mq消息丢失
- 一旦MQ宕机,内存的消息就会丢失。
- 设置的内存空间有限,队列溢出等,会导致消息丢失。
解决办法:
因为默认的情况下Mq会把消息存到内存中,所以我们要将这些存在内存中的消息进行持久化到磁盘上。
(1)交换机持久化
@Bean
public DirectExchange exchange() {
return new DirectExchange(EXCHANGE_NAME, true, false); // 第二个参数为true表示交换机是持久的
}
(2)queue持久化
@Bean
public Queue queue() {
return new Queue(QUEUE_NAME, true); // 第二个参数为true表示队列是持久的
}
(3)消息持久化
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
(4)改变队列模式:Lazy Queue:
接到消息后会先存入磁盘中而非内存,内存中只保留(2048条消息)。
消费者读取消息时会从磁盘中读取并且加载到内存中,支持百万条消息存储。
//惰性队列
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable(LAZY_QUEUE).lazy().build();
}
总结:
1.首先配置交换机、队列、消息都进行持久化,队列的消息都可以存在磁盘上,MQ宕机消息依然在。
2.RabbitMQ3.12之后默认队列模式为Lazy Queue ,开启持久化和生产者确认时,都是存到磁盘上然后发送ack确认消息 。
3.消费者消息丢失
- 当队列queue投递到consumer时,RabbitMQ 会自动把发送出去的消息设置为确认,然后从内存或磁盘中删除,而不管消费者是否真正的接收到了消息,此时网络波动或者消费者宕机,所以导致消息丢失。
- 当消费者发生异常了,消息会在次重新发送给消费者,然后再次异常,在次发送,无限循环,导致mq的消息处理飙升,带来不必要的压力。
解决办法:
(1)消费者确认机制
当消费者处理消息结束后,应该想RabbitMq发送一个回执,告知RabbitMq自己的消息处理状态。
回执有三种可选值
ack成功处理消息,RabbitMq从队列中删除该消息;
nack消息处理失败,RabbitMq需要再次投递消息;
reject消息处理失败并拒绝该消息,RabbitMq从队列中删除该消息。
其中的ack又有三种方式
none:不处理,消息投递给消费者后立刻ack,消息会立即删除,非常不安全(不建议);
manual:手动模式,需要自己在业务代码中调用api;
auto:自动模式,业务正常执行返回ack,业务异常自动返回nack,消费者处理异常,自动返回reject。
spring:
rabbitmq:
#手动ack
listener:
simple:
acknowledge-mode: manual # none关闭ack auto开启自动ack
@RabbitListener(queues = "normal_queue")
/**
* @Header(AmqpHeaders.DELIVERY_TAG)Long tag
* 抽取该参数到方法中,其中AmqpHeaders.DELIVERY_TAG是为消费者提供唯一的标识符;
* 消费者通过DELIVERY_TAG接收或者拒绝消息
* (也可以接收Message取参)
* 不论业务逻辑是否处理成功,最终都要将消息手动签收。MQ本身就是辅助工具,不要给MQ过多压力,导致业务畸形
*/
public void handleMessage1(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) Long tag) throws IOException {
log.info("正常队列接收到的消息是:{}", message);
try {
// 处理业务
} catch (Exception ex) {
// 记录日志,通过后台管理或其他方式人工处理失败的业务。
} finally {
// 手动签收
channel.basicAck(tag, false);
}
}
(2)消费者重试机制
spring:
rabbitmq:
listener:
simple:
prefetch: 1
retry:
enabled: true #开启消费者失败重试
initial-interval: 1000ms #初始的失败等待时长为1秒
multiplier: 1 #下次等待的时长倍数,下次等待时长=multiplier * initial-interval
max-attempts: 3 #最大重试次数
stateless: true #true无状态,false有状态,如果业务中包含事务,要设置false
在开启重试机制后,重试次数耗尽,消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现。
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"errorExchange","errorRoutingKey");
}
总结:消费者如何保证消息不丢失?
1.开启手动消息确认机制acknowledge-mode: manual,确认消息处理用ack,异常用nack。
2.开启消费者失败重试机制:并设置MessageRecoverer,多次重试失败后将消息投递到异常的交换机,交由人工处理。
二.如何保证消息不被重复消费
幂等性:是一个数学概念,用于描述一个操作或函数在其被多次执行时产生的效果。也就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。
例如在开发中,商城订单和库存系统,当我们提交订单时,我们应该去锁定库存,但是由于网络原因,我们提交订单时没有反应,我们又提交了一次,此时减库存的操作执行2次,导致库存数量不对,这就是没有实现幂等。我们可用token机制或者各种锁的机制来实现接口的幂等性。
保证消息不被重复消费
1.消费方业务接口做好幂等
(具体根据自己业务来拓展,比如乐观锁、各种唯一约束、防重表等)
2.唯一消息id
给每个消息都设置唯一的id,利用id来区分是否重复消费(业务有入侵性,因为要查表,影响性能)
(1)每条消息都生成一个唯一id,跟消息一起投递给消费者。
(2)消费者接收到的消息可以处理自己的业务,处理完成将id保存数据库中(redis)。
(3)如果下次又收到相同的消息,我们可以查询该消息是否被消费过。
设置唯一id
@Bean
public MessageConverter messageConverter(){
//定义消息转换器
Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
//配置自动创建消息id,用于识别不同的消息,也可以基于id去做判断是否重复消息
messageConverter.setCreateMessageIds(true);
return messageConverter;
}
查看源码发现,createMessageIds默认的是false ,我们需要设置为true开启,并且id生成策略为UUID。
拿到messageProperties后我们可以更改为雪花算法,具体不展示了,自行百度。
3.结合业务逻辑判断
例如:
商城订单模块中,当客户支付后,我们要需改订单的状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付,只有未支付的订单才需要需改,其他状态不做处理。
也就是根据业务逻辑来进行判断,我们收到消息然后更改订单状态,此时又收到一样的消息,因为我们查询订单状态为已经支付了,那么我们就不需要在修改状态了,这样就保证了同样的消息不会被消费。不管这个订单消息执行1次或者10次,我们最终的结果是订单只能被修改1次。
三.如何避免消息堆积
什么原因导致的消息堆积?怎么避免?
1.消费者处理消息太慢导致的消息堆积
(1)我们可以增加消费者的数量,通过水平拓展,增加消费者的数量来提高消息处理能力(只要钱到位,都不是问题,如果没钱就用第四种,嘿嘿)。
(2)优化消费者的性能,提高消费者处理消息的效率比如代码优化等等。
(3)消息预取限制:调整消息的预取数量channel.basicQos(),避免一次处理过多的消息导致消费者处理缓慢。注意的是通常是通过basic.qos方法在信道级别设置的,这实际上会影响该信道上的所有消费者。
(4)建立专门的队列消费服务,将消息批量取出并持久化(存数据库),之后再慢慢消费。
2.消费者故障导致消息堆积
(1)我们可以用死信队列,将无法消费的消息发送到死信队列,防止阻塞主队列。
(2)我们要做好容错,实现消费者的自动重启和错误的处理逻辑。
3.消息生产速度大于消费者消费速度
(1)使用消息限流:控制消息的生产速度,确认不会超过消费者的消息处理能力。
(2)负载均衡:确保消息在消费者进行公平的消费处理,防止个别消费者过载。
文章总结:
消息丢失、消息重复、消息积压三个问题中,实际上主要解决的还是消息丢失,因为我们很少遇到消息积压的场景,而稍微有水平的公司核心业务都会解决幂等问题,所以几乎不存在消息重复的可能;我们不要小看RabbitMq的性能,太多的措施不仅会给MQ带来压力,也会增加我们系统的复杂程度。