RabbitMQ 消息队列

什么是 RabbitMQ

MQ(Message Queue)消息队列

消息队列中间件,是分布式系统中的重要组件;
主要解决异步处理、应用解耦、流量削峰等问题,从而实现高性能,高可用,可伸缩和最终一致性的架构。

使用较多的消息队列产品:RabbitMQ,RocketMQ,ActiveMQ,ZeroMQ,Kafka 等。

异步处理

用户注册后,需要发送验证邮箱和手机验证码。

将注册信息写入数据库,发送验证邮件,发送手机,三个步骤全部完成后,返回给客户端。

传统:

客户端 <-> 注册信息写入数据库 -> 发送注册邮件 -> 发送注册短信

现在:

客户端 <-> 注册信息写入数据库 -> 写入消息队列 -> 异步 [发送注册邮件,发送注册短信]

应用解耦

场景:订单系统需要通知库存系统。

如果库存系统异常,则订单调用库存失败,导致下单失败。

原因:订单系统和库存系统耦合度太高。

传统:

用户 <-> 订单系统 - 调用库存接口 -> 库存系统

现在:

用户 <-> 订单系统 - 写入 -> 消息队列 <- 订阅 - 库存系统

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户,下单成功。

库存系统:订阅下单的消息,获取下单信息,库存系统根据下单信息,再进行库存操作。

假如:下单的时候,库存系统不能正常运行,也不会影响下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了,实现了订单系统和库存系统的应用解耦。

所以,消息队列是典型的“生产者-消费者“模型。

生产者不断的向消息队列中生产消息,消费者不断的从队列中获取消息。

因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的入侵,这样就实现了生产者和消费者的解耦。

流量削峰

抢购,秒杀等业务,针对高并发的场景。

因为流量过大,暴增会导致应用挂掉,为解决这个问题,在前端加入消息队列。

用户的请求,服务器接收后,首先写入消息队列,如果超过队列的长度,就抛弃,发送一个结束的页面;而请求成功的就是进入队列的用户。

背景知识介绍

AMQP 高级消息队列协议

Advanced Message Queuing Protocol 是一个提供统一消息服务的应用层标准高级消息队列协议。

协议:数据在传输的过程中必须要遵守的规则。

基于此协议的客户端可以与消息中间件传递消息。

并不受产品、开发语言等条件的限制。

JMS

Java Message Server 是 Java 消息服务应用程序接口,一种规范,和 JDBC 担任的角色类似。

JMS 是一个 Java 平台中关于面向消息中间件的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

二者的联系

JMS 是定义了统一接口,统一消息操作;AMQP 通过协议统一数据交互格式。

JMS 必须是 Java 语言;AMQP 只是协议,与语言无关。

Erlang 语言

Erlang 是一种通用的面向并发的编程语言,目的是创造一种可以应对大规模并发活动的编程语言和运行环境。

最初是专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合构建分布式,实时软并行计算系统。

Erlang 运行时环境是一个虚拟机,有点像 Java 的虚拟机,这样代码一经编译,同样可以随处运行。

为什么选择 RabbitMQ

  • RabbitMQ 由 Erlang 开发,AMQP 的最佳搭档,安装部署简单,上手门槛低。

  • 企业级消息队列,经过大量实践考验的高可靠,大量成功的应用案例,例如阿里、网易等一线大厂都有使用。

  • 有强大的 WEB 管理页面。

  • 强大的社区支持,为技术进步提供动力。

  • 支持消息持久化、支持消息确认机制、灵活的任务分发机制等,支持功能非常丰富。

  • 集群扩展很容易,并且可以通过增加节点实现成倍的性能提升。

总结:如果希望使用一个可靠性高、功能强大、易于管理的消息队列系统那么就选择 RabbitMQ;如果想用一个性能高,但偶尔丢点数据,可以使用 Kafka 或者 ZeroMQ。

Kafka 和 ZeroMQ 的性能比 RabbitMQ 好很多。

RabbitMQ 各组件功能

RabbitMQ 消息队列_第1张图片

RabbitMQ 组件

1. Broker - 消息队列服务器实体。

2. Virtual Host - 虚拟主机:

  • 标识一批交换机、消息队列和相关对象,形成的整体。
  • 虚拟主机是共享相同的身份认证和加密环境的独立服务器域。
  • 每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。
  • VHost 是 AMQP 概念的基础,RabbitMQ 默认的 vhost 是 /,必须在链接时指定。

3. Exchange - 交换器(路由):用来接收生产者发送的消息并将这些消息通过路由发给服务器中的队列。

4. Banding - 绑定。用于交换机和消息队列之间的关联

5. Queue - 消息队列:

  • 用来保存消息直到发送给消费者。
  • 它是消息的容器,也是消息的终点。
  • 一个消息可投入一个或多个队列。
  • 消息一直在队列里面,等待消费者连接到这个队列将其取走。

6. Channel - 通道(信道):

  • 多路复用连接中的一条独立的双向数据流通道。
  • 信道是建立在真实的 TCP 连接内的虚拟链接。
  • AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,都是通过信道完成的。
  • 因为对于操作系统来说,建立和销毁 TCP 连接都是非常昂贵的开销,所以引入了信道的概 念,用来复用 TCP 连接。

7. Connection - 网络连接,比如一个 TCP 连接。

8. Publisher - 消息的生产者,也是一个向交换器发布消息的客户端应用程序。

9. Consumer - 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

10. Message - 消息:

  • 消息是不具名的,它是由消息头和消息体组成。
  • 消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(优先级)、delivery-mode(消息可能需要持久性存储[消息的路由模式])等。

使用 RabbitMQ

想要安装 RabbitMQ,必须先安装 erlang 语言环境;类似安装 tomcat,必须先安装 JDK。
查看匹配的版本:https://www.rabbitmq.com/which-erla.html

RabbitMQ 安装启动

Erlang 下载:https://dl.bintray.com/rabbitmq-erlang/rpm/erlang

Socat 下载:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm

RabbitMQ 下载:https://www.rabbitmq.com/install-rpm.html#downloads

安装

启动 Linux 系统(192.168.186.128),传输相关的三个 rpm 到 /opt 目录下,然后在 /opt 目录下按顺序执行安装命令:

rpm -ivh erlang-21.3.8.16-1.el7.x86_64.rpm
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
rpm -ivh rabbitmq-server-3.8.6-1.el7.noarch.rpm

启动后台管理插件

rabbitmq-plugins enable rabbitmq_management

启动 RabbitMQ

systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
systemctl restart rabbitmq-server.service
systemctl stop rabbitmq-server.service

查看进程

ps -ef | grep rabbitmq

测试

  1. 关闭防火墙
systemctl stop firewalld

(或者防火墙开放对应的端口号)

firewall-cmd --zone=public --add-port=15672/tcp --permanent
firewall-cmd --zone=public --add-port=5671/tcp --permanent
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=25672/tcp --permanent
firewall-cmd --reload
  1. 浏览器输入:http://ip:15672(比如这里输入:http://192.168.186.128:15672)
  2. 默认帐号和密码是 guest,而 guest 用户默认不允许远程连接

创建账号:

rabbitmqctl add_user zm 123456

设置用户角色:

rabbitmqctl set_user_tags zm administrator

设置用户权限:

rabbitmqctl set_permissions -p "/" zm ".*" ".*" ".*"

查看当前用户和角色:

rabbitmqctl list_users

修改用户密码:

rabbitmqctl change_password zm NewPassword

管理界面介绍:

  • Overview - 概览
  • Connections - 查看链接情况
  • Channels - 信道(通道)情况
  • Exchanges - 交换机(路由)情况,默认4类7个
  • Queues - 消息队列情况
  • Admin - 管理员列表
  • RabbitMQ 提供给编程语言客户端链接的端口 - 5672;RabbitMQ 管理界面的端口 - 15672;RabbitMQ 集群的端口 - 25672。

RabbitMQ 快速操作

依赖



    UTF-8
    UTF-8
    1.11
    11
    11



    
        com.rabbitmq
        amqp-client
        5.7.3
    
    
        org.slf4j
        slf4j-log4j12
        1.7.25
        compile
    
    
        org.apache.commons
        commons-lang3
        3.9
    

日志依赖 log4j(可选项)

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rebbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
log4j.rootLogger=debug, stdout,file

创建连接

先在 RabbitMQ 管理界面 Admin -> Virtual Hosts -> Add a new virtual host 创建虚拟主机 (Name: /zm, Description: zm, Tags: administrator);

然后编写连接的代码:

public class ConnectionUtil {

    public static Connection getConnection() throws  Exception{
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.在工厂对象中设置 MQ 的连接信息(ip, port, vhost, username, password)
        factory.setHost("192.168.186.128");
        factory.setPort(5672);
        factory.setVirtualHost("/zm");
        factory.setUsername("zm");
        factory.setPassword("123456");
        // 3.通过工厂获得与 MQ 的连接
        return factory.newConnection();
    }


    public static void main(String[] args) throws Exception {
        Connection connection = getConnection();
        System.out.println("Connection: " + connection);
        connection.close();
    }

}

RabbitMQ 模式

RabbitMQ 提供了 6 种消息模型,但是第 6 种其实是 RPC,并不是 MQ。
在线手册:https://www.rabbitmq.com/getstarted.html

5 种消息模型,大体分为两类:

  • 1 和 2 属于点对点。
  • 3、4、5 属于发布订阅模式(一对多)。

点对点模式 - P2P(Point to Point)模式:

  • 包含三个角色:消息队列 queue,发送者 sender,接收者 receiver。
  • 每个消息发送到一个特定的队列中,接收者从中获得消息。
  • 队列中保留这些消息,直到他们被消费或超时。
  • 如果希望发送的每个消息都会被成功处理,那需要 P2P。

特点:

  1. 每个消息只有一个消费者,一旦消费,消息就不在队列中了。
  2. 发送者和接收者之间没有依赖性,发送者发送完成,不管接收者是否运行,都不会影响消息发送到队列中。
  3. 接收者成功接收消息之后需向对象应答成功(确认)。

发布订阅模式 - publish / subscribe 模式:

  • Pub / Sub 模式包含三个角色:交换机 exchange,发布者 publisher,订阅者 subcriber。
  • 多个发布者将消息发送交换机,系统将这些消息传递给多个订阅者。
  • 如果希望发送的消息被多个消费者处理,可采用 Pub / Sub。

特点:

  1. 每个消息可以有多个订阅者。
  2. 发布者和订阅者之间在时间上有依赖,对于某个交换机的订阅者,必须创建一个订阅后,才能消费发布者的消息。
  3. 为了消费消息,订阅者必须保持运行状态。

简单模式(Hello World!)


RabbitMQ 本身只是接收,存储和转发消息,并不会对信息进行处理;类似邮局,处理信件的应该是收件人而不是邮局。

生产者 P

public class Sender {

    public static void main(String[] args) throws Exception {
        String msg = "Hello, 你好 zm";

        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.在连接中创建通道(信道)
        Channel channel = connection.createChannel();
        // 3.创建消息队列 (1,2,3,4,5)
        /*
            参数 1: 队列的名称
            参数 2: 队列中的数据是否持久化
            参数 3: 是否排外(是否支持扩展,当前队列只能自己用,不能给别人用)
            参数 4: 是否自动删除(当队列的连接数为 0 时,队列会销毁,不管队列是否还存保存数据)
            参数 5: 队列参数(没有参数为 null)
         */
        channel.queueDeclare("queue1", false, false, false, null);
        // 4.向指定的队列发送消息 (1,2,3,4)
        /*
            参数 1: 交换机名称,当前是简单模式,也就是 P2P 模式,没有交换机,所以名称为 ""
            参数 2: 目标队列的名称
            参数 3: 设置消息的属性(没有属性则为 null)
            参数 4: 消息的内容 (只接收字节数组)
         */
        channel.basicPublish("", "queue1", null, msg.getBytes());
        System.out.println("发送:" + msg);
        // 5.释放资源
        channel.close();
        connection.close();
    }

}

启动生产者,即可前往管理端查看队列中的信息,会有一条信息没有处理。

消费者 C

public class Receiver {

    public static void main(String[] args) throws Exception {
        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.获得通道(信道)
        Channel channel = connection.createChannel();

        // 3.从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 就是从队列中获取的消息
                String s = new String(body);
                System.out.println("获取消息为:" + s);
            }
        };
        // 4.监听队列 true: 自动消息确认
        channel.basicConsume("queue1", true, consumer);
    }

}

启动消费者,前往管理端查看队列中的信息,所有信息都已经处理和确认,显示 0。

消息确认机制 ACK
通过刚才的案例可以看出,消息一旦被消费,消息就会立刻从队列中移除。

如果消费者接收消息后,还没执行操作就抛异常宕机导致消费失败,但是 RabbitMQ 无从得知,这样消息就丢失了。

因此,RabbitMQ 有一个 ACK 机制,当消费者获取消息后,会向 RabbitMQ 发送回执 ACK,告知消息已经被接收。

ACK - Acknowledge character 即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符,表示发来的数据已确认接收无误。在使用 http 请求时,http 的状态码 200 就是表示服务器执行成功。

整个过程就像快递员将包裹送到你手里,并且需要你的签字,并拍照回执。

不过这种回执 ACK 分为两种情况:

  • 自动 ACK - 消息接收后,消费者立刻自动发送 ACK,类似快递放在快递柜。
  • 手动 ACK - 消息接收后,不会发送 ACK,需要手动调用,类似快递必须本人签收。

两种情况如何选择,需要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,自动 ACK 会比较方便。
  • 如果消息非常重要,最好消费完成手动 ACK;因为如果自动 ACK 消费后,RabbitMQ 就会把消息从队列中删除,而此时消费者抛异常宕机,那么消息就永久丢失了。

修改启动手动 ACK 消息确认:

// 监听队列 false: 手动消息确认
channel.basicConsume("queue1", false, consumer);

启动生产者和消费者,前往管理端查看队列中的信息,会有一条信息没有确认(Unacked)。

手动 ACK消息确认解决问题:

public class ReceiverAck {

    public static void main(String[] args) throws Exception {
        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.获得通道(信道)
        final Channel channel = connection.createChannel();

        // 3.从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body就是从队列中获取的消息
                String s = new String(body);
                System.out.println("获取消息为:" + s);
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 4.监听队列 false: 手动消息确认
        channel.basicConsume("queue1", false, consumer);
    }

}

工作队列模式(Work queues)

简单模式,一个消费者来处理消息,如果生产者生产消息过快过多,而消费者的能力有限,就会产生消息在队列中堆积(生活中的滞销)。

当运行许多消费者程序时,消息队列中的任务会被众多消费者共享,但其中某一个消息只会被一个消费者获取(100 支肉串 20 个人吃,但是其中的某支肉串只能被一个人吃)。

生产者 P

public class Sender {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("test_work_queue",false,false,false,null);

        for(int i = 1;i<=100;i++) {
            String msg = "Message --> " + i;
            channel.basicPublish("", "test_work_queue", null, msg.getBytes());
            System.out.println(msg);
        }

        channel.close();
        connection.close();
    }

}

消费者 1

public class Receiver1 {

    // 统计获取的信息的数量
    static int counter = 1;

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        // queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
        channel.queueDeclare("test_work_queue", false, false, false, null);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Receiver 1: " + s + ". Total Message Count: " + counter++);
                // 模拟网络延迟 200 毫秒
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("test_work_queue", false, consumer);
    }

}

消费者 2

public class Receiver2 {

    // 统计获取的信息的数量
    static int counter = 1;

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        // queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
        channel.queueDeclare("test_work_queue", false, false, false, null);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Receiver 2: " + s + ". Total Message Count: " + counter++);
                // 模拟网络延迟 900 毫秒
                try {
                    Thread.sleep(900);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("test_work_queue", false, consumer);
    }

}

能者多劳
先运行 2 个消费者,排队等候消费(取餐),再运行生产者开始生产消息(烤肉串)。

由运行结果可以看到,虽然两个消费者的消费速度不一致(线程休眠时间),但是消费的数量却是一致的,各消费 50 个消息。

  • 例如:工作中,A 编码速率高,B 编码速率低,两个人同时开发一个项目,A 10 天完成,B 30 天完成,A 完成自己的编码部分,就无所事事了,等着 B 完成就可以了,这样是不可以的,应该遵循“能者多劳”。
  • 效率高的多干点,效率低的少干点。

为了克服这个问题,可以使用设置为 prefetchCount = 1 的 basicQos 方法。这告诉RabbitMQ 一次不要给一个 worker 发送一条以上的消息。或者,换句话说,在 worker 处理并确认前一个消息之前,不要向它发送新消息。相反,它将把它分派到下一个不繁忙的 worker。

在消费者 1 和消费者 2 中加上 channel.basicQos(1):

...
// queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
channel.queueDeclare("test_work_queue", false, false, false, null);
// 开启一次接受一条消息。可以理解为:快递一个一个送,送完一个再送下一个,速度快的送件就多
channel.basicQos(1);
...

能者多劳必须要配合手动的 ACK 机制才生效。

如何避免消息堆积?

  • Workqueue,多个消费者监听同一个队列。
  • 接收到消息后,通过线程池,异步消费。

发布/订阅模式(Publish/Subscribe)

工作队列背后的假设是,每个任务都被准确地交付给一个工作者;“发布/订阅”模式将一个消息传递给多个消费者。

生活中的案例:众多粉丝关注一个视频主,视频主发布视频,所有粉丝都可以得到视频通知。

  • 生产者 P 发送信息给路由 X,路由 X 将信息转发给绑定路由 X 的队列;队列将信息通过信道发送给消费者,最后消费者进行消费。整个过程,必须先创建路由。

  • 路由在生产者程序中创建。

  • 路由没有存储消息的能力,当生产者将信息发送给路由后,消费者还没有运行,所以没有队列,路由并不知道将信息发送给谁。

  • 运行程序的顺序:

  1. 执行一次 MessageSender,声明了路由。
  2. 执行 MessageReceiver1 和 MessageReceiver2,绑定到路由。
  3. 再次执行 MessageSender,发送消息给路由。

生产者

public class Sender {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由(路由名,路由类型)
        // fanout:不处理路由键(只需要将队列绑定到路由上,发送到路由的消息都会被转发到与该路由绑定的所有队列上)
        channel.exchangeDeclare("test_exchange_fanout", "fanout");

        String msg = "Hello,world";
        channel.basicPublish("test_exchange_fanout", "", null, msg.getBytes());
        System.out.println("Publisher:" + msg);

        channel.close();
        connection.close();
    }

}

消费者 1

public class Receiver1 {

    private static final String RECEIVER_QUEUE = "test_exchange_fanout_queue_1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(关注)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_fanout", "");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Subscriber 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

消费者 2

public class Receiver2 {

    private static final String RECEIVER_QUEUE = "test_exchange_fanout_queue_2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(关注)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_fanout", "");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Subscriber 2: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

路由模式(Routing)

路由会根据类型进行定向(direct)分发消息给不同的队列;每种类型可以对应多个消费者。

运行程序的顺序:

  • 先运行一次 Sender(创建路由器)。
  • 有了路由器之后,在创建两个 Receiver1 和 Receiver2,进行队列绑定。
  • 再次运行 Sender,发出消息。

生产者

public class Sender {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由 (路由名,路由类型)
        // direct:根据路由键进行定向分发消息
        channel.exchangeDeclare("test_exchange_direct", "direct");

        String msg = "Register New User: userid=S101";
        channel.basicPublish("test_exchange_direct", "insert", null, msg.getBytes());
        System.out.println(msg);

        channel.close();
        connection.close();
    }

}

消费者 1

public class Receiver1 {

    private static final String RECEIVER_QUEUE = "test_exchange_direct_queue_1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);

        // 绑定路由(如果路由键的类型是 添加,删除,修改 的话,绑定到这个队列 1 上)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "insert");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "update");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "delete");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Cosumer 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

消费者 2

public class Receiver2 {

    private static final String RECEIVER_QUEUE = "test_exchange_direct_queue_2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);

        // 绑定路由(如果路由键的类型是 查询 的话,绑定到这个队列 2 上)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "select");
       
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Cosumer 2: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

通配符模式(Topics)

通配符模式是和路由模式差不多,唯独的区别就是路由键支持模糊匹配。

匹配符号:

* :只能匹配一个词(正好一个词,多一个不行,少一个也不行)。
# : 匹配 0 个或更多个词。

案例:

Q1 绑定了路由键 `*.orange.*`      
Q2 绑定了路由键 `*.*.rabbit` 和 `lazy.#`

quick.orange.rabbit         # Q1    Q2
lazy.orange.elephant        # Q1    Q2
quick.orange.fox            # Q1
lazy.brown.fox              # Q2
lazy.pink.rabbit            # Q2
quick.brown.fox             # 无
orange                      # 无
quick.orange.male.rabbit    # 无

生产者

public class Sender {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由 (路由名,路由类型)
        // topic:模糊匹配的定向分发
        channel.exchangeDeclare("test_exchange_topic", "topic");

        String msg = "price-off promotion";
        channel.basicPublish("test_exchange_topic", "product.price", null, msg.getBytes());
        System.out.println("Provider: " + msg);

        channel.close();
        connection.close();
    }

}

消费者 1

public class Receiver1 {

    private static final String RECEIVER_QUEUE = "test_exchange_topic_queue_1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(绑定用户相关的消息)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "user.#");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Consumer 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

消费者 2

public class Receiver2 {

    private static final String RECEIVER_QUEUE = "test_exchange_topic_queue_2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(绑定商品和订单相关的消息)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "product.#");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "order.#");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Consumer 2: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

持久化

  • 消息的可靠性是 RabbitMQ 的一大特色,那么 RabbitMQ 是如何避免消息丢失?

    1. 消费者的 ACK 确认机制,可以防止消费者丢失消息。

    2. 万一在消费者消费之前,RabbitMQ 服务器宕机了,那消息也会丢失。

  • 想要将消息持久化,那么路由和队列都要持久化才可以。

生产者

public class Sender {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由 (路由名,路由类型,持久化)
        // topic:模糊匹配的定向分发
        channel.exchangeDeclare("test_exchange_topic", "topic", true);

        String msg = "price-off promotion";
        // 信道持久化
        channel.basicPublish("test_exchange_topic", "product.price", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
        System.out.println("Provider: " + msg);

        channel.close();
        connection.close();
    }

}

消费者

public class Receiver1 {

    private static final String RECEIVER_QUEUE = "test_exchange_topic_queue_1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列 (第二个参数为 true:支持持久化)
        channel.queueDeclare(RECEIVER_QUEUE, true, false, false, null);
        // 绑定路由(绑定用户相关的消息)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "user.#");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Consumer 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

Spring 整合 RabbitMQ

五种消息模型,在企业中应用最广泛的就是定向匹配 topics

Spring AMQP 是基于 Spring 框架的 AMQP 消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO 的消息监听等,简化了对于 RabbitMQ 相关程序的开发。

生产端工程

  • 依赖 pom.xml

    
        org.springframework.amqp
        spring-rabbit
        2.0.1.RELEASE
    
    
        org.slf4j
        slf4j-log4j12
        1.7.25
        compile
    
    
        org.apache.commons
        commons-lang3
        3.9
    

  • spring-rabbitmq-producer.xml



    
    

    
    

    
    

    
    
        
            
            
        
    

    
    

    
    


  • 发消息 com.zm.test.Sender:
public class Sender {

    public static void main(String[] args) {
        // 1.创建 spring 容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");

        // 2.从 spring 容器中获得 rabbit 模版对象
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);

        // 3.发消息
        Map map = new HashMap();
        map.put("name", "张三");
        map.put("email", "[email protected]");
        rabbitTemplate.convertAndSend("msg.user", map);
        System.out.println("Message Sent...");

        context.close();
    }

}

消费端工程

  • 依赖与生产者一致
  • spring-rabbitmq-consumer.xml



    
    

    
    

    
    

    
    

    
    
        
    


  • 消费者:

MessageListener 接口用于 spring 容器接收到消息后处理消息;

如果需要使用自己定义的类型来实现处理消息时,必须实现该接口,并重写 onMessage() 方法;

当 spring 容器接收消息后,会自动交由 onMessage 进行处理。

com.zm.listener.ConsumerListener:

@Component
public class ConsumerListener implements MessageListener {

    /**
     * jackson 提供序列化和反序列中使用最多的类,用来转换 json 的
     */
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message) {
        // 将 message对象转换成 json
        JsonNode jsonNode = null;
        try {
            jsonNode = MAPPER.readTree(message.getBody());
            String name = jsonNode.get("name").asText();
            String email = jsonNode.get("email").asText();
            System.out.println("Message From Queue:{" + name + ", " + email + "}");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
  • 启动项目 com.zm.test.TestRunner:
public class TestRunner {

    public static void main(String[] args) throws IOException {
        // 获得容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-consumer.xml");
        // 让程序一直运行,别终止
        System.in.read();
    }

}

消息成功确认机制

在实际场景下,有的生产者发送的消息是必须保证成功发送到消息队列中,需要 事务机制 和 发布确认机制

事务机制

AMQP 协议提供的一种保证消息成功投递的方式,通过信道开启 transactional 模式;

利用信道的三个方法来实现以事务方式发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递

  • channel.txSelect() - 开启事务
  • channel.txCommit() - 提交事务
  • channel.txRollback() - 回滚事务

Spring 已经对上面三个方法进行了封装,所以这里使用原始的代码演示。

生产者

public class Sender {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare("test_transaction", "topic");
        // 开启事务
        channel.txSelect();
        try {
            channel.basicPublish("test_transaction", "product.price", null, "Item 1: price-off".getBytes());
            // 模拟出错
            // System.out.println(1 / 0);
            channel.basicPublish("test_transaction", "product.price", null, "Item 2: price-off".getBytes());
            // 提交事务(一起成功)
            channel.txCommit();
            System.out.println("Producer: All Messages Sent");
        } catch (Exception e) {
            System.out.println("All Messages Rollback");
            // 事务回滚(一起失败)
            channel.txRollback();
            e.printStackTrace();
        } finally {
            channel.close();
            connection.close();
        }
    }

}

消费者

public class Receiver {

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("test_transaction_queue", false, false, false, null);
        channel.queueBind("test_transaction_queue", "test_transaction", "product.#");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String s = new String(body);
                System.out.println("Consumer: " + s);
            }
        };

        // 4.监听队列 true:自动消息确认
        channel.basicConsume("test_transaction_queue", true, consumer);
    }

}

Confirm 发布确认机制

RabbitMQ 为了保证消息的成功投递,采用通过 AMQP 协议层面提供事务机制的方案,但是采用事务会大大降低消息的吞吐量。

开启事务性能最大损失超过 250 倍。

事务效率低下原因:100 条消息,前 99 条成功,如果第 100 条失败,那么 99 条消息要全部撤销回滚。

更加高效的解决方式是采用 Confirm 模式,而 Confirm 模式则采用补发第 100 条的措施来完成 100 条消息的送达。

在 Spring 中应用

  • resources\spring\spring-rabbitmq-producer.xml
...





...
  • 消息确认处理类 com.zm.confirm.MessageConfirm:
public class MessageConfirm implements RabbitTemplate.ConfirmCallback {

    /**
     * @param correlationData 消息相关的数据对象(封装了消息的唯一 id)
     * @param b               消息是否确认成功
     * @param s               异常信息
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        if (b) {
            System.out.println("Successfully Confirmed Message");
        } else {
            System.out.println("Fail to Confirm Message, error: " + s);
            // 如果本条消息一定要发送到队列中,例如下订单消息,可以采用补发
            // 1.采用递归(限制递归的次数)
            // 2.redis + 定时任务(jdk 的 timer,或者定时任务框架 Quartz)
        }
    }
}
  • resources\log4j.properties
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rabbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l  %m%n

log4j.rootLogger=debug, stdout,file
  • 发送消息 com.zm.test.Sender:
...
// 3.发消息
Map map = new HashMap();
map.put("name", "张三");
map.put("email", "[email protected]");
// 模拟发送消息失败
// rabbitTemplate.convertAndSend("fuck", "msg.user", map);
rabbitTemplate.convertAndSend("msg.user", map);
System.out.println("Message Sent...");
...

消费端限流

RabbitMQ 服务器积压了成千上万条未处理的消息,然后随便打开一个消费者客户端,就会出现这样的情况:巨量的消息瞬间全部喷涌推送过来,但是单个客户端无法同时处理这么多数据,就会被压垮崩溃。

所以,当数据量特别大的时候,对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,这是用户的行为 - 是无法约束的。

应该对消费端限流,用于保持消费端的稳定。

RabbitMQ 提供了一种 QoS(Quality of Service,服务质量)服务质量保证功能;

即在非自动确认消息的前提下,如果一定数目的消息未被确认前,不再进行消费新的消息。

生产者 com.zm.test.Sender 使用循环发出多条消息:

...
for (int i = 0; i < 10; i++) {
    rabbitTemplate.convertAndSend("msg.user", map);
    System.out.println("Message Sent...");
}
...

RabbitMQ 的管理页面可以看到生产了 10 条堆积未处理的消息。

消费者进行限流处理:

resources\spring\spring-rabbitmq-consumer.xml

...
5.配置监听 -->



    

...

com.zm.listener.ConsumerListener

@Component
public class ConsumerListener extends AbstractAdaptableMessageListener {

    /**
     * jackson 提供序列化和反序列中使用最多的类,用来转换 json 的
     */
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        // 将 message对象转换成 json
//        JsonNode jsonNode = MAPPER.readTree(message.getBody());
//        String name = jsonNode.get("name").asText();
//        String email = jsonNode.get("email").asText();
//        System.out.println("Message From Queue:{" + name + ", " + email + "}");

        String str = new String(message.getBody());
        System.out.println("str = " + str);

        /**
         * 手动确认消息(参数1,参数2)
         * 参数 1:RabbitMQ 想该 channel 投递的这条消息的唯一标识 ID,此 ID 是一个单调递增的正整数。
         * 参数 2:为了减少网络流量,手动确认可以被批量处理;当该参数为 true 时,则可以一次性确认小于等于 msgId 值的所有消息。
         */
        long msgId = message.getMessageProperties().getDeliveryTag();
        channel.basicAck(msgId, true);

        Thread.sleep(3000);
        System.out.println("Rest for 3 seconds and then continue for more messages...");
    }
}

每次最多只确认接收 3 条消息,直到消息队列为空。

过期时间 TTL

Time To Live - 生存时间、还能活多久,单位毫秒。

在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为 dead message 并投入到死信队列,无法消费该消息)。

RabbitMQ 可以对消息和队列设置 TTL:

  • 通过队列设置,队列中所有消息都有相同的过期时间。
  • 对消息单独设置,每条消息的 TTL 可以不同(更颗粒化)。

设置队列 TTL

RabbitMQ 管理端删除掉 test_spring_queue_1 队列。

resources\spring\spring-rabbitmq-producer.xml



    
        
    

5 秒之后,消息自动删除。

设置消息 TTL

RabbitMQ 管理端删除掉 test_spring_queue_1 队列。

设置某条消息的 TTL,只需要在创建发送消息时指定即可。

resources\spring\spring-rabbitmq-producer.xml


com.zm.test.Sender2

public class Sender2 {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);

        // 创建消息配置对象
        MessageProperties messageProperties = new MessageProperties();
        // 设置消息过期时间
        messageProperties.setExpiration("6000");
        // 创建消息
        Message message = new Message("This Message will be deleted in 6000 ms".getBytes(), messageProperties);
        // 发消息
        rabbitTemplate.convertAndSend("msg.user", message);
        System.out.println("Message Sent...");

        context.close();
    }

}

如果同时设置了 queue 和 message 的 TTL 值,则只有二者中较小的才会起作用。

死信队列

DLX(Dead Letter Exchanges)死信交换机 / 死信邮箱,当消息在队列中由于某些原因没有被及时消费而变成死信(dead message)后,这些消息就会被分发到 DLX 交换机中,而绑定 DLX 交换机的队列,称之为:“死信队列”。
消息没有被及时消费的原因:

  • 消息被拒绝(basic.reject / basic.nack)并且不再重新投递 requeue=false。
  • 消息超时未消费。
  • 达到最大队列长度。

RabbitMQ 消息队列_第2张图片

流程图

resources\spring\spring-rabbitmq-producer-dlx.xml




    
    

    
    

    
    

    
    

    
    
        
            
            
        
    

    
    
        
            
            
            
            
        
    

    
    
        
            
            
            
            
        
    

    
    
        
            
            
        
    


发消息进行测试

public class SendDLX {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer-dlx.xml");
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        
        // 测试超时
        // rabbitTemplate.convertAndSend("dlx_ttl", "Overtime: Close".getBytes());

        // 测试超过最大长度
        rabbitTemplate.convertAndSend("dlx_max", "OverSize: 1".getBytes());
        rabbitTemplate.convertAndSend("dlx_max", "OverSize: 2".getBytes());
        rabbitTemplate.convertAndSend("dlx_max", "OverSize: 3".getBytes());

        System.out.println("Message Sent...");

        context.close();
    }

}

延迟队列

延迟队列 = TTL + 死信队列的合体。

死信队列只是一种特殊的队列,里面的消息仍然可以消费。

在电商开发部分中,都会涉及到延时关闭订单,此时延迟队列正好可以解决这个问题。

生产者

沿用上面死信队列案例的超时测试,超时时间改为订单关闭时间即可。

消费者

resources\spring\spring-rabbitmq-consumer.xml

...


    

...

你可能感兴趣的:(分布式技术,rabbitmq,中间件,分布式)