RabbitMQ 从入门到精通

1 MQ架构设计原理

1.1 什么是消息中间件

消息中间件基于队列模型实现异步/同步传输数据

作用:可以实现支撑高并发、异步解耦、流量削峰、降低耦合度。

1.2 传统的http请求存在那些缺点

1.Http请求基于请求与响应的模型,在高并发的情况下,客户端发送大量的请求到达服务器端有可能会导致我们服务器端处理请求堆积。

2.Tomcat服务器处理每个请求都有自己独立的线程,如果超过最大线程数会将该请求缓存到队列中,如果请求堆积过多的情况下,有可能会导致tomcat服务器崩溃的问题。

所以一般都会在nginx入口实现限流熔断 网关整合Sentinal,整合服务保护框架。

(nginx可以从博主的Nginx从入门到入土(一):DNS域名解析_nginx 域名解析-CSDN博客等系列文章了解)

"Sentinel" 指的是阿里巴巴开源的 流量控制和服务保护框架 ,全称为 Sentinel。它是一个面向分布式系统的轻量级流量控制组件,主要用于实现限流、熔断、系统负载保护等功能,确保服务在高并发或异常情况下的稳定性。

3.http请求处理业务逻辑如果比较耗时的情况下,容易造成客户端一直等待,阻塞等待过程中会导致客户端超时发生重试策略,有可能会引发幂等性问题。

重试策略 :当客户端发起 HTTP 请求后,如果服务器处理时间过长,客户端可能会因为超时而认为请求失败,从而触发 重试机制。重试策略是指客户端在请求失败时,自动重新发送请求的行为。

幂等性问题:由于重试导致请求被重复处理,可能引发数据不一致或重复操作的问题

注意事项:接口是http协议的情况下,最好不要处理比较耗时的业务逻辑,耗时的业务逻辑应该单独交给多线程或者是mq处理。

1.3 Mq应用场景有那些

  1. 异步发送短信
  2. 异步发送新人优惠券
  3. 处理一些比较耗时的操作

1.4 为什么需要使用mq

可以实现支撑高并发、异步解耦、流量削峰、降低耦合度。

1.5 同步发送http请求

场景描述

在会员注册的业务逻辑中,客户端发送请求到服务器端后,服务器需要依次执行以下操作:

  1. 插入会员数据(insertMember():耗时约 1 秒。

  2. 发送登录短信提醒(sendSms():耗时约 3 秒。

  3. 发送新人优惠券(sendCoupons():耗时约 3 秒。

如果采用同步处理方式,客户端需要阻塞等待 7 秒 才能收到响应,这会导致用户体验较差,尤其是在高并发场景下,还可能引发请求超时、重试等问题。

优化方案

为了提升系统性能和用户体验,可以采用 异步处理 的方式,将耗时操作从主流程中解耦。以下是两种常见的实现方式:

  1. 多线程异步

通过多线程技术,将耗时操作(如发送短信、发放优惠券)放到独立的线程中执行,主线程只需处理核心逻辑(如插入会员数据),从而快速响应客户端。

优点

  • 实现简单,适合小规模系统。

  • 无需引入额外中间件。

缺点

  • 线程管理复杂,容易引发资源耗尽问题。

  • 无法保证消息的可靠性和顺序性。

  1. 消息队列(MQ)异步

通过消息队列(如 RabbitMQ)将耗时操作异步化,具体流程如下:

  1. 客户端发送注册请求。

  2. 服务器处理核心逻辑(如插入会员数据),并将后续耗时操作(发送短信、发放优惠券)封装为消息,发送到消息队列。

  3. 服务器立即响应客户端,无需等待耗时操作完成。

  4. 消费者从消息队列中获取消息,异步执行发送短信、发放优惠券等操作。

优点

  • 解耦业务逻辑,提升系统可扩展性。

  • 支持高并发,避免请求阻塞。

  • 保证消息的可靠性和顺序性。

缺点

  • 需要引入消息队列中间件,增加系统复杂度。

推荐方案:基于 RabbitMQ 的异步处理

对于互联网项目(客户端为 Android/iOS,服务器端为 PHP/Java),推荐使用 RabbitMQ 实现异步处理,具体流程如下:

  1. 客户端:发送注册请求。

  2. 服务器

    • 处理核心逻辑(如插入会员数据)。

    • 将发送短信、发放优惠券等操作封装为消息,发送到 RabbitMQ。

    • 立即响应客户端,减少等待时间。

  3. RabbitMQ

    • 存储消息,确保消息不丢失。

    • 将消息分发给消费者。

  4. 消费者

    • 从 RabbitMQ 获取消息,异步执行发送短信、发放优惠券等操作。

通过这种方式,客户端只需等待核心逻辑的处理时间(如 1 秒),而无需阻塞等待所有操作完成,从而显著提升用户体验和系统性能。

1.6 多线程处理业务逻辑

用户向数据库中插入一条数据之后,在单独开启一个线程异步发送短信和优惠操作。

客户端只需要等待1s时间

优点:适合于小项目 实现异步

缺点:有可能会消耗服务器 cpu等资源

1.7 Mq处理业务逻辑

先向数据库中插入一条会员数据,让后再向MQ中投递一个消息,MQ服务器端在将消息推送给消费者异步解耦处理发送短信和优惠券。

1.8 Mq与多线程之间区别

MQ可以实现异步/解耦/流量削峰问题;

多线程也可以实现异步,但是消耗到cpu资源,没有实现解耦。

2 Mq设计基础知识

多线程版本mq

基于网络通讯版本mq netty 实现

2.1 基于多线程队列简单实现mq

java 复制代码
import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.LinkedBlockingDeque;

public class BoyatopThreadMQ {

    // 消息队列(Broker),用于存储生产者生成的消息
    private static LinkedBlockingDeque<JSONObject> broker = new LinkedBlockingDeque<>();

    public static void main(String[] args) {
        // 创建并启动生产者线程
        Thread producer = new Thread(() -> {
            while (true) {
                try {
                    // 模拟生产者每隔1秒生成一条消息
                    Thread.sleep(1000);

                    // 创建消息数据
                    JSONObject data = new JSONObject();
                    data.put("phone", "18611111111");

                    // 将消息放入队列
                    broker.offer(data);
                    System.out.println("生产者生成数据: " + data.toJSONString());
                } catch (InterruptedException e) {
                    System.err.println("生产者线程被中断: " + e.getMessage());
                    Thread.currentThread().interrupt(); // 恢复中断状态
                    break;
                }
            }
        }, "生产者");

        producer.start();

        // 创建并启动消费者线程
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    // 从队列中获取消息
                    JSONObject data = broker.poll();

                    if (data != null) {
                        // 处理消息
                        System.out.println(Thread.currentThread().getName() + " 消费数据: " + data.toJSONString());
                    }
                } catch (Exception e) {
                    System.err.println("消费者处理数据异常: " + e.getMessage());
                }
            }
        }, "消费者");

        consumer.start();
    }
}

输出示例:

生产者生成数据: {"phone":"18611111111"}

消费者 消费数据: {"phone":"18611111111"}

生产者生成数据: {"phone":"18611111111"}

消费者 消费数据: {"phone":"18611111111"}

...

2.2 基于netty实现mq

消费者netty客户端与nettyServer端MQ服务器端保持长连接,MQ服务器端保存消费者连接。

生产者netty客户端发送请求给nettyServer端MQ服务器端,MQ服务器端再将该消息内容发送给消费者。

body:{"msg":{"userId":"123456","age":"23"},"type":"producer","topic":""}

生产者投递消息与消息可靠性

  1. 消息持久化机制

生产者通过 Netty 客户端将消息投递到 RabbitMQ 服务器后,RabbitMQ 服务器需要缓存该消息。为了防止 RabbitMQ 服务器宕机导致消息丢失,RabbitMQ 采用了 持久化机制

  • 消息持久化:将消息存储到磁盘中,确保即使服务器重启,消息也不会丢失。

  • 队列持久化:将队列的元数据(如队列名称、绑定关系等)持久化到磁盘。

  • 交换机持久化:将交换机的元数据持久化到磁盘。

通过持久化机制,RabbitMQ 能够在服务器宕机后恢复消息和元数据,确保消息不丢失。

  1. 消息确认机制

当 RabbitMQ 接收到生产者投递的消息后,即使消费者暂时不在线,消息也不会丢失。这是因为 RabbitMQ 采用了 消息确认机制

  • 生产者确认(Publisher Confirm):生产者发送消息后,RabbitMQ 会返回一个确认(ACK),表示消息已成功接收并持久化。

  • 消费者确认(Consumer Ack):消费者成功消费消息后,会通过 Netty 长连接向 RabbitMQ 发送确认(ACK),RabbitMQ 才会将该消息从队列中删除。

  • 消息重试:如果消费者未发送确认,RabbitMQ 会认为消息未被成功消费,并将其重新投递给其他消费者。

消息投递模式

  1. RabbitMQ 服务器主动推送消息
  • RabbitMQ 服务器与消费者通过 Netty 保持长连接。

  • 当有新消息时,RabbitMQ 会通过 Netty 将消息推送给消费者。

  • 这种模式适合实时性要求高的场景,但需要消费者具备较高的处理能力。

  1. 消费者主动拉取消息
  • 消费者可以通过 Netty 客户端主动从 RabbitMQ 拉取消息。

  • 消费者首次启动时,会从 RabbitMQ 拉取未消费的消息。

  • 这种模式适合消费者处理能力有限的场景,但可能存在一定的延迟。

RabbitMQ 的高并发设计思想

  1. 基于 Netty 的高性能网络通信
  • RabbitMQ 使用 Netty 作为网络通信框架,支持高并发的消息传输。

  • Netty 的异步、事件驱动模型能够高效处理大量并发连接,适合 RabbitMQ 的高并发场景。

  1. 消费者负载均衡
  • RabbitMQ 支持多个消费者同时消费一个队列中的消息,实现负载均衡。

  • 通过 Netty 长连接,RabbitMQ 可以将消息均匀分发给多个消费者,提升整体消费能力。

  1. 消息预取(Prefetch)
  • RabbitMQ 允许消费者根据自身能力设置预取数量(Prefetch Count),即一次性从队列中拉取多条消息。

  • 通过调整预取数量,可以平衡消费者的处理能力和消息消费速率。

消费者消费速率的优化

  1. 消费者集群
  • 通过部署多个消费者实例(消费者集群),实现消息的并行处理,提升整体消费能力。

  • RabbitMQ 会自动将消息分发给不同的消费者,避免单点瓶颈。

  1. 批量拉取消息
  • 消费者可以通过 Netty 客户端一次性拉取多条消息进行批量处理,减少网络通信开销,提高消费效率。
  1. 异步处理
  • 消费者可以使用 Netty 的异步处理机制,将消息处理任务交给线程池或异步框架执行,避免阻塞主线程。

潜在问题与解决方案

  1. 消息消费延迟
  • 问题:在高并发场景下,消费者可能无法及时处理消息,导致消息积压和延迟。

  • 解决方案

    • 增加消费者实例,提升并行处理能力。

    • 调整预取数量,优化消费者的消息拉取策略。

    • 使用批量处理和异步处理机制,提高消费速率。

  1. 消息重复消费
  • 问题:在网络抖动或消费者异常的情况下,可能导致消息重复消费。

  • 解决方案

    • 在消费者端实现幂等性处理,确保多次消费同一条消息不会产生副作用。

    • 使用分布式锁或唯一标识(如消息ID)避免重复处理。

Maven依赖
java 复制代码
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.0.23.Final</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.11</version>
    </dependency>
  </dependencies>

2.3 Mq消息中间件名词

  • Producer 生产者:投递消息到MQ服务器端;
  • Consumer 消费者:从MQ服务器端获取消息处理业务逻辑;
  • Broker MQ服务器端
  • Topic 主题:分类业务逻辑发送短信主题、发送优惠券主题
  • Queue 存放消息模型 队列 先进先出 后进后出原则 数组/链表
  • Message 生产者投递消息报文:json

2.4 主流mq区别对比

|-------|---------------------------------|---------------------------------------|---------------|------------------------------------------------------|
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | kafka |
| 开发语言 | java | erlang | java | scala |
| 单机吞吐量 | 万级 | 万级 | 10万级 | 10万级 |
| 时效性 | ms级 | us级 | ms级 | ms级以内 |
| 可用性 | 高(主从架构) | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构) |
| 功能特性 | 成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低管理界面较丰富 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |

3 RabbitMQ

3.1 RabbitMQ基本介绍

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),RabbitMQ服务器是用Erlang语言编写的。

RabitMQ官方网站:

RabbitMQ: One broker to queue them all | RabbitMQ

1.点对点(简单)的队列

2.工作(公平性)队列模式

3.发布订阅模式

4.路由模式Routing

5.通配符模式Topics

6.RPC

https://www.rabbitmq.com/getstarted.html

3.2 RabbitMQ基本安装

由于都是外网的文件,开个梯子会有意想不到的效果

1.给出Erlang官网下载地址:Downloads - Erlang/OTP

等待下载,需要空间大概400MB少一点

2.配置erlang环境变量

找到控制面板里的环境变量,并新增一个系统变量。

我把该文件放在了F盘下,根据自己的需要编写变量值。

双击path,新增路径%ERLANG_HOME%\bin

3.打开命令窗口,输入erl或者erl -version()验证环境是否配置成功

一定得记住,打开的这些个窗口一定都得点确定保存!如果出现了命令行告诉你erl不是内部或者外部命令可能就是由于这个,亦或者是你的环境变量设置的不对。

4 安装rabbitmq

给出RabbitMQ官网下载址:Installing on Windows --- RabbitMQ。找到exe文件并点击下载安装

5 配置Rabbitmq环境变量

注意是sbin,是根据你目录来的

6 安装管理工具RabbitMQ-Plugins,进入sbin文件下,打开命令窗口输入

bash 复制代码
rabbitmq-plugins enable rabbitmq_management

直接进入目录,输入cmd跳进命令行

7 安装好管理工具后,进入sbin目录 ,点击rabbit_server.bat,启动rabbitMQ

8 最后输入http://localhost:15672/(默认账号:guest,密码:guest)就能进入RabbitMQ管理界面

3.3 RabbitMQ 启动常见问题:无法访问管理平台

问题描述

RabbitMQ 启动成功后,无法通过浏览器访问管理平台页面(通常为 http://localhost:15672)。

原因分析

默认情况下,RabbitMQ 的管理插件(rabbitmq_management)并未自动启用,需要手动启用该插件。

解决方案
  1. 启用 RabbitMQ 管理插件

  2. 打开命令行工具,并导航到 RabbitMQ 的 sbin 目录。例如:

    复制代码
    cd F:\path\rabbitmq\rabbitmq\rabbitmq_server-3.6.9\sbin
  3. 执行以下命令,启用管理插件:

    复制代码
    rabbitmq-plugins enable rabbitmq_management
  4. 启动 RabbitMQ 应用

如果 RabbitMQ 未完全启动,可以执行以下命令启动应用

复制代码
rabbitmqctl start_app
  1. 验证管理平台

  2. 打开浏览器,访问 RabbitMQ 管理平台

    复制代码
    http://localhost:15672
  3. 使用默认用户名和密码登录:

    • 用户名:guest

    • 密码:guest

注意事项
  • 确保 RabbitMQ 服务已启动。可以通过以下命令检查服务状态:

    复制代码
    rabbitmqctl status
  • 如果仍然无法访问,请检查防火墙设置,确保端口 15672 未被阻止。

  • 如果使用的是云服务器或远程主机,请确保安全组或防火墙规则允许外部访问 15672 端口。

3.4 Rabbitmq管理平台中心

RabbitMQ 管理平台地址 http://127.0.0.1:15672

默认账号:guest/guest 用户可以自己创建新的账号

Virtual Hosts:

像mysql有数据库的概念并且可以指定用户对库和表等操作的权限。那RabbitMQ呢?

RabbitMQ也有类似的权限管理。在RabbitMQ中可以虚拟消息服务器VirtualHost,每个VirtualHost相当月一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通。

  1. 默认的端口15672:rabbitmq管理平台端口号
  2. 默认的端口5672: rabbitmq消息中间内部通讯的端口
  3. 默认的端口号25672 rabbitmq集群的端口号

3.5 RabbitMQ 核心概念与关键配置

1. Virtual Hosts(虚拟主机)
  • 定义:虚拟主机是 RabbitMQ 中资源的逻辑隔离单元,类似于数据库中的"命名空间"。

  • 作用

    • 实现不同业务或环境(如开发、测试、生产)的消息队列隔离。

    • 每个 Virtual Host 拥有独立的队列、交换机、绑定关系等资源。

  • 示例

    • /booyaVirtualHosts:用于订单和支付业务。

    • /test:用于测试环境。

2. Queue(队列)
  • 定义:队列是消息的临时存储容器,生产者将消息发送到队列,消费者从队列中获取消息。

  • 特性

    • 持久化:队列可配置为持久化(Durable),防止 RabbitMQ 重启后丢失。

    • 自动删除:队列在无消费者时自动删除(Auto-Delete)。

  • 示例

    • 订单队列:存储订单相关消息。

    • 支付队列:存储支付相关消息。

3. Exchange(交换机)
  • 定义:交换机是消息路由的核心组件,负责将生产者的消息分发到指定队列。

  • 路由模式

    • Direct Exchange:基于路由键(Routing Key)精确匹配队列。

    • Topic Exchange:支持通配符的路由键匹配。

    • Fanout Exchange:广播消息到所有绑定队列。

    • Headers Exchange:基于消息头属性路由。

  • 类比

    • 类似于邮局的分拣系统,根据地址(路由规则)将信件(消息)投递到正确的邮箱(队列)。
4. 关键端口与协议
端口号 协议/用途 说明
15672 HTTP RabbitMQ 管理平台端口,用于 Web 界面操作(默认地址:http://localhost:15672)。
5672 AMQP(Advanced Message Queuing Protocol) RabbitMQ 的核心通信端口,客户端(生产者/消费者)通过此端口与服务器交互。
25672 Erlang 分布式通信 RabbitMQ 节点间集群通信和数据同步的端口。
5. AMQP 协议
  • 角色:AMQP 是 RabbitMQ 的核心协议,定义了消息的格式、传输规则及确认机制。

  • 特点

    • 支持异步、可靠的消息传递。

    • 提供事务、消息确认等机制保障可靠性。

4 快速入门RabbitMQ简单队列

4.1. 创建 Virtual Host 和队列

在 RabbitMQ 管理平台中,首先需要创建一个 Virtual Host 和队列。

  1. 登录 RabbitMQ 管理平台

    • 打开浏览器,访问 http://localhost:15672

    • 使用默认用户名和密码登录:

      • 用户名:guest

      • 密码:guest

  2. 创建 Virtual Host

    • 在管理平台中,导航到 Admin 选项卡。

    • 点击 Virtual Hosts,然后点击 Add a new virtual host

    • 输入 Virtual Host 名称(例如 /booyaVirtualHosts),点击 Add virtual host

  3. 创建队列

    • 导航到 Queues 选项卡。

    • 点击 Add a new queue

    • 输入队列名称(例如 订单队列),选择刚刚创建的 Virtual Host(/booyaVirtualHosts),点击 Add queue

    • 重复上述步骤,创建另一个队列(例如 支付队列)。

4.2 编写生产者代码

生产者负责向 RabbitMQ 队列发送消息。

java 复制代码
public class RabbitMQConnection {

    /**
     * 获取连接
     *
     * @return
     */
    public static Connection getConnection() throws IOException, TimeoutException {
        // 1.创建连接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.设置连接地址
        connectionFactory.setHost("127.0.0.1");
        // 3.设置端口号:
        connectionFactory.setPort(5672);
        // 4.设置账号和密码
        connectionFactory.setUsername("zhangsan");
        connectionFactory.setPassword("123456");
        // 5.设置VirtualHost
        connectionFactory.setVirtualHost("/Boyatop");
        return connectionFactory.newConnection();
    }
}
java 复制代码
public class Producer {

    private static final String QUEUE_NAME = "Boyatop";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.设置通道
        Channel channel = connection.createChannel();
        // 3.设置消息
        String msg = "hello world";
        System.out.println("msg:" + msg);
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        channel.close();
        connection.close();
    }
}

Maven依赖

java 复制代码
<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>3.6.5 </version>
    </dependency>
</dependencies>

4.3 编写消费者代码

消费者负责从 RabbitMQ 队列中接收并处理消息。

java 复制代码
public class Consumer {

    private static final String QUEUE_NAME = "Boyatop";

 
    public static void main(String[] args) throws IOException, TimeoutException {

        // 1.创建连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.设置通道
        Channel channel = connection.createChannel();
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费者获取消息:" + msg);
            }
        };
        // 3.监听队列
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
    }
}

4.4 RabbitMQ如何保证消息不丢失

Mq如何保证消息不丢失:

4.4.1 生产者角色

确保生产者投递消息到MQ服务器端成功。

Ack 消息确认机制

同步或者异步的形式

  • 方式1:Confirms
  • 方式2:事务消息
4.4.2 消费者角色

在rabbitmq情况下:

必须要将消息消费成功之后,才会将该消息从mq服务器端中移除。

在kafka中的情况下:

不管是消费成功还是消费失败,该消息都不会立即从mq服务器端移除。

4.4.3 Mq服务器端 在默认的情况下 都会对队列中的消息实现持久化

1 使用消息确认机制+持久技术

A.消费者确认收到消息机制

channel.basicConsume(QUEUE_NAME, false, defaultConsumer);

注:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。

在处理完消息时,返回应答状态,true表示为自动应答模式。

channel.basicAck(envelope.getDeliveryTag(), false);

B.生产者确认投递消息成功 使用Confirm机制 或者事务消息

Confirm机制 同步或者是异步的形式

RabbitMQ默认创建是持久化的

代码中设置 durable为true

参数名称详解:

durable是否持久化 durable为持久化、 Transient 不持久化

autoDelete 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除

2 使用rabbitmq事务消息;

java 复制代码
            channel.txSelect();
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
            channel.txCommit();

相关核心代码

生产者

java 复制代码
public class Producer {

    private static final String QUEUE_NAME = "Boyatop-queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {

        //1.创建一个新连接
        Connection connection = RabbitMQConnection.getConnection();
        //2.设置channel
        Channel channel = connection.createChannel();
        //3.发送消息
        String msg = "Hello Wrold6666";
channel.confirmSelect();
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        boolean result = channel.waitForConfirms();
        if (result) {
            System.out.println("消息投递成功");
        } else {
            System.out.println("消息投递失败");
        }
        channel.close();
        connection.close();
    }
}

消费者

java 复制代码
public class Consumer {

    private static final String QUEUE_NAME = "Boyatop-queue";

    public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException {
        // 1.创建连接
        Connection connection = RabbitMQConnection.getConnection();
        // 2.设置通道
        Channel channel = connection.createChannel();
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费者获取消息:" + msg);
                // 消费者完成 消费该消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 3.监听队列
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

5 RabbitMQ五种消息模式

5.1RabitMQ工作队列

默认的传统队列是为均摊消费,存在不公平性;如果每个消费者速度不一样的情况下,均摊消费是不公平的,应该是能者多劳。

采用工作队列

在通道中只需要设置basicQos为1即可,表示MQ服务器每次只会给消费者推送1条消息必须手动ack确认之后才会继续发送。

channel.basicQos(1);

5.2 RabbitMQ交换机类型

  • Direct exchange(直连交换机)
  • Fanout exchange(扇型交换机)
  • Topic exchange(主题交换机)
  • Headers exchange(头交换机)
  • /Virtual Hosts---区分不同的团队
  • ----队列 存放消息
  • ----交换机 路由消息存放在那个队列中 类似于nginx
  • ---路由key 分发规则

5.3 RabbitMQ Fanout 发布订阅

生产者发送一条消息,经过交换机转发到多个不同的队列,多个不同的队列就多个不同的消费者。

原理:

  1. 需要创建两个队列 ,每个队列对应一个消费者;
  2. 队列需要绑定我们交换机
  3. 生产者投递消息到交换机中,交换机在将消息分配给两个队列中都存放起来;
  4. 消费者从队列中获取这个消息。

生产者代码

java 复制代码
import com.Boyatop.rabbitmq.RabbitMQConnection;

  import com.rabbitmq.client.Channel;

  import com.rabbitmq.client.Connection;

  

  import java.io.IOException;
  import java.util.concurrent.TimeoutException;

 
  public class ProducerFanout {
    /**
     * 定义交换机的名称
     */
    private static final String EXCHANGE_NAME = "fanout_exchange";


    public static void main(String[] args) throws IOException, TimeoutException {

        //  创建Connection
        Connection connection = RabbitMQConnection.getConnection();
        // 创建Channel
        Channel channel = connection.createChannel();
        // 通道关联交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
        String msg = "Hello Wrold6666";
        channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
        channel.close();
        connection.close();
    }
}

消费者代码

邮件消费者
java 复制代码
  import com.Boyatop.rabbitmq.RabbitMQConnection;
  import com.rabbitmq.client.*;
  import java.io.IOException;
  import java.util.concurrent.TimeoutException;

 
  public class MailConsumer {

    /**
     * 定义邮件队列
     */
    private static final String QUEUE_NAME = "fanout_email_queue";
    /**
     * 定义交换机的名称
     */
    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {

        System.out.println("邮件消费者...");
        // 创建我们的连接
        Connection connection = RabbitMQConnection.getConnection();
        // 创建我们通道
        final Channel channel = connection.createChannel();
        // 关联队列消费者关联队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("邮件消费者获取消息:" + msg);
            }
        };
        // 开始监听消息 自动签收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }

}
短信消费者
java 复制代码
  public class SmsConsumer {

    /**
     * 定义短信队列
     */
    private static final String QUEUE_NAME = "fanout_email_sms";
    /**
     * 定义交换机的名称
     */
    private static final String EXCHANGE_NAME = "fanout_exchange";


    public static void main(String[] args) throws IOException, TimeoutException {

        System.out.println("短信消费者...");
        // 创建我们的连接
        Connection connection = RabbitMQConnection.getConnection();
        // 创建我们通道
        final Channel channel = connection.createChannel();
        // 关联队列消费者关联队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("短信消费者获取消息:" + msg);
            }
        };
        // 开始监听消息 自动签收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

5.4 Direct路由模式

当交换机类型为direct类型时,根据队列绑定的路由建转发到具体的队列中存放消息

5.5 Topic主题模式

当交换机类型为topic类型时,根据队列绑定的路由建模糊转发到具体的队列中存放。

#号表示支持匹配多个词;

*号表示只能匹配一个词

6 SpringBoot整合RabbitMQ

6.1 Maven依赖

java 复制代码
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
  </parent>

  <dependencies>
    <!-- springboot-web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot对amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>

6.2 配置类

java 复制代码
  import org.springframework.amqp.core.Binding;
  import org.springframework.amqp.core.BindingBuilder;
  import org.springframework.amqp.core.FanoutExchange;
  import org.springframework.amqp.core.Queue;
  import org.springframework.context.annotation.Bean;
  import org.springframework.stereotype.Component;


 /**
 * @ClassName RabbitMQConfig
 * @Author 
 * @Version V1.0
 **/
  @Component
  public class RabbitMQConfig {
    /**
     * 定义交换机
     */
    private String EXCHANGE_SPRINGBOOT_NAME = "/Boyatop_ex";

    /**
     * 短信队列
     */
    private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
    /**
     * 邮件队列
     */
    private String FANOUT_EMAIL_QUEUE = "fanout_email_queue";

    /**
     * 配置smsQueue
     *
     * @return
     */
    @Bean
    public Queue smsQueue() {
        return new Queue(FANOUT_SMS_QUEUE);
    }

 
    /**
     * 配置emailQueue
     *
     * @return
     */
    @Bean
    public Queue emailQueue() {
        return new Queue(FANOUT_EMAIL_QUEUE);
    }

    /**
     * 配置fanoutExchange
     *
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME);
    }

 
    // 绑定交换机 sms
    @Bean
    public Binding bindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(smsQueue).to(fanoutExchange);
    }


    // 绑定交换机 email
    @Bean
    public Binding bindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(emailQueue).to(fanoutExchange);
    }
}

6.3 配置文件

application.yml

bash 复制代码
spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: guest
    ####密码
    password: guest
    ### 地址
    virtual-host: booyatopVirtualHosts

6.4 生产者

java 复制代码
  import org.springframework.amqp.core.AmqpTemplate;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.bind.annotation.RestController;

  
 /**
 * @ClassName FanoutProducer
 * @Author   www.Boyatop.com
 * @Version V1.0
 **/
  @RestController
  public class FanoutProducer {

    @Autowired
    private AmqpTemplate amqpTemplate;

    /**
     * 发送消息
     *
     * @retur
     */
    @RequestMapping("/sendMsg")
    public String sendMsg(String msg) {
        /**
         * 1.交换机名称
         * 2.路由key名称
         * 3.发送内容
         */
        amqpTemplate.convertAndSend("/Boyatop_ex", "", msg);
        return "success";
    }
}

6.5 消费者

java 复制代码
  import lombok.extern.slf4j.Slf4j;
  import org.springframework.amqp.rabbit.annotation.RabbitHandler;
  import org.springframework.amqp.rabbit.annotation.RabbitListener;
  import org.springframework.stereotype.Component;

 /**
 * @ClassName FanoutEmailConsumer
 * @Author   www.Boyatop.com
 * @Version V1.0
 **/
@Slf4j
@Component
@RabbitListener(queues = "fanout_email_queue")
  public class FanoutEmailConsumer {

    @RabbitHandler
    public void process(String msg) {
        log.info(">>邮件消费者消息msg:{}<<", msg);
    }
}
java 复制代码
  import lombok.extern.slf4j.Slf4j;
  import org.springframework.amqp.rabbit.annotation.RabbitHandler;
  import org.springframework.amqp.rabbit.annotation.RabbitListener;
  import org.springframework.stereotype.Component;
 
/**
 * @ClassName fanout_sms_queue
 * @Author   www.Boyatop.com
 * @Version V1.0
 **/
@Slf4j
@Component
@RabbitListener(queues = "fanout_sms_queue")
  public class FanoutSmsConsumer {

    @RabbitHandler
    public void process(String msg) {
        log.info(">>短信消费者消息msg:{}<<", msg);
    }
}

7 生产者如何获取消费结果

  1. 根据业务来定

消费者消费成功结果:

能够在数据库中插入一条数据

2. Rocketmq 自带全局消息id ,能够根据该全局消息获取消费结果

原理:

生产者投递消息到mq 服务器,mq 服务器端在这时候返回一个全局的消息id

当我们消费者消费该消息成功之后,消费者会给我们mq服务器端发送通知标记该消息

消费成功。

生产者获取到该消息全局id,每隔2s 时间调用mq 服务器端接口查询该消息是否

有被消费成功。

  1. 异步返回一个全局id,前端使用ajax定时主动查询;
  2. 在rocketmq中,自带根据消息id查询是否消费成功

8 RabbitMQ死信队列

8.1 死信队列产生的背景

RabbitMQ死信队列俗称,备胎队列;消息中间件因为某种原因拒收该消息后,可以转移到死信队列中存放,死信队列也可以有交换机和路由key等。

8.2 产生死信队列的原因

  1. 消息投递到MQ中存放 消息已经过期 消费者没有及时的获取到我们消息,消息如果存放到mq服务器中过期之后,会转移到备胎死信队列存放。
  2. 队列达到最大的长度 (队列容器已经满了)
  3. 消费者消费多次消息失败,就会转移存放到死信队列中

代码整合 参考 Boyatop-springboot-rabbitmq|#中order-dead-letter-queue项目

8.3 死信队列的架构原理

死信队列和普通队列区别不是很大

普通与死信队列都有自己独立的交换机和路由key、队列和消费者。

区别:

1.生产者投递消息先投递到我们普通交换机中,普通交换机在将该消息投到

普通队列中缓存起来,普通队列对应有自己独立普通消费者。

2.如果生产者投递消息到普通队列中,普通队列发现该消息一直没有被消费者消费

的情况下,在这时候会将该消息转移到死信(备胎)交换机中,死信(备胎)交换机

对应有自己独立的 死信(备胎)队列 对应独立死信(备胎)消费者。

8.4 死信队列应用场景

30分钟订单超时设计

  1. Redis过期key :
  2. 死信延迟队列实现:

采用死信队列,创建一个普通队列没有对应的消费者消费消息,在30分钟过后

就会将该消息转移到死信备胎消费者实现消费。

备胎死信消费者会根据该订单号码查询是否已经支付过,如果没有支付的情况下

则会开始回滚库存操作。

9 RabbitMQ消息幂等问题

9.1 RabbitMQ消息自动重试机制

1.当我们消费者处理执行我们业务代码的时候,如果抛出异常的情况下

在这时候mq会自动触发重试机制,默认的情况下rabbitmq是无限次数的重试。需要人为指定重试次数限制问题

2.在什么情况下消费者需要实现重试策略?

A.消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试?

该情况下需要实现重试策略,网络延迟只是暂时调用不通,重试多次有可能会调用通。

B.消费者获取消息后,因为代码问题抛出数据异常,是否需要重试?

该情况下是不需要实现重试策略,就算重试多次,最终还是失败的。可以将日志存放起来,后期通过定时任务或者人工补偿形式。

如果是重试多次还是失败消息,需要重新发布消费者版本实现消费。可以使用死信队列

3 Mq在重试的过程中,有可能会引发消费者重复消费的问题。

Mq消费者需要解决 幂等性问题

幂等性 保证数据唯一

方式1:

生产者在投递消息的时候,生成一个全局唯一id,放在我们消息中。

Msg id=123456

Msg id=123456

Msg id=123456

消费者获取到我们该消息,可以根据该全局唯一id实现去重复。

全局唯一id 根据业务来定的 订单号码作为全局的id

实际上还是需要再db层面解决数据防重复。

业务逻辑是在做insert操作 使用唯一主键约束

业务逻辑是在做update操作 使用乐观锁

  1. 当消费者业务逻辑代码中,抛出异常自动实现重试 (默认是无数次重试)
  2. 应该对RabbitMQ重试次数实现限制,比如最多重试5次,每次间隔3s;重试多次还是失败的情况下,存放到死信队列或者存放到数据库表中记录后期人工补偿

9.2 如何合理选择消息重试

  1. 消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试 ?
  2. 消费者获取消息后,应该代码问题抛出数据异常,是否需要重试?

总结:如果消费者处理消息时,因为代码原因抛出异常是需要从新发布版本才能解决的,那么就不需要重试,重试也解决不了该问题的。存放到死信队列或者是数据库表记录、后期人工实现补偿。

9.3 Rabbitmq如何开启重试策略

java 复制代码
spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: guest
    ####密码
    password: guest
    ### 地址
    virtual-host: /booya_rabbitmq
    listener:
      simple:
        retry:
          ####开启消费者(程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔次数
          initial-interval: 3000

9.4 消费者重试过程中,如何避免幂等性问题

重试的过程中,为了避免业务逻辑重复执行,建议提前全局id提前查询,如果存在的情况下,就无需再继续做该流程。重试的次数最好有一定间隔次数,在数据库底层层面保证数据唯一性,比如加上唯一id。

9.5 SpringBoot开启消息确认机制

配置文件新增

java 复制代码
spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: guest
    ####密码
    password: guest
    ### 地址
    virtual-host: /booyaVirtualHosts
    listener:
      simple:
        retry:
          ####开启消费者(程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔次数
          initial-interval: 3000
        acknowledge-mode: manual
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver

消费者ack代码

java 复制代码
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")

  public class FanoutOrderConsumer {

    @Autowired
    private OrderManager orderManager;
    @Autowired
    private OrderMapper orderMapper;

  

    @RabbitHandler
    public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException {

  //        try {
        log.info(">>orderEntity:{}<<", orderEntity.toString());
        String orderId = orderEntity.getOrderId();
        if (StringUtils.isEmpty(orderId)) {
            log.error(">>orderId is null<<");
            return;
        }
        OrderEntity dbOrderEntity = orderMapper.getOrder(orderId);
        if (dbOrderEntity != null) {
           log.info(">>该订单已经被消费过,无需重复消费!<<");
            // 无需继续重试
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            return;
        }
        int result = orderManager.addOrder(orderEntity);

        log.info(">>插入数据库中数据成功<<");
        if (result >= 0) {
            // 开启消息确认机制      channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
       int i = 1 / 0;

      } catch (Exception e) {
            // 将失败的消息记录下来,后期采用人工补偿的形式

        }
    }
}
相关推荐
小刘爱喇石( ˝ᗢ̈˝ )1 小时前
行式数据库与列式数据库区别
数据库·分布式
MiniFlyZt2 小时前
消息队列MQ(RabbitMQ)
spring boot·分布式·微服务·rabbitmq
梦城忆2 小时前
常用的分布式 ID 设计方案
分布式
qxlxi2 小时前
【分布式】聊聊分布式id实现方案和生产经验
分布式·架构
小袁拒绝摆烂3 小时前
RabbitMQ从入门到实战-2
分布式·rabbitmq
对许4 小时前
Hadoop的运行模式
大数据·hadoop·分布式
穿越在未来4 小时前
【RabbitMQ】Producer之TTL过期时间 - 基于AMQP 0-9-1
分布式·消息队列·rabbitmq·消息中间件·rabbitmq ttl
小五Z4 小时前
RabbitMQ高级特性--消息确认机制
java·rabbitmq·intellij-idea
Hellc0074 小时前
使用 Docker 部署 RabbitMQ 并实现数据持久化
docker·rabbitmq·ruby