RabbitMQ 应用问题

目录

前言

一、幂等性保障

[1. 幂等性概念](#1. 幂等性概念)

[2. MQ 幂等性介绍](#2. MQ 幂等性介绍)

[3. 幂等性的解决方法](#3. 幂等性的解决方法)

二、顺序性保障

[1. 顺序性介绍](#1. 顺序性介绍)

[2. 解决方案](#2. 解决方案)

三、消息积压

[1. 原因分析](#1. 原因分析)

[2. 解决方案](#2. 解决方案)

四、仲裁队列

总结


前言

本文介绍 RabbitMQ 在实际应用中,可能出现的一些应用问题,如幂等性保障,顺序性保障,消息积压等;以及针对这些问题,一些典型的解决方案。


一、幂等性保障

1. 幂等性概念

在计算机领域中,幂等性指的是同一个接口,请求或者操作,发起一次或重复发起 N 次,对系统资源或者数据状态,造成的影响完全相同,不会产生脏数据,重复扣款或者重复创建等问题。

核心本质:重复执行,结果一致

技术价值:解决消息重复,重试带来的数据错乱问题;

MQ 消息消费幂等:同一条消息重复消费 N 次,业务只生效一次

2. MQ 幂等性介绍

通常消息中间件的消息传输保障分为 3 个等级:

  1. at most once:最多一次,消息有可能会丢失,但绝不可能重复传输;
  2. at least once:最少一次,消息绝不会丢失,但是可能重复传输;
  3. exactly once: 恰好一次,每条消息只会被传输一次;

RabbitMQ 支持最多一次和最少一次;

通常业务可靠性要求比较高时,会使用"最少一次",防止消息丢失。但是最少一次,就会存在消息重复传输的问题;

如果不对重复消息进行幂等性处理,有可能会造成严重的问题。比如用户付款后,由于网络问题,业务没有进行处理,用户重复点击后,再次进行了扣款。

3. 幂等性的解决方法

1. 全局唯一 ID

1)为每条消息分配一个唯一 ID,比如 UUID 或者 MQ 中的 deliveryId;

2)消费者收到消息后,判断该 Id 是否已经消费过;

3)如果未消费过,则开始消费消息,并保存唯一 ID;如果消费过,则放弃处理;

2. 业务逻辑判断

处理消息之前,先检查相关业务状态,确保对应的操作尚未执行,再进行消息的处理;

二、顺序性保障

1. 顺序性介绍

消息的顺序性指的是:消费者消费消息的顺序和生产者发送消息的顺序是一致的

有些业务场景,消费者消费消息是不需要保障顺序的;比如不同用户的操作,即使顺序不一致,也不影响业务处理的结果;

有些业务场景,可能存在多个消息顺序处理的场景;比如同一个用户的操作,顺序不一致,可能会影响结果;

RabbitMQ 如果一个生产者只有一个消费者,借助队列先进先出的特性,可以保障消息的顺序;

打破 RabbitMQ 顺序性的场景:

    1. 多个消费者:多个消费者处理消息的速度不相同,不能保证消息的顺序性;
    1. 网络波动:消息确认丢失,导致消息重新入队,重新消费;
    1. 消息重试:消费者处理消息后没有及时确认,导致消息重试;
    1. 消息路由:消息根据路由键不同,被路由到不同的队列;
    1. 死信队列:消息消费时出现异常,消息被路由到死信队列;

2. 解决方案

消息的顺序保障分为:局部消息顺序保障和全局消息顺序保障

局部顺序保障:在单个队列中保证顺序;

全局顺序保障:在多个队列和对各消费者之间保障顺序;

常用策略:

  1. 单队列单消费者:利用队列的先进先出特性,保证消息的顺序;

  2. 分区消费:单个消费者的吞吐太低,当需要多个消费者提高处理速度时,可以使用分区消费,把一个队列分割成多个分区:每个分区由一个消费者进行处理,保证分区内消息的顺序性;

  3. 消息确认:消息确认,消费者按照顺序进行确认,每处理完一个消息,按照消息顺序发送确认;

  4. 业务逻辑控制:消息可能会乱序达到,由业务逻辑实现顺序控制;

三、消息积压

消息积压指的是,消费者消费消息的速度小于生产者生产消息的速度,导致消息在队列中越来越多;

1. 原因分析

消息积压通常有一下几种原因:

  1. 消息生产过快:消息生产的速度超过了消费的速度;

  2. 消费者能力不足:消费者能力不足,如消费逻辑复杂耗时,消费代码性能不足,系统资源限制,异常处理不当等;

  3. 网络问题:由于网络问题,消费者无法及时接收或者确认消息;

  4. RabbitMQ 服务器配置偏低;

2. 解决方案

出现消息积压的情况,首先要分析消息积压的原因,根据原因调整策略;

1. 提高消费者效率

1)增加消费者数量

2)优化业务逻辑,使用多线程的解决方案;

3)设置 prefetchCount,当一个消费者阻塞,分发消息到另一个消费者;

4)消息消费异常时,重试或者转发到死信队列;

2. 限制生产者速率

1)在消息生产者中实现流量控制;

2)限流,给生产者发送速率设置上限;

3)设置过期时间:过期消息进入死信队列;

3. 升级 RabbitMQ 服务器的配置

四、仲裁队列

RabbitMQ 的仲裁队列是一种基于 Raft 一致性算法实现的持久化,复制的 FIFO 队列;

仲裁队列提供队列的复制能力,保障数据的高可用和安全性;

仲裁队列可以在 RabbitMQ 节点之间进行队列数据的复制,从而实现一个节点宕机,其它节点依然可以提供服务;

Raft 协议下的消息复制:

每个仲裁队列都有多个副本,包含一个主副本和多个从副本,每个副本都在不同的 RabbitMQ 节点上;

客户端只会与主副本进行交互,主副本再将这些命令复制到从副本。当主副本所在的节点下线,另外一个从副本就会被选举为主副本,继续提供服务;

消息复制和主副本选举的操作,需要半数以上的副本同意,当生产者发送一条消息,需超过半数的队列副本,将消息写入磁盘以后,才会向生产者进行确认,这意味着少部分比较慢的副本不影响整个队列的性能;

交换机,队列及其绑定关系:

java 复制代码
public class Constants {
    public static final String QUORUM_QUEUE = "quorum.queue";
    public static final String QUORUM_EXCHANGE = "quorum.exchange";
    public static final String  CLUSTER_QUEUE = "cluster.queue";
    public static final String CLUSTER_EXCHANGE = "cluster.exchange";
}
java 复制代码
@Configuration
public class RabbitMQConfig {
    /**
     * 仲裁队列
     * @return
     */
    @Bean("quorumQueue")
    public Queue quorumQueue(){
        return QueueBuilder.durable(Constants.QUORUM_QUEUE).quorum().build();
    }
    @Bean("quorumExchange")
    public DirectExchange quorumExchange(){
        return ExchangeBuilder.directExchange(Constants.QUORUM_EXCHANGE).build();
    }
    @Bean("quorumBinding")
    public Binding quorumBinding(@Qualifier("quorumQueue") Queue queue,
                                 @Qualifier("quorumExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("quorum");
    }
}

生产者:

java 复制代码
@RestController
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("quorum")
    public String quorum(){
        String message = "hello, quorum test...";
        rabbitTemplate.convertAndSend(Constants.QUORUM_EXCHANGE, "quorum", message);
        return "消息发送成功!";
    }
}

运行结果:

可以看到队列后面有两个镜像节点,都保存了仲裁队列的从副本;

停止主节点,观察仲裁队列:

可以看到队列中的消息仍然存在,没有丢失。原因是从副本重新选举为主副本。

当有多个仲裁队列时,主副本和从副本会分布在集群的不同节点上,每个节点可以承载多个主副本和从副本。


总结

本文仅仅简单介绍了 RabbitMQ 在实际应用中可能存在的一些问题,以及这些问题的常见的解决方案。

相关推荐
2401_833269303 小时前
Java网络编程入门
java·开发语言
金銀銅鐵4 小时前
[Java] 如何将 Lambda 表达式对应的类保存到 class 文件中?
java·后端
それども4 小时前
Gradle 构建疑难杂症 Could not find netty-transport-native-epoll-linux-aarch_64.ja
java·服务器·gradle·maven
还在忙碌的吴小二4 小时前
XXL-JOB - 分布式任务调度平台新手入门指南
分布式
正儿八经的少年5 小时前
application.yml 系列配置文件作用与区别
java·配置文件
鱼很腾apoc5 小时前
【学习篇】第20期 超详解 C++ 多态:从语法规则到底层原理
java·c语言·开发语言·c++·学习·算法·青少年编程
cheems95276 小时前
[Spring MVC] 统一功能与拦截器实践总结
java·spring·mvc
Full Stack Developme6 小时前
Spring Boot 事务管理完整教程
java·数据库·spring boot
城管不管7 小时前
前后端远程协作
java
青云计划7 小时前
Feed流
java·后端·spring