消息队列 ~ RabbitMQ ~ 从入门到入坑。

消息队列 ~ RabbitMQ ~ 从入门到入坑。


文章目录


why ~ RabbitMQ?

常见。

  • ActiveMQ。

ActiveMQ 是 Apache 出品,最流行的,能力强劲的开源消息总线。ta 是一个完全支持 JMS 规范的的消息中间件。丰富的 API,多种集群架构模式让 ActiveMQ 在业界成为老牌的消息中间件,在中小型企业颇受欢迎。

  • Kafka。

Kafka 是 LinkedIn 开源的分布式发布一订阅消息系统,目前归属于 Apache 顶级项目。Kafka 主要特点是基于 Pull 的模式来处理消息消费,
追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8 版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。

  • RocketMQ。

RocketMQ 是阿里开源的消息中间件,它是纯 Java 开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ 思路起
源于 Kafka,但并不是 Kafka 的—个 Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消
息推送、日志流式处理、binglog 分发等场景。

  • RabbitMQ。
    RabbitMQ 是使用 Erlang 语言开发的开源消息队列系统,基于AMQP 协议来实现。AMQP 的主要特征是面向消息、队列、路由(包括点对点和
    发布/ 订阅)、可靠性、安全。QP 协议更用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。


RabbitMQ 中的相关概念。

  • Broke。
    接收和分发消息的应用。RabbitMQ Server 就是 Message Broker。

  • Virtual Host。
    出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ Server 提供的服务时可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange / queue 等。

  • Connection。
    publisher / consumer 和 broker 之间的 TCP 连接。

  • ChanneI。
    如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 Connection 内部建立的逻辑连接。如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销。

  • Exchange。
    message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct(point-to-point),topic(publish-subscribe)and fanout(multicast)。

  • Queue。
    消息最终被送到这里等待 consumer 取走。

  • Binding。
    exchange 和 queue 之间的虚拟连接。binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据。



JMS。

JMS 即 Java 消息服务(Java Message Service)应用程序接囗,是一个Java 平台中关于面向消息中间件的 API。

JMS 是 JavaEE 规范中的一种,类比 JDBC。

很多消息中间件都实现了 JMS 规范,eg. ActiveMQ。RabbitMQ 官方没有提供 JMS 的实现包,但是开源社区有。



安装。

RabbitMQ 基于 AMQP 协议,erlang 语言开发。

  • 官网:https://www.rabbitmq.com/

  • 下载 RabbitMQ 3.7.18(RPM 包)。

  • 所需依赖:erlang(因为 RabbitMQ 是 erlang 编写的,所以需要先安装 erlang)- socat。



安装 erlang。

查询 RabbitMQ 对应 erlang 版本。

https://www.rabbitmq.com/which-erlang.html

使用 YUM。

配置 YUM 源。

配置方法参考:CentOS 配置 Yum ~ 附多种问题解决方案。

repo 文件参考:https://github.com/rabbitmq/erlang-rpm

[root@localhost ~]# cd /etc/yum.repos.d/
[root@localhost yum.repos.d]# vim /etc/yum.repos.d/rabbitmq-erlang.repo

To use Erlang 22.x on CentOS 7:

# In /etc/yum.repos.d/rabbitmq_erlang.repo
[rabbitmq_erlang]
name=rabbitmq_erlang
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/$basearch
repo_gpgcheck=1
gpgcheck=1
enabled=1
# PackageCloud's repository key and RabbitMQ package signing key
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
       https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300

[rabbitmq_erlang-source]
name=rabbitmq_erlang-source
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
# PackageCloud's repository key and RabbitMQ package signing key
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
       https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300

yum install erlang

此时有提示:缺少依赖:socat。

[root@localhost tools_my]# rpm -ivh rabbitmq-server-3.7.18-1.el7.noarch.rpm
error: Failed dependencies:
socat is needed by rabbitmq-server-3.7.18-1.el7.noarch

rabbitmq-server 依赖 socat。

安装 socat。

yum install socat

再次安装 erlang。

安装 rabbitmq-server。

依赖安装完成,可以安装 rabbitmq-server 了。

[root@localhost tools_my]# rpm -ivh rabbitmq-server-3.7.18-1.el7.noarch.rpm

至此,安装完成。



配置。

rabbitmq 默认从 /etc/rabbitmq/ 中读取配置文件。

[root@localhost ~]# ls /etc/rabbitmq/
[root@localhost ~]# 

但发现初始情况目录下并没有配置文件。

RabbitMQ 在安装目录下提供了配置文件模板 rabbitmq.config.example

查找一下 ta 在哪里。

[root@localhost ~]# find / -name rabbitmq.config.example
/usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example

将 ta 复制到指定目录下。并修改文件名,注意文件名不要 .example

[root@localhost ~]# cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config

修改配置文件。配置管理用户。

{loopback_users, []} 注释放开。(逗号也要去掉)。

 55    %% The default "guest" user is only permitted to access the server
 56    %% via a loopback interface (e.g. localhost).
 57    %% {loopback_users, [<<"guest">>]},
 58    %%
 59    %% Uncomment the following line if you want to allow access to the
 60    %% guest user from anywhere on the network.
 61    %% {loopback_users, []},

{loopback_users, []}


使用。

启用插件。
[root@localhost ~]# rabbitmq-plugins enable rabbitmq_management
Enabling plugins on node rabbit@localhost:
rabbitmq_management
The following plugins have been configured:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@localhost...
The following plugins have been enabled:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch

set 3 plugins.
Offline change; changes will take effect at broker restart.

开启服务。
[root@localhost ~]# rabbitmq-server 

  ##  ##
  ##  ##      RabbitMQ 3.7.18. Copyright (C) 2007-2019 Pivotal Software, Inc.
  ##########  Licensed under the MPL.  See https://www.rabbitmq.com/
  ######  ##
  ##########  Logs: /var/log/rabbitmq/rabbit@localhost.log
                    /var/log/rabbitmq/rabbit@localhost_upgrade.log

              Starting broker...
 completed with 3 plugins.





RabbitMQ 服务管理。

rabbitmqctl

rabbitmqctl 是 RabbitMQ 自己的服务管理命令。Shell 中输入 rabbitmqctl 可以查看 usage。

Linux 系统的服务管理命令也可以管理 RabbitMQ。

[root@localhost ~]# systemctl stop rabbitmq-server
[root@localhost ~]# systemctl start rabbitmq-server
[root@localhost ~]# systemctl restart rabbitmq-server

[geek@192 tools_my]$ rabbitmqctl
Error:

Usage

rabbitmqctl [--node <node>] [--timeout <timeout>] [--longnames] [--quiet] <command> [<command options>]

Available commands:

Help:

   help                          Displays usage information for a command
   version                       Displays CLI tools version

Nodes:

   await_startup                 Waits for the RabbitMQ application to start on the target node
   reset                         Instructs a RabbitMQ node to leave the cluster and eturn to its virgin state
   rotate_logs                   Instructs the RabbitMQ node to perform internal log rotation
   shutdown                      Stops RabbitMQ and its runtime (Erlang VM). Monitors progress for local nodes. Does not require a PID file path.
   start_app                     Starts the RabbitMQ application but leaves the runtime (Erlang VM) running
   stop                          Stops RabbitMQ and its runtime (Erlang VM). Requires a local node pid file path to monitor progress.
   stop_app                      Stops the RabbitMQ application, leaving the runtime (Erlang VM) running
   wait                          Waits for RabbitMQ node startup by monitoring a local PID file. See also 'rabbitmqctl await_online_nodes'

Cluster:

   await_online_nodes            Waits for <count> nodes to join the cluster
   change_cluster_node_type      Changes the type of the cluster node
   cluster_status                Displays all the nodes in the cluster grouped by node type, together with the currently running nodes
   force_boot                    Forces node to start even if it cannot contact or rejoin any of its previously known peers
   force_reset                   Forcefully returns a RabbitMQ node to its virgin state
   forget_cluster_node           Removes a node from the cluster
   join_cluster                  Instructs the node to become a member of the cluster that the specified node is in
   rename_cluster_node           Renames cluster nodes in the local database
   update_cluster_nodes          Instructs a cluster member node to sync the list of known cluster members from <seed_node>

Replication:

   cancel_sync_queue             Instructs a synchronising mirrored queue to stop synchronising itself
   sync_queue                    Instructs a mirrored queue with unsynchronised mirrors (follower replicas) to synchronise them

Users:

   add_user                      Creates a new user in the internal database
   authenticate_user             Attempts to authenticate a user. Exits with a non-zero code if authentication fails.
   change_password               Changes the user password
   clear_password                Clears (resets) password and disables password login for a user
   delete_user                   Removes a user from the internal database. Has no effect on users provided by external backends such as LDAP
   list_users                    List user names and tags
   set_user_tags                 Sets user tags

Access Control:

   clear_permissions             Revokes user permissions for a vhost
   clear_topic_permissions       Clears user topic permissions for a vhost or exchange
   list_permissions              Lists user permissions in a virtual host
   list_topic_permissions        Lists topic permissions in a virtual host
   list_user_permissions         Lists permissions of a user across all virtual hosts
   list_user_topic_permissions   Lists user topic permissions
   list_vhosts                   Lists virtual hosts
   set_permissions               Sets user permissions for a vhost
   set_topic_permissions         Sets user topic permissions for an exchange

Monitoring, observability and health checks:

   environment                   Displays the name and value of each variable in the application environment for each running application
   list_bindings                 Lists all bindings on a vhost
   list_channels                 Lists all channels in the node
   list_ciphers                  Lists cipher suites supported by encoding commands
   list_connections              Lists AMQP 0.9.1 connections for the node
   list_consumers                Lists all consumers in a vhost
   list_exchanges                Lists exchanges
   list_hashes                   Lists hash functions supported by encoding commands
   list_queues                   Lists queues and their properties
   list_unresponsive_queues      Tests queues to respond within timeout. Lists those which did not respond
   node_health_check             Performs several opinionated health checks of the target node
   ping                          Checks that the node OS process is up, registered with EPMD and CLI tools can authenticate with it
   report                        Generate a server status report containing a concatenation of all server status information for support purposes
   schema_info                   Lists schema database tables and their properties
   status                        Displays broker status information

Parameters:

   clear_global_parameter        Clears a global runtime parameter
   clear_parameter               Clears a runtime parameter.
   list_global_parameters        Lists global runtime parameters
   list_parameters               Lists runtime parameters for a virtual host
   set_global_parameter          Sets a runtime parameter.
   set_parameter                 Sets a runtime parameter.

Policies:

   clear_operator_policy         Clears an operator policy
   clear_policy                  Clears (removes) a policy
   list_operator_policies        Lists operator policy overrides for a virtual host
   list_policies                 Lists all policies in a virtual host
   set_operator_policy           Sets an operator policy that overrides a subset of arguments in user policies
   set_policy                    Sets or updates a policy

Virtual hosts:

   add_vhost                     Creates a virtual host
   clear_vhost_limits            Clears virtual host limits
   delete_vhost                  Deletes a virtual host
   list_vhost_limits             Displays configured virtual host limits
   restart_vhost                 Restarts a failed vhost data stores and queues
   set_vhost_limits              Sets virtual host limits
   trace_off                     
   trace_on                      

Node configuration:

   decode                        Decrypts an encrypted configuration value
   encode                        Encrypts a sensitive configuration value
   set_cluster_name              Sets the cluster name
   set_disk_free_limit           Sets the disk_free_limit setting
   set_log_level                 Sets log level in the running node
   set_vm_memory_high_watermark  Sets the vm_memory_high_watermark setting

Operations:

   close_all_connections         Instructs the broker to close all connections for the specified vhost or entire RabbitMQ node
   close_connection              Instructs the broker to close the connection associated with the Erlang process id
   eval                          Evaluates a snippet of Erlang code on the target node
   exec                          Evaluates a snippet of Elixir code on the CLI node
   force_gc                      Makes all Erlang processes on the target node perform/schedule a full sweep garbage collection
   hipe_compile                  Performs HiPE-compilation of [some] server modules to the given directory to be used with RABBITMQ_SERVER_CODE_PATH

Queues:

   delete_queue                  Deletes a queue
   purge_queue                   Purges a queue (removes all messages in it)

Other:

   enable_feature_flag           
   list_feature_flags            

Use 'rabbitmqctl help <command>' to learn more about a specific command


Only root or rabbitmq can run rabbitmqctl



图形化管理界面。

因为之前启用了图形化管理插件 rabbitmq-plugins enable

可以通过 15672 端口访问管理页面。

http://192.168.223.128:15672/

初始登录用户名密码。

Username: guest
Password: guest

默认 7 个交换机。

在这里插入图片描述



插件管理。
[root@localhost ~]# rabbitmq-plugins

Usage

rabbitmq-plugins [--node <node>] [--timeout <timeout>] [--longnames] [--quiet] <command> [<command options>]

Available commands:

Help:

   help         Displays usage information for a command
   version      Displays CLI tools version

Monitoring, observability and health checks:

   directories  Displays plugin directory and enabled plugin file paths
   is_enabled   Health check that exits with a non-zero code if provided plugins are not enabled on target node

Plugin Management:

   disable      Disables one or more plugins
   enable       Enables one or more plugins
   list         Lists plugins and their state
   set          Enables one or more plugins, disables the rest

Use 'rabbitmq-plugins help <command>' to learn more about a specific command


  • 查看所有已安装插件。

[E*] 表示我们自己主动启用的插件。
[e*] 表示主插件所依赖的功能插件。

explicit
a. 清楚明白的;易于理解的;(说话)清晰的,明确的;直言的;坦率的;直截了当的;不隐晦的;不含糊的

implicit
a. 含蓄的;不直接言明的;成为一部分的;内含的;完全的;无疑问的

[root@localhost ~]# rabbitmq-plugins list
Listing plugins with pattern ".*" ...
 Configured: E = explicitly enabled; e = implicitly enabled
 | Status: * = running on rabbit@localhost
 |/
[  ] rabbitmq_amqp1_0                  3.7.18
[  ] rabbitmq_auth_backend_cache       3.7.18
[  ] rabbitmq_auth_backend_http        3.7.18
[  ] rabbitmq_auth_backend_ldap        3.7.18
[  ] rabbitmq_auth_mechanism_ssl       3.7.18
[  ] rabbitmq_consistent_hash_exchange 3.7.18
[  ] rabbitmq_event_exchange           3.7.18
[  ] rabbitmq_federation               3.7.18
[  ] rabbitmq_federation_management    3.7.18
[  ] rabbitmq_jms_topic_exchange       3.7.18
[E*] rabbitmq_management               3.7.18
[e*] rabbitmq_management_agent         3.7.18
[  ] rabbitmq_mqtt                     3.7.18
[  ] rabbitmq_peer_discovery_aws       3.7.18
[  ] rabbitmq_peer_discovery_common    3.7.18
[  ] rabbitmq_peer_discovery_consul    3.7.18
[  ] rabbitmq_peer_discovery_etcd      3.7.18
[  ] rabbitmq_peer_discovery_k8s       3.7.18
[  ] rabbitmq_random_exchange          3.7.18
[  ] rabbitmq_recent_history_exchange  3.7.18
[  ] rabbitmq_sharding                 3.7.18
[  ] rabbitmq_shovel                   3.7.18
[  ] rabbitmq_shovel_management        3.7.18
[  ] rabbitmq_stomp                    3.7.18
[  ] rabbitmq_top                      3.7.18
[  ] rabbitmq_tracing                  3.7.18
[  ] rabbitmq_trust_store              3.7.18
[e*] rabbitmq_web_dispatch             3.7.18
[  ] rabbitmq_web_mqtt                 3.7.18
[  ] rabbitmq_web_mqtt_examples        3.7.18
[  ] rabbitmq_web_stomp                3.7.18
[  ] rabbitmq_web_stomp_examples       3.7.18
[root@localhost ~]# 



AMQP。

AMQP,Advanced Message Queuing ProtocoI(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的宫户端与消息中间件可传递消息,并不受客户端 / 中间件不同产品、不同开发言等条件的限制。2006 年,AMQP 规范发布。类比 HTTP。



Java 程序。

在这里插入图片描述
https://www.rabbitmq.com/getstarted.html

  • Maven 工程引入 Dependencies。
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.7.2</version>
</dependency>
  • 建立虚拟主机。

虚拟主机以 / 开头。

  • 虚拟机绑定用户。


Hello World ~ publisher / sender ~ “The simplest thing that does something”。(直连)。

应用场景:注册短信验证码。

在这里插入图片描述

在这里插入图片描述

package com.geek.helloworld;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import org.junit.Test;

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

/**
 * sender。
 */
public class Publisher {

    /**
     * 发布消息。
     *
     * @throws IOException
     */
    @Test
    public void testSendMessages() throws IOException, TimeoutException {
/*
        // 创建连接工厂。
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 设置连接 RabbitMQ 主机。
        connectionFactory.setHost("192.168.142.161");
        // 连接 RabbitMQ 主机端口。
        connectionFactory.setPort(5672);
        // 虚拟主机。
        connectionFactory.setVirtualHost("/amqp_test");
        // 设置访问虚拟主机的用户名和密码。
        connectionFactory.setUsername("geek");
        connectionFactory.setPassword("123.");

        // 获取连接对象。
        Connection connection = connectionFactory.newConnection();
*/

        // 通过工具获取对象。
        Connection connection = RabbitMQUtils.getConnection();

        // 获取连接中通道对象。
        Channel channel = connection.createChannel();

        // 通道声明对应的消息队列。
        // 参数 1:queue。队列名称,如果队列不存在,会自动创建。
        // 参数 2:durable。队列特性:是否持久化。只是队列持久化,队列中的消息不会持久化。false ~ RabbitMQ 重启队列就没了。
        // 参数 3:exclusive。是否独占连接。
        // 参数 4:autoDelete。是否在消费完成后删除队列。(要在消费者释放连接后才删除)。
        // 参数 5:argument。额外 / 附加参数。
        channel.queueDeclare("hello", false, false, false, null);

        // 发布消息。
        // 参数 1:exchange。交换机名称。P/C 没有交换机。
        // 参数 2:routingKey。队列名称。
        // 参数 3:props。传递消息额外参数。MessageProperties.PERSISTENT_TEXT_PLAIN ~ 队列中的消息持久化。
        // 参数 4:消息具体内容。字节形式。
        channel.basicPublish("", "hello", MessageProperties.PERSISTENT_TEXT_PLAIN, "hello".getBytes());

//        channel.close();
//        connection.close();
        // 通过工具关闭连接。
        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }

}

package com.geek.producer;

import com.geek.producer.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

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

public class ProducerHelloWorld {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        // 创建队列 Queue。
        /*
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                 Map<String, Object> arguments) throws IOException;
        参数:
            queue ~ 队列名称。
            durable ~ 是否持久化,当 mq 重启之后,还在。
            exclusive ~
                是否独占。只能有一个消费者监听这队列。
                当 Connection 关闭时,是否删除队列。
            autoDelete ~ 是否自动删除。当没有 Consumer 时,自动删除队列。
            arguments ~ 参数。
         */
        // 如果没有一个名字叫 hello_world 的队列,则会创建该队列,如果有则不会创建。
        channel.queueDeclare("hello_world", true, false, false, null);
        /*
    void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
        参数:
            exchange ~ 交换机名称。简单模式下交换机会使用默认的 ""。
            routingKey ~ 路由名称。简单模式写队列名。
            props ~ 配置信息。
            body ~ 发送消息数据。
         */
        String body = "hello rabbitmq ~~~ ";
        // 发送消息。
        channel.basicPublish("", "hello_world", null, body.getBytes());
        // 释放资源。
        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }

}

运行程序,发送消息。

在这里插入图片描述



consumer / receiver。
package com.geek.helloworld;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {

    public static void main(String[] args) throws IOException {
        // 通过工具获取对象。
        Connection connection = RabbitMQUtils.getConnection();

        // 创建通道。
        Channel channel = connection.createChannel();

        // 通道声明队列。
        channel.queueDeclare("hello", false, false, false, null);
        // 参数要一致。
        // Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=405, reply-text=RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'hello' in vhost '/amqp_test'. It could be originally declared on another connection or the exclusive property value does not match that of the original declaration., class-id=50, method-id=10)

        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume("hello", true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag
             * @param envelope
             * @param properties
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);
                System.out.println("body = " + new String(body));
                // consumerTag = amq.ctag-241G8Sk2zGys_1QO9IxbxA
                //envelope = Envelope(deliveryTag=1, redeliver=false, exchange=, routingKey=hello)
                //properties = #contentHeader<basic>(content-type=text/plain, content-encoding=null, headers=null, delivery-mode=2, priority=0, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
                //body = hello
            }
        });

        // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();
    }

}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerHelloWorld {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        // 创建队列 Queue。
        /*
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                 Map<String, Object> arguments) throws IOException;
        参数:
            queue ~ 队列名称。
            durable ~ 是否持久化,当 mq 重启之后,还在。
            exclusive ~
                是否独占。只能有一个消费者监听这队列。
                当 Connection 关闭时,是否删除队列。
            autoDelete ~ 是否自动删除。当没有 Consumer 时,自动删除队列。
            arguments ~ 参数。
         */
        // 如果没有一个名字叫 hello_world 的队列,则会创建该队列,如果有则不会创建。
        channel.queueDeclare("hello_world", true, false, false, null);

        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume("hello_world", true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);
                System.out.println("body = " + new String(body));
                // consumerTag = amq.ctag-epbgB2Mc-twaMn0GQOx6sA
                //envelope = Envelope(deliveryTag=1, redeliver=false, exchange=, routingKey=hello_world)
                //properties = #contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
                //body = hello rabbitmq ~~~
            }
        });
    }

    // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();

}



优化代码 ~ 工具类。
package com.geek.helloworld.utils;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

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

public class RabbitMQUtils {

    /**
     * 提供连接对象。
     *
     * @return
     */
    public static Connection getConnection() {
        // 创建连接工厂。
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 设置连接 RabbitMQ 主机。
        connectionFactory.setHost("192.168.223.128");
        // 连接 RabbitMQ 主机端口。
        connectionFactory.setPort(5672);
        // 虚拟主机。
        connectionFactory.setVirtualHost("/amqp_test");
        // 设置访问虚拟主机的用户名和密码。
        connectionFactory.setUsername("geek");
        connectionFactory.setPassword("123.");

        try {
            return connectionFactory.newConnection();
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }

        return null;
    }

    public static void closeConnectionAndChannel(Channel channel, Connection connection) {
        if (channel != null) {
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }

        if (connection != null) {
            try {
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}



优化代码 ~ static。
package com.geek.helloworld.utils;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

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

public class RabbitMQUtils {

    private static ConnectionFactory connectionFactory;

    // 重量级资源。类加载时执行,只执行一次。
    static {
        // 创建连接工厂。
        connectionFactory = new ConnectionFactory();
        // 设置连接 RabbitMQ 主机。
        connectionFactory.setHost("192.168.223.128");
        // 连接 RabbitMQ 主机端口。
        connectionFactory.setPort(5672);
        // 虚拟主机。
        connectionFactory.setVirtualHost("/amqp_test");
        // 设置访问虚拟主机的用户名和密码。
        connectionFactory.setUsername("geek");
        connectionFactory.setPassword("123.");
    }

    /**
     * 提供连接对象。
     *
     * @return
     */
    public static Connection getConnection() {
        try {
            return connectionFactory.newConnection();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 释放资源工具。
     *
     * @param channel
     * @param connection
     */
    public static void closeConnectionAndChannel(Channel channel, Connection connection) {
        if (channel != null) {
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }

        if (connection != null) {
            try {
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}



参数细节。

// 参数 2:durable。队列特性:是否持久化。只是队列持久化,队列中的消息不会持久化。
因为 ta 只是队列的声明,对消息不起作用。

  • 要想持久化队列中的消息,在发送消息时指定参数 3。
        // 发布消息。
        // 参数 1:exchange。交换机名称。
        // 参数 2:routingKey。队列名称。
        // 参数 3:props。传递消息额外参数。MessageProperties.PERSISTENT_TEXT_PLAIN ~ 队列中的消息持久化。
        // 参数 4:消息具体内容。字节形式。
        channel.basicPublish("", "hello", MessageProperties.PERSISTENT_TEXT_PLAIN, "hello".getBytes());
        // 通道绑定对应的消息队列。
        // 参数 1:queue。队列名称,如果队列不存在,会自动创建。
        // 参数 2:durable。队列特性:是否持久化。只是队列持久化,队列中的消息不会持久化。
        // 参数 3:exclusive。是否独占连接。
        // 参数 4:autoDelete。是否在消费完成后删除队列。
        // 参数 5:argument。额外 / 附加参数。
        channel.queueDeclare("hello", false, false, false, null);

        // 发布消息。
        // 参数 1:exchange。交换机名称。
        // 参数 2:routingKey。队列名称。
        // 参数 3:props。传递消息额外参数。MessageProperties.PERSISTENT_TEXT_PLAIN ~ 队列中的消息持久化。
        // 参数 4:消息具体内容。字节形式。
        channel.basicPublish("", "hello", MessageProperties.PERSISTENT_TEXT_PLAIN, "hello".getBytes());
  • // 参数 4:autoDelete。是否在消费完成后删除队列。(要在消费者释放连接后才删除)。


模型 2 ~ Work Queues ~ “Distributing tasks among workers (the competing consumers pattern)”。

在这里插入图片描述

第一种方式只有一个消费者,可能产生消息堆积。

work queues,也被称为(Task queues),任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用 work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。

↓ ↓ ↓

多个消费者。缓解压力。

package com.geek.helloworld.workQueues;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;

public class WorkQueuesPublisher {

    public static void main(String[] args) throws IOException {
        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();
        // 获取通道对象,
        Channel channel = connection.createChannel();
        // 通过通道声明对象。
        channel.queueDeclare("work", true, false, false, null);

        // 发布消息。
        for (int i = 0; i < 10; i++) {
            channel.basicPublish("", "work", null, ("hello work queues ~ " + i).getBytes());
        }

        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }

}

consumer01。

package com.geek.helloworld.workQueues;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer01 {

    public static void main(String[] args) throws IOException {
        // 获取连接。
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.basicQos(1);// 每次只消费一个消息。
        channel.queueDeclare("work", true, false, false, null);
        // autoAck ~ false ~ 不自动确认消息。
        channel.basicConsume("work", false, new DefaultConsumer(channel) {
            /**
             * No-op implementation of {@link Consumer#handleDelivery}.
             *
             * @param consumerTag
             * @param envelope
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("消费者 ~ 1 ~ " + new String(body));
                // 参数 1:deliveryTag。确认消息队列中的哪个消息。
                // 参数 2:multiple。是否开启多个消息同时确认。
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });

        // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();
    }

}

consumer02。

package com.geek.helloworld.workQueues;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer02 {

    public static void main(String[] args) throws IOException {
        // 获取连接。
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.basicQos(1);// 每次只消费一个消息。
        channel.queueDeclare("work", true, false, false, null);
        // autoAck ~ false ~ 不自动确认消息。
        channel.basicConsume("work", false, new DefaultConsumer(channel) {
            //            /**
//             * No-op implementation of {@link Consumer#handleDelivery}.
//             *
//             * @param consumerTag
//             * @param envelope
//             * @param properties
//             * @param body
//             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("消费者 ~ 2 ~ " + new String(body));
                // 参数 1:deliveryTag。确认消息队列中的哪个消息。
                // 参数 2:multiple。是否开启多个消息同时确认。
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        });

        // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();
    }

}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerWorkQueues01 {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        // 创建队列 Queue。
        /*
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                 Map<String, Object> arguments) throws IOException;
        参数:
            queue ~ 队列名称。
            durable ~ 是否持久化,当 mq 重启之后,还在。
            exclusive ~
                是否独占。只能有一个消费者监听这队列。
                当 Connection 关闭时,是否删除队列。
            autoDelete ~ 是否自动删除。当没有 Consumer 时,自动删除队列。
            arguments ~ 参数。
         */
        // 如果没有一个名字叫 hello_world 的队列,则会创建该队列,如果有则不会创建。
        channel.queueDeclare("work_queues", true, false, false, null);

        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume("work_queues", true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                /*System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);*/
                System.out.println("body = " + new String(body));
            }
        });
    }

    // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();

}



特点:默认平均分配。

By default, RabbitMQ will send each message to the next consumer, in sequence. On average every consumer will get the same number of messages. This way of distributing messages is called round-robin. Try this out with three or more workers.



改进:能者多劳。
消息确认机制。

基于 worker queues 模式,一旦消费者接受消息,RabbitMQ 就把全部消息一次性给了消费者,然后将 MQ 中的消息一次性全部删除。消费者处理消息需要时间。此过程中一旦消费者宕机,没有处理的消息就丢失了。

  • Message acknowledgment

Doing a task can take a few seconds. You may wonder what happens if one of the consumers starts a long task and dies with it only partly done. With our current code, once RabbitMQ delivers a message to the consumer it immediately marks it for deletion. In this case, if you kill a worker we will lose the message it was just processing. We’ll also lose all the messages that were dispatched to this particular worker but were not yet handled.



解决。
  • 关闭自动消息确认。

// 参数 2:开启消息自动确认机制。——> 关闭。

        // 消费消息。
        // 参数 1:队列名称。
        // 参数 2:开启消息自动确认机制。
        // 参数 3:浪费时的回调接口。
        channel.basicConsume("work", false, new DefaultConsumer(channel) {
  • 不要让消息队列一次性将消息都给消费者。

channel.basicQos(1);// 每次只消费一个消息。

  • 业务处理完成后,手动消息确认。

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

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("消费者 ~ 1 ~ " - new String(body));
                // 参数 1:deliveryTag。确认消息队列中的哪个消息。
                // 参数 2:multiple。是否开启多个消息同时确认。
                channel.basicAck(envelope.getDeliveryTag(), false);
            }



模型 3 ~ Publish / Subscribe ~ 交换机类型:fanout ~ “Sending messages to many consumers at once”。

fanout
n. 输出(端数);展(散)开;扇出; 分列账户

在这里插入图片描述

在这里插入图片描述

在订阅模型中,多了一个 Exchange 角色,而且过程略有变化。

  • P ~ 生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给 X(交换机)。
  • C ~ 消费者,消息的接收者,会一直等待消息到来。
  • Queue ~ 消息队列,接收消息、缓存消息。
  • Exchange ~ 交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于 Exchange 的类型。Exchange 有常见以下 3 种类型。

Fanout ~ 广播,将消息交给所有绑定到交换机的队列。
Direct ~ 定向,把消息交给符合指定 routing key 的队列。
Topic ~ 通配符,把消息交给符合 routing pattern(路由模式)的队列。

package com.rabbitmq.client;

/**
 * Enum for built-in exchange types.
 */
public enum BuiltinExchangeType {

    DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");

    private final String type;

    BuiltinExchangeType(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!

  • 广播模式。
  • 可以有多个消费者。
  • 每个消费者都有自己的 Queue(队列)。
  • 每个 Queue 都要绑定到 Exchange(交换机)。
  • 生产者发送的消息只能发送到交换机。交换机来决定要发送给哪个队列。生产者无法决定。
  • 交换机把消息发送给绑定过的所有队列。
  • 队列的消费者都能拿到消息。实现了一条消息被多个消费者消费。
  • 代码。
package com.geek.helloworld.psFanout;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;

public class Publisher {

    public static void main(String[] args) throws IOException {
        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 通道声明交换机。
        // 参数 1:交换机名称。
        // 参数 2:交换机类型。fanout ——> 广播类型。
        channel.exchangeDeclare("logs", "fanout");

        // 发送消息。
        // 参数 1:exchange。交换机。
        // 参数 2:routingKey。路由 key。广播模式路由 key 无意义。
        // 参数 3:props。附加参数。
        // 参数 4:消息。
        channel.basicPublish("logs", "", null, "fanout type messages".getBytes());

        // 释放资源。
        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }
}

package com.geek.producer;

import com.geek.producer.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

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

public class ProducerPubSub {

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

        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();

        Channel channel = connection.createChannel();

        String exchangeName = "test_fanout";
/*
        AMQP.Exchange.DeclareOk exchangeDeclare (String exchange,  // 交换机名称。
            String type,  // 交换机类型。public enum BuiltinExchangeType {
            boolean durable,  // 是否持久化。
            boolean autoDelete,  // 自动删除。
            boolean internal,  // 内部使用,一般 false。
            Map<String, Object> arguments  // 参数。
        ) throws IOException;
*/
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);

        String queue1Name = "test_fanout_queue1";
        String queue2Name = "test_fanout_queue2";
        // 声明队列。
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);

        // 绑定队列和交换机。
        // 交换机类型 ~ fanout,routingKey 为 ""。
        channel.queueBind(queue1Name, exchangeName, "");
        channel.queueBind(queue2Name, exchangeName, "");

        // 发送消息。
        // 参数 1:exchange。交换机。
        // 参数 2:routingKey。路由 key。广播模式不需要路由 key。
        // 参数 3:props。附加参数。
        // 参数 4:消息。
        String body = "日志信息 ~ 张三调用了 findAll(); 方法...日志级别 ~ info...";
        channel.basicPublish(exchangeName, "", null, body.getBytes());

        // 释放资源。
        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }

}

消费者 × 3。

package com.geek.helloworld.psFanout;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer01 {

    public static void main(String[] args) throws IOException {
        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 通道声明交换机。
        channel.exchangeDeclare("logs", "fanout");

        // 临时队列。
        String queueName = channel.queueDeclare().getQueue();

        // 绑定交换机的队列。
        channel.queueBind(queueName, "logs", "");

        // 消费消息。
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            /**
             * No-op implementation of {@link Consumer#handleDelivery}.
             *
             * @param consumerTag
             * @param envelope
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("消费者 ~ 1 ~ " - new String(body));
            }
        });
    }
}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerPubSub01 {

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

        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();

        Channel channel = connection.createChannel();

        String queue1Name = "test_fanout_queue1";
        String queue2Name = "test_fanout_queue2";

        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume(queue1Name, true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                /*System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);*/
                System.out.println("body = " + new String(body));
                System.out.println("将日志信息打印到控制台。");
            }
        });
    }

    // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();

}



模型 4 ~ Routing ~ “Receiving messages selectively” ~ (路由之 Direct)。

在这里插入图片描述

P ——> 生产者。

fanout 模型中,一条消息,会被所有订阅的队列都消费。但在某些情况下,我们希望不同的消息被不同的队列消费。这时就要用到 Direct 类型的 Exchange。

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 routing key(路由 key)。
  • 消息的发送方向 Exchange 发送消息时,也必须指定消息的 routing key
  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 routing key 进行判断,只有队列的 routing key 与消息的 routing key 完成一致时,才会接受消息。
  • 代码。
package com.geek.helloworld.routingDirect;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;

public class Publisher {

    public static void main(String[] args) throws IOException {
        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();
        // 连接创建通道。
        Channel channel = connection.createChannel();
        // 通过通道声明交换机。
        // 参数 1:交换机名称。
        // 参数 2:交换机类型。 ~ direct。
        channel.exchangeDeclare("logs_direct", "direct");

        // 发送消息。
        String routingKey = "info";
        channel.basicPublish("logs_direct", routingKey, null, ("direct 类型路由器 " - routingKey - " 发布的消息。").getBytes());

        // 释放资源。
        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }
}

package com.geek.producer;

import com.geek.producer.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

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

public class ProducerRouting {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();
        /*
        AMQP.Exchange.DeclareOk exchangeDeclare (String exchange,  // 交换机名称。
            String type,  // 交换机类型。public enum BuiltinExchangeType {
                DIRECT("direct"), ~ 定向。
                FANOUT("fanout"), ~ 扇形(广播),发送消息到每一个与之绑定队列。
                TOPIC("topic"), ~ 通配符的方式。
                HEADERS("headers"); ~ 参数匹配。
            boolean durable,  // 是否持久化。
            boolean autoDelete,  // 自动删除。
            boolean internal,  // 内部使用,一般 false。
            Map<String, Object> arguments  // 参数。
        ) throws IOException;
*/
        String exchangeName = "test_direct";
        // 声明交换机。
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);

        // 创建队列。
        String queue1Name = "test_direct_queue1";
        String queue2Name = "test_direct_queue2";

        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);

        // 绑定队列和交换机。
        /*
    Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;
        参数:
            queue ~ 队列名称。
            exchange ~ 交换机名称。
            routingKey ~ 路由键,绑定规则。
                如果交换机的类型为 fanout,routingKey 设置为 ""。
         */
        // 队列 1 绑定 error。
        channel.queueBind(queue1Name, exchangeName, "error");
        // 队列 2 绑定 info error warning。
        channel.queueBind(queue2Name, exchangeName, "info");
        channel.queueBind(queue2Name, exchangeName, "error");
        channel.queueBind(queue2Name, exchangeName, "warning");

        String body = "日志信息:张三调用了delete方法...出错误了。。。日志级别:error...";
        // 发送消息。
        channel.basicPublish(exchangeName, "warning", null, body.getBytes());

        // 释放资源。
        channel.close();
        connection.close();
    }

}

// 基于 route key 绑定队列和交换机。
channel.queueBind(queueName, “logs_direct”, “error”);

生产者发送消息的路由 key 是 info
这个消费者不能接收消息。

package com.geek.helloworld.routingDirect;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer01 {

    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();

        Channel channel = connection.createChannel();

        String exchangeName = "logs_direct";

        // 通道声明交换机以及交换机类型。
        channel.exchangeDeclare(exchangeName, "direct");

        // 得到一个临时队列。
        String queueName = channel.queueDeclare().getQueue();

        // 基于 route key 绑定队列和交换机。
        channel.queueBind(queueName, exchangeName, "error");

        // 获取消费的消息。
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("消费者 01 ~ " - new String(body) - "(routing key ~ error");
            }
        });
    }

}

// 基于 route key 绑定队列和交换机。
channel.queueBind(queueName, “logs_direct”, “info”);
channel.queueBind(queueName, “logs_direct”, “error”);
channel.queueBind(queueName, “logs_direct”, “warning”);

生产者发送消息的路由 key 是 info
这个消费者可以接收消息。

package com.geek.helloworld.routingDirect;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer02 {

    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();

        Channel channel = connection.createChannel();

        String exchangeName = "logs_direct";

        // 通道声明交换机,类型 Direct。
        channel.exchangeDeclare(exchangeName, "direct");

        // 创建一个临时队列。
        String queueName = channel.queueDeclare().getQueue();

        // 基于 route key 绑定队列和交换机。
        channel.queueBind(queueName, exchangeName, "info");
        channel.queueBind(queueName, exchangeName, "error");
        channel.queueBind(queueName, exchangeName, "warning");

        // 获取消费的消息。
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("消费者 02 ~ " - new String(body));
            }
        });
    }
}

package com.geek.producer;

import com.geek.producer.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

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

public class ProducerRouting {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();
        /*
        AMQP.Exchange.DeclareOk exchangeDeclare (String exchange,  // 交换机名称。
            String type,  // 交换机类型。public enum BuiltinExchangeType {
                DIRECT("direct"), ~ 定向。
                FANOUT("fanout"), ~ 扇形(广播),发送消息到每一个与之绑定队列。
                TOPIC("topic"), ~ 通配符的方式。
                HEADERS("headers"); ~ 参数匹配。
            boolean durable,  // 是否持久化。
            boolean autoDelete,  // 自动删除。
            boolean internal,  // 内部使用,一般 false。
            Map<String, Object> arguments  // 参数。
        ) throws IOException;
*/
        String exchangeName = "test_fanout";
        // 声明交换机。
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);

        // 创建队列。
        String queue1Name = "test_direct_queue1";
        String queue2Name = "test_direct_queue2";

        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);

        // 绑定队列和交换机。
        /*
    Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;
        参数:
            queue ~ 队列名称。
            exchange ~ 交换机名称。
            routingKey ~ 路由键,绑定规则。
                如果交换机的类型为 fanout,routingKey 设置为 ""。
         */
        // 队列 1 绑定 error。
        channel.queueBind(queue1Name, exchangeName, "error");
        // 队列 2 绑定 info error warning。
        channel.queueBind(queue2Name, exchangeName, "info");
        channel.queueBind(queue2Name, exchangeName, "error");
        channel.queueBind(queue2Name, exchangeName, "warning");

        String body = "日志信息 ~ 张三调用了 delete(); 方法...出错误了。。。日志级别:error...";
        // 发送消息。
        channel.basicPublish(exchangeName, "warning", null, body.getBytes());

        // 释放资源。
        channel.close();
        connection.close();
    }

}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

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

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        String queue1Name = "test_direct_queue1";
        String queue2Name = "test_direct_queue2";

        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume(queue1Name, true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                /*System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);*/
                System.out.println("body = " + new String(body));
                System.out.println("将日志信息打印到控制台。");
            }
        });
    }

    // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();

}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

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

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        String queue1Name = "test_direct_queue1";
        String queue2Name = "test_direct_queue2";

        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume(queue2Name, true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                /*System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);*/
                System.out.println("body = " + new String(body));
                System.out.println("将日志信息保存到数据库。");
            }
        });
    }

    // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();

}



模型 5 ~ Topics ~ “Receiving messages based on a pattern (topics)” ~ (动态路由)。

在这里插入图片描述

Topics 类型的 Exchange 与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topics 类型 exchange 可以让队列绑定 RoutingKey 时使用通配符。这种模型 RoutingKey 一般都是由一个或多个单词组成,多个单词之间可以用 . 分隔。eg. item.insert。

  • * (star) can substitute for exactly one word.
  • # (hash) can substitute for zero or more words.

one word ——> 指以 . 分隔的单词。

eg.

lazy.* ——> lazy.insert, lazy.update
lazy.# ——> lazy, lazy.insert.user, lazy.update.user.name

  • 代码。

publisher。

package com.geek.helloworld.routingTopics;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.io.IOException;

public class Publisher {

    public static void main(String[] args) throws IOException {
        // 获取连接对象。
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 声明交换机以及交换机类型  ~  topic。
        channel.exchangeDeclare("topics", "topic");

        // 发布消息。
        String routingKey = "user.save";

        channel.basicPublish("topics", routingKey, null, ("topic 动态路由模型。routingKey ~ " - routingKey).getBytes());

        // 释放资源。
        RabbitMQUtils.closeConnectionAndChannel(channel, connection);
    }
}

  • 消费者。
package com.geek.helloworld.routingTopics;

import com.geek.helloworld.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer01 {

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

        // 获取连接。
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明交换机以及交换机类型。
        channel.exchangeDeclare("topics", "topic");

        // 创建临时队列。
        String queue = channel.queueDeclare().getQueue();
        // 指定队列和交换机。动态通配符形式。RoutingKey。
        channel.queueBind(queue, "topics", "user.*");

        // 消费消息。
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
            /**
             * No-op implementation of {@link Consumer#handleDelivery}.
             *
             * @param consumerTag
             * @param envelope
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("消费者 ~ 01 ~ " - new String(body));
            }
        });
    }
}

package com.geek.producer;

import com.geek.producer.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

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

public class ProducerTopics {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();
        /*
        AMQP.Exchange.DeclareOk exchangeDeclare (String exchange,  // 交换机名称。
            String type,  // 交换机类型。public enum BuiltinExchangeType {
                DIRECT("direct"), ~ 定向。
                FANOUT("fanout"), ~ 扇形(广播),发送消息到每一个与之绑定队列。
                TOPIC("topic"), ~ 通配符的方式。
                HEADERS("headers"); ~ 参数匹配。
            boolean durable,  // 是否持久化。
            boolean autoDelete,  // 自动删除。
            boolean internal,  // 内部使用,一般 false。
            Map<String, Object> arguments  // 参数。
        ) throws IOException;
*/
        String exchangeName = "test_topic";
        //5. 创建交换机
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);
        //6. 创建队列
        String queue1Name = "test_topic_queue1";
        String queue2Name = "test_topic_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 绑定队列和交换机。
        /*
    Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;
        参数:
            queue ~ 队列名称。
            exchange ~ 交换机名称。
            routingKey ~ 路由键,绑定规则。
                如果交换机的类型为 fanout,routingKey 设置为 ""。
         */
        // routing key 系统的名称.日志的级别。
        // 需求:所有 error 级别的日志存入数据库,所有 order 系统的日志存入数据库>
        channel.queueBind(queue1Name, exchangeName, "#.error");
        channel.queueBind(queue1Name, exchangeName, "order.*");
        channel.queueBind(queue2Name, exchangeName, "*.*");

        String body = "日志信息 ~ 张三调用了 findAll(); 方法...日志级别:info...";
        // 发送消息。
//        channel.basicPublish(exchangeName, "order.error", null, body.getBytes());
        channel.basicPublish(exchangeName, "goods.info", null, body.getBytes());
        // 释放资源。
        channel.close();
        connection.close();
    }

}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerTopic01 {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        String queue1Name = "test_topic_queue1";
        String queue2Name = "test_topic_queue2";
        /*
        basicConsume(String queue, boolean autoAck, Consumer callback)
        参数:
            1. queue:队列名称
            2. autoAck:是否自动确认
            3. callback:回调对象

         */
        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume(queue1Name, true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                /*System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);*/
                System.out.println("body = " + new String(body));
                System.out.println("将日志信息保存到数据库。");
            }
        });

        // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();
    }

}

package com.geek.consumer;

import com.geek.consumer.utils.RabbitMQUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerTopic02 {

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

        Connection connection = RabbitMQUtils.getConnection();

        // 创建 Channel。
        Channel channel = connection.createChannel();

        String queue1Name = "test_topic_queue1";
        String queue2Name = "test_topic_queue2";
        /*
        basicConsume(String queue, boolean autoAck, Consumer callback)
        参数:
            1. queue:队列名称
            2. autoAck:是否自动确认
            3. callback:回调对象

         */
        // 消费消息。
        // 参数 1:queue。队列名称。
        // 参数 2:autoAck。开启消息自动确认机制。
        // 参数 3:消费时的回调接口。
        channel.basicConsume(queue2Name, true, new DefaultConsumer(channel) {
            /**
             * @param consumerTag   标识。
             * @param envelope      交换机、路由...信息。
             * @param properties    配置信息。
             * @param body          消息队列中取出的消息。(注:(异步的)必须用 main() 方法。Junit Test 不支持多线程模型的)。
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//                super.handleDelivery(consumerTag, envelope, properties, body);
                /*System.out.println("consumerTag = " + consumerTag);
                System.out.println("envelope = " + envelope);
                System.out.println("properties = " + properties);*/
                System.out.println("body = " + new String(body));
                System.out.println("将日志信息打印到控制台。");
            }
        });

    // 不关闭就可以一直监听。关闭只消费一次。
//        channel.close();
//        connection.close();
    }

}



Spring 整合 RabbitMQ。

publisher。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.geek</groupId>
    <artifactId>spring-rabbitmq-publisher</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

rabbitmq.host=192.168.142.161
rabbitmq.port=5672
rabbitmq.username=geek
rabbitmq.password=123.
rabbitmq.virtual-host=/amqp_test
  • spring-rabbitmq-publisher.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 定义管理交换机、队列。-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!-- 定义持久化队列,不存在则自动创建。
    不绑定到交换机则默认绑定到默认交换机 direct,名称为:"",路由键为队列名称。
    -->
    <!--
        id ~ bean 的名称。
        name ~ queue 的名称。
        auto-declare ~ 自动创建。
        auto-delete ~ 自动删除。最后一个消费者和该队列断开连接后,自动删除队列。
        exclusive ~ 是否独占。
        durable ~ 是否持久化。
    -->
    <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>

    <!-- ~ ~ ~ ~ ~ ~ ~ ~ 广播;所有队列都能收到消息。 ~ ~ ~ ~ ~ ~ ~ ~ -->
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>

    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>

    <!-- 定义广播类型交换机,并绑定上述两个队列。-->
    <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding queue="spring_fanout_queue_1"/>
            <rabbit:binding queue="spring_fanout_queue_2"/>
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!--<rabbit:direct-exchange name="aa">-->
        <!--<rabbit:bindings>-->
            <!--&lt;!&ndash; direct 类型的交换机绑定队列。key ~ 路由 key。queue ~ 队列名称。&ndash;&gt;-->
            <!--<rabbit:binding queue="spring_queue" key="xxx"/>-->
        <!--</rabbit:bindings>-->
    <!--</rabbit:direct-exchange>-->

    <!-- ~ ~ ~ ~ ~ ~ ~ ~ 通配符;* 匹配一个单词,# 匹配多个单词。~ ~ ~ ~ ~ ~ ~ ~-->
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/>
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/>

    <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="heima.*" queue="spring_topic_queue_star"/>
            <rabbit:binding pattern="heima.#" queue="spring_topic_queue_well"/>
            <rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 定义 rabbitTemplate 对象操作可以在代码中方便发送消息。-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

</beans>

package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-publisher.xml")
public class PublishTest {

    // 注入 RabbitTemplate。
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testHelloWorld() {
        // 发送消息。
        rabbitTemplate.convertAndSend("spring_queue", "hello world spring...");
    }

    @Test
    public void testFanout() {
        // 发送消息。
        rabbitTemplate.convertAndSend("spring_fanout_exchange", "", "hello fanout spring...");
    }

    @Test
    public void testTopics() {
        // 发送消息。
        rabbitTemplate.convertAndSend("spring_topic_exchange", "heima.hehe.haha", "hello topics spring...");
    }


}



consumer。
  • rabbitmq.properties。
rabbitmq.host=192.168.142.161
rabbitmq.port=5672
rabbitmq.username=geek
rabbitmq.password=123.
rabbitmq.virtual-host=/amqp_test

  • spring-rabbitmq-consumer.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 监听器对象。-->
    <bean id="springQueueListener" class="com.geek.rabbitmq.listener.SpringQueueListener"/>
    <!--<bean id="fanoutListener1" class="com.geek.rabbitmq.listener.FanoutListener1"/>
    <bean id="fanoutListener2" class="com.geek.rabbitmq.listener.FanoutListener2"/>
    <bean id="topicListenerStar" class="com.geek.rabbitmq.listener.TopicListenerStar"/>
    <bean id="topicListenerWell" class="com.geek.rabbitmq.listener.TopicListenerWell"/>
    <bean id="topicListenerWell2" class="com.geek.rabbitmq.listener.TopicListenerWell2"/>-->

    <!-- 绑定队列。-->
    <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
        <rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>
        <!--<rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
         <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
         <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
         <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
         <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>-->
    </rabbit:listener-container>

</beans>

  • 实现 MessageListener。
package com.geek.rabbitmq.listener;

import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;

import java.util.List;

public class SpringQueueListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("message = " + message);
    }

    @Override
    public void containerAckMode(AcknowledgeMode mode) {

    }

    @Override
    public void onMessageBatch(List<Message> messages) {

    }

}

package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
public class ConsumerTest {

    @Test
    public void test() {
        while (true) {

        }
    }

}



SpringBoot 整合 RabbitMQ。

  • pom。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
  • 配置。apllication.yml。
spring:
  application:
    name: rabbitmq-springboot
  rabbitmq:
    host: 192.168.223.128
    port: 5672
    username: guest
    password: guest
    virtual-host: /amqp_test

  • RabbitTemplate ——> 简化操作。
    @Autowired
    private RabbitTemplate rabbitTemplate;
  • 测试类。
package com.geek.springboot_rabbitmq;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = SpringbootRabbitmqApplication.class)
@RunWith(SpringRunner.class)
public class TestRabbitMQ {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // Hello World。
    @Test
    public void helloWorld() {
        rabbitTemplate.convertAndSend("hello", "Hello World.");
        // convertAndSend(String routingKey, Object object); 会把 Object 转化为 byte。
    }

}

生产者运行后并不会创建队列。



Hello world。
  • 消费者。
package com.geek.springboot_rabbitmq.hello;

import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queuesToDeclare = @Queue("hello"))// 默认 持久化,非独占,不自动删除。
//@RabbitListener(queuesToDeclare = @Queue(value = "hello", durable = "true", exclusive = "false", autoDelete = "false"))
public class Consumer {

    @RabbitHandler
    public void receive(String message) {
        System.out.println("~ ~ ~ ~ ~ ~ ~");
        System.out.println("message = " - message);
        System.out.println("~ ~ ~ ~ ~ ~ ~");
    }

}

  • @RabbitListener() 的参数。

在这里插入图片描述

  • @Queue() 的参数。

默认:持久化,非独占,不自动删除。

在这里插入图片描述



Worker Queues。
  • 测试类(生产者)。
    // worker。
    @Test
    public void testWorker() {
        for (int i = 0; i < 5; i++) {
            rabbitTemplate.convertAndSend("worker", "Worker Queues");
        }
    }

package com.geek.springboot_rabbitmq;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = SpringbootRabbitmqApplication.class)
@RunWith(SpringRunner.class)
public class TestRabbitMQ {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // worker。
    @Test
    public void testWorker() {
        for (int i = 0; i < 5; i++) {
            rabbitTemplate.convertAndSend("worker", "Worker Queues" - i);
        }
    }

    // Hello World。
    @Test
    public void helloWorld() {
        rabbitTemplate.convertAndSend("hello", "Hello World.");
        // convertAndSend(String routingKey, Object object); 会把 Object 转化为 byte。
    }

}

  • 消费者。
package com.geek.springboot_rabbitmq.worker;

import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class WorkerConsumer {

    // 一个消费者。
    @RabbitListener(queuesToDeclare = @Queue("worker"))
    public void consumer(String message) {
        System.out.println("message1 = " - message);
    }

    // 一个消费者。
    @RabbitListener(queuesToDeclare = @Queue("worker"))
    public void consumer02(String message) {
        System.out.println("message02 = " - message);
    }
}

  • 结果。
message1 = Worker Queues0
message02 = Worker Queues1
message02 = Worker Queues3
message1 = Worker Queues2
message1 = Worker Queues4


Publish / Subscribe。
  • 测试类 ~ 生产者。
    // Publish / Subscribe。fanout。
    @Test
    public void testFanout() {
        rabbitTemplate.convertAndSend("logs", "", "fanout 模型发送斩消息。");
    }
  • 消费者。Consumer。
package com.geek.springboot_rabbitmq.fanout;


import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class FanoutConsumer {

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,// 创建临时队列。
                    exchange = @Exchange(value = "logs", type = "fanout")// 绑定的交换机。
            )
    })
    public void receiver01(String message) {
        System.out.println("message01 = " - message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,// 创建临时队列。
                    exchange = @Exchange(value = "logs", type = "fanout")// 绑定的交换机。
            )
    })
    public void receiver02(String message) {
        System.out.println("message02 = " - message);
    }
}

  • 消费者。
package com.geek.springboot_rabbitmq.route;

import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class RouteConsumer {

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,// 创建临时队列。
                    exchange = @Exchange(value = "directs", type = "direct"),// 自定交换机名称和类型。
                    key = {"info", "error", "warn"}
            )
    })
    public void receive01(String message) {
        System.out.println("message1 = " - message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,// 创建临时队列。
                    exchange = @Exchange(value = "directs", type = "direct"),// 自定交换机名称和类型。
                    key = {"error"}
            )
    })
    public void receive02(String message) {
        System.out.println("message2 = " - message);
    }
}



topics。
  • publisher。
    // topic 动态路由。
    @Test
    public void testTopic() {
        rabbitTemplate.convertAndSend("topics", "user.save", "user.save 动态路由消息。");
    }
  • consumer。
package com.geek.springboot_rabbitmq.topic;

import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class TopicConsumer {

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,
                    exchange = @Exchange(type = "topic", name = "topics"),
                    key = {"user.save", "user.*"}
            )
    })
    public void receive01(String message) {
        System.out.println("message01 = " - message);
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue,
                    exchange = @Exchange(type = "topic", name = "topics"),
                    key = {"order.#", "product.#", "user.*"}
            )
    })
    public void receive02(String message) {
        System.out.println("message02 = " - message);
    }
}



Publisher。

  • 配置类。
package com.geek.springboot_rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";

    /**
     * 交换机。
     *
     * @return
     */
    @Bean("bootExchange")
    public Exchange bootExchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME)
                .durable(true)
                .build();
    }

    /**
     * 队列。
     *
     * @return
     */
    @Bean("bootQueue")
    public Queue bootQueue() {
        return QueueBuilder
                .durable(QUEUE_NAME)
                .build();
    }

    /**
     * 队列和交换机绑定关系。
     *
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("boot.#")
                .noargs();
    }

}

  • 发消息。
package com.geek.springboot_rabbitmq;

import com.geek.springboot_rabbitmq.config.RabbitMQConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class PublisherTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSend() {
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.haha", "hello boot mq.");
    }

}



Consumer。

package com.geek.springbootrabbitmqconsumer.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class RabbitMQListener {

    @RabbitListener(queues = "boot_queue")
    public void listenerQueue(Message message) {
        System.out.println("message = " + message);
    }

}



应用场景。

异步处理。
  • 串行 ——> 并行。——> 消息队列。

在这里插入图片描述

  • 消息队列。

在这里插入图片描述



应用解耦。

双 11 ,用户下单后,订单系统需要通知库存系统。

传统。

当库存系统故障时,订单失败。高耦合。

在这里插入图片描述



流量削峰。

秒杀活动,一般会因为流量过大,导致应用挂掉。

使用消息队列。

  • 可以控制活动人数,超过一定阀值的订单会直接丢弃。(我为啥一次秒杀都没有成功过)。

  • 防止短时间内高流量压垮应用。



RabbitMQ 集群。

主从复制。

文档。

http://previous.rabbitmq.com/v3_6_x/clustering.html

主节点 down 了,从节点不能使用。

集群。
  • 保证每个集群节点的 .erlang.cookie 一样。

How Nodes (and CLI tools) Authenticate to Each Other: the Erlang Cookie
~
On UNIX systems, the cookie will be typically located in /var/lib/rabbitmq/.erlang.cookie (used by the server) and $HOME/.erlang.cookie (used by CLI tools).
~
On Windows, the locations are C:\Users\Current User.erlang.cookie (%HOMEDRIVE% - %HOMEPATH%.erlang.cookie) or C:\Documents and Settings\Current User.erlang.cookie, and C:\Windows.erlang.cookie for RabbitMQ Windows service. If Windows service is used, the cookie should be placed in both places.

[root@localhost ~]# cat /var/lib/rabbitmq/.erlang.cookie 
EITWNAZAJEGRUIMYVWEY[root@localhost ~]# 

vim /etc/hosts
192.168.223.128 mq1
192.168.223.129 mq2
192.168.223.130 mq3

node1。

vim /etc/hostname
mq1

node2。

vim /etc/hostname
mq2

node3。

vim /etc/hostname
mq3

  • 复制 .erlang.cookie

scp /var/lib/rabbitmq/.erlang.cookie root@mq2:/var/lib/rabbitmq/.erlang.cookie

scp /var/lib/rabbitmq/.erlang.cookie root@mq3:/var/lib/rabbitmq/.erlang.cookie



Starting independent nodes ~ 启动。

不建议使用 systemctl

rabbit1$ rabbitmq-server -detached
rabbit2$ rabbitmq-server -detached
rabbit3$ rabbitmq-server -detached

  • 以这种方式启动,不会加载 plugin。所以没有图形管理界面。

  • 此时集群还没有成功。

查看状态。

rabbit1$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit1 …
[{nodes,[{disc,[rabbit@rabbit1]}]},{running_nodes,[rabbit@rabbit1]}]
…done.
rabbit2$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit2 …
[{nodes,[{disc,[rabbit@rabbit2]}]},{running_nodes,[rabbit@rabbit2]}]
…done.
rabbit3$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit3 …
[{nodes,[{disc,[rabbit@rabbit3]}]},{running_nodes,[rabbit@rabbit3]}]
…done.



Creating the cluster ~ 创建集群 ~ 子节点加入集群。

rabbitmqctl join_cluster rabbit@[节点主机名](mq1)

rabbit2$ rabbitmqctl stop_app
Stopping node rabbit@rabbit2 …done.
rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1
Clustering node rabbit@rabbit2 with [rabbit@rabbit1] …done.
rabbit2$ rabbitmqctl start_app
Starting node rabbit@rabbit2 …done.

  • 查看状态。

rabbit1$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit1 …
[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2]}]},
{running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}]
…done.
rabbit2$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit2 …
[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2]}]},
{running_nodes,[rabbit@rabbit1,rabbit@rabbit2]}]
…done.



重启集群节点。

rabbitmqctl stop_app
rabbitmqctl start_app

此时 plugins 可以使用。



查看集群节点状态。

rabbitmqctl cluster_status



镜像集群。


RabbitMQ 消息可靠投递。

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。

  • confirm 确认模式。
  • return 退回模式。

rabbitmq 整个消息投递的路径为

producer —> rabbitmq broker —> exchange —> queue —> consumer

  • confirm 确认模式。
    消息从 producer —> exchange 会返回一个 confirmCallback。
  • return 退回模式。
    消息从 exchange —> queue 投递失败会返回一个 returnCallback。

利用这两个 callBack 控制消息的可靠性投递。

package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-publisher.xml")
public class PublisherConfirmTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 确认模式。
     * <p>
     * - 开启确认模式。<rabbit:connection-factory publisher-confirms="true"/>
     * - 在 RabbitTemplate 定义 ConfirmCallBack().confirm(); 回调函数。
     */
    @Test
    public void testConfirm() {
        // 定义 ConfirmCallBack().confirm(); 回调函数。
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 定义 confirmCallBack(); 回调函数。
             * 消息从 producer ---> exchange 会返回一个 confirmCallback。
             * @param correlationData 相关配置信息。
             * @param ack exchange 交换机是否成功收到消息。
             * @param cause 失败原因。
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("confirm(); 方法执行了。。。");
                if (ack) {
                    // 接收成功。
                    System.out.println("接收消息成功 ~ " + cause);// null。
                } else {
                    // 接收失败。
                    System.out.println("接收消息失败 ~ " + cause);
                }
            }
        });

        // 发送消息。
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "confirmCallBack();.");
    }

    /**
     * 回退模式。
     * 当消息发送给 exchange 后,exchange 路由到 Queue 失败时会执行 ReturnCallback 的 returnedMessage();。
     * <p>
     * - 开启回退模式。
     * - 设置 ReturnCallback。<rabbit:connection-factory publisher-returns="true"/>
     * - 设置 Exchange 处理消息的模式。
     * - - 如果消息没有路由到 queue,则丢弃消息(默认)。
     * - - 如果消息没有路由到 queue,返回给消息发送方 ReturnCallback。
     */
    @Test
    public void testReturn() {
        // 设置交换机处理失败消息的模式。
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             * @param message 消息对象。
             * @param replyCode 错误码。
             * @param replyText 错误信息。
             * @param exchange 交换机。
             * @param routingKey 路由 key。
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("return 执行了。");
                System.out.println("message = " + message);
                System.out.println("replyCode = " + replyCode);
                System.out.println("replyText = " + replyText);
                System.out.println("exchange = " + exchange);
                System.out.println("routingKey = " + routingKey);
                /*
                return 执行了。
                message = (Body:'confirmCallBack();.' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
                replyCode = 312
                replyText = NO_ROUTE
                exchange = test_exchange_confirm
                routingKey = confirm1
                 */
            }
        });

        // 发送消息。
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm1", "confirmCallBack();.");
    }

}

在 RabbitMQ 中也提供了事务机制,但是性能较差,此处不做讲解。
使用 channel 下列方法,完成事务控制:
txSelect(); 用于将当前 channel 设置成 transaction 模式。
txCommit(); 用于提交事务。
txRollback(); 用于回滚事务。



Consumer ACK。

acknowledge

v. 承认(属实);承认(权威、地位);告知收悉

消费端收到消息后的确认方式。

三种方式。

  • 自动确认 ~ acknowledge=“none”。
    消费端收到消息立即确认。如果业务处理异常,MQ 不管。
  • 手动确认 ~ acknowledge=“manual”。
  • 根据异常情况确认 ~ acknowledge=“auto”。

其中自动确认是指,当消息一旦被 Consumer 接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,但业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式则需要在业务处理成功后,调用 channel.basicAck(); 手动签收,如果出现异常,则调用 channel.basicNack(); 方法,让其自动重新发送消息。

  • spring-rabbitmq-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 监听器对象。-->
    <!--<bean id="springQueueListener" class="com.geek.rabbitmq.listener.SpringQueueListener"/>-->
    <!-- 使用包扫描。-->
    <context:component-scan base-package="com.geek.rabbitmq.listener"/>

    <!-- 监听器窗口。绑定队列。-->
    <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
        <rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>
    </rabbit:listener-container>

</beans>

package com.geek.rabbitmq.listener;

import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class AckListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("message = " + message);
    }

    @Override
    public void containerAckMode(AcknowledgeMode mode) {

    }

    @Override
    public void onMessageBatch(List<Message> messages) {

    }

}

package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
public class ConsumerTest {

    @Test
    public void test() {
        while (true) {

        }
    }

}

package com.geek.rabbitmq.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Consumer ack 机制。
 * - 设置手动签收。<rabbit:listener-container acknowledge="manual">
 * - implements MessageListener 换成 ChannelAwareMessageListener。
 * - 消息成功处理,调用 channel 的 basicAck(); 签收。
 * - 消息成功失败,调用 channel 的 basicNack(); 拒绝签收,broker 重新发送消息给 Consumer。
 */
@Component
public class AckListener implements ChannelAwareMessageListener {

    /**
     * Callback for processing a received Rabbit message.
     * <p>Implementors are supposed to process the given Message,
     * typically sending reply messages through the given Session.
     *
     * @param message the received AMQP message (never <code>null</code>)
     * @param channel the underlying Rabbit Channel (never <code>null</code>)
     * @throws Exception Any.
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 接收转换消息。
            System.out.println(message);
            // 处理业务逻辑。
            int i = 1 / 0;
            System.out.println("处理业务逻辑。");
            // 手动签收。(deliveryTag,签收多条消息);。
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            e.printStackTrace();
            // 拒绝签收。(deliveryTag,签收多条消息,requeue ~ 消息重回队列,broker 会重新发送该消息给消费端);。
            channel.basicNack(deliveryTag, true, true);
//            channel.basicReject(deliveryTag, true);
        }
    }

    @Override
    public void onMessage(Message message) {

    }

    @Override
    public void onMessageBatch(List<Message> messages, Channel channel) {

    }

}



消费端限流。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 监听器对象。-->
    <!--<bean id="springQueueListener" class="com.geek.rabbitmq.listener.SpringQueueListener"/>-->
    <!-- 使用包扫描。-->
    <context:component-scan base-package="com.geek.rabbitmq.listener"/>

    <!-- 监听器窗口。绑定队列。-->
    <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true" acknowledge="manual" prefetch="1">

        <!--<rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>-->
        <rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>

        <!--<rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>-->
        <!--<rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
         <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
         <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
         <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
         <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>-->
    </rabbit:listener-container>

</beans>

package com.geek.rabbitmq.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Consumer 限流机制。
 * - 确保 ack 机制为手动确认。
 * - listener-container 属性 prefetch = 1
 * ~ 表示消费端每次从 mq 拉取一条消息来消费,直到手动确认消费完毕后,才会去拉取下一条消息。
 */
@Component
public class QosListener implements ChannelAwareMessageListener {

    /**
     * Callback for processing a received Rabbit message.
     * <p>Implementors are supposed to process the given Message,
     * typically sending reply messages through the given Session.
     *
     * @param message the received AMQP message (never <code>null</code>)
     * @param channel the underlying Rabbit Channel (never <code>null</code>)
     * @throws Exception Any.
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        Thread.sleep(1000);
        // 获取消息。
        System.out.println(new String(message.getBody()));
        // 业务处理逻辑。
        // 签收。
//        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    @Override
    public void onMessage(Message message) {

    }

    @Override
    public void onMessageBatch(List<Message> messages, Channel channel) {

    }

}



TTL。

Time To Live ~ 存活时间 / 过期时间。

当消息达到存活时间后,如果还没有被消费,会被自动清除。

RabbitMQ 可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

    <!-- ttl。-->
    <rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <rabbit:topic-exchange name="test_exchange_ttl">
        <rabbit:bindings>
            <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-rabbitmq-publisher.xml")
public class TTLTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * time to live.
     * - 队列统一过期。
     * - 消息单独过期。
     */
    @Test
    public void testTtl01() {
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl...");
        }
    }

    /**
     * time to live.
     * - 消息单独过期。
     */
    @Test
    public void testTtl02() {
        rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl...", new MessagePostProcessor() {
            /**
             * 消息后处理对象,设置消息参数。
             * @param message
             * @return
             * @throws AmqpException
             */
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置 message 信息。过期时间。队列改为 100s 过期,便于观察。
                message.getMessageProperties().setExpiration("10000");
                // 返回该消息。
                return message;
            }
        });
    }

    /**
     * time to live.
     * - 消息单独过期。
     * 消息过期后,只有消息在队列顶端,都会判断其是否过期,再移除。
     */
    @Test
    public void testTtl03() {
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                // 消息单独过期。10 秒后队列消息还是 10,因为 ta 不在队列顶端。
                rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl...", new MessagePostProcessor() {
                    /**
                     * 消息后处理对象,设置消息参数。
                     *
                     * @param message
                     * @return
                     * @throws AmqpException
                     */
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        // 设置 message 信息。过期时间。队列改为 100s 过期,便于观察。
                        message.getMessageProperties().setExpiration("10000");
                        // 返回该消息。
                        return message;
                    }
                });
            } else {
                rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl...");
            }
        }
    }

}



死信队列 ~ DLX。

Dead Letter Exchange ~ 死信交换机(其他有的 MQ 没有交换机这个概念),当消息成为 Dead Message 后,(这个队列如果绑定了死信交换机)可以被重新发送到另一个交换机,这个交换机就是 DLX。

消息成为死信的三种情况。

  • 队列消息长度达到限制。
  • 消费者拒绝浪费消息(basicNack / basicReject),并且不把消息重新放入原队列(requeue = false)。
  • 原队列存在消息过期设置,消息到超时时间未被消费。
  • 队列绑定死信交换机。

给队列设置参数:

x-dead-letter-exchange
x-dead-letter-routing-key

在这里插入图片描述
在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"
                               publisher-confirms="true"
                               publisher-returns="true"/>

    <!-- 定义管理交换机、队列。-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!-- 定义持久化队列,不存在则自动创建。
    不绑定到交换机则默认绑定到默认交换机 direct,名称为:"",路由键为队列名称。
    -->
    <!--
        id ~ bean 的名称。
        name ~ queue 的名称。
        auto-declare ~ 自动创建。
        auto-delete ~ 自动删除。最后一个消费者和该队列断开连接后,自动删除队列。
        exclusive ~ 是否独占。
        durable ~ 是否持久化。
    -->
    <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>

    <!-- ~ ~ ~ ~ ~ ~ ~ ~ 广播;所有队列都能收到消息。 ~ ~ ~ ~ ~ ~ ~ ~ -->
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>

    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>

    <!-- 定义广播类型交换机,并绑定上述两个队列。-->
    <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding queue="spring_fanout_queue_1"/>
            <rabbit:binding queue="spring_fanout_queue_2"/>
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!--<rabbit:direct-exchange name="aa">-->
    <!--<rabbit:bindings>-->
    <!--&lt;!&ndash; direct 类型的交换机绑定队列。key ~ 路由 key。queue ~ 队列名称。&ndash;&gt;-->
    <!--<rabbit:binding queue="spring_queue" key="xxx"/>-->
    <!--</rabbit:bindings>-->
    <!--</rabbit:direct-exchange>-->

    <!-- ~ ~ ~ ~ ~ ~ ~ ~ 通配符;* 匹配一个单词,# 匹配多个单词。~ ~ ~ ~ ~ ~ ~ ~-->
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/>
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/>

    <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="heima.*" queue="spring_topic_queue_star"/>
            <rabbit:binding pattern="heima.#" queue="spring_topic_queue_well"/>
            <rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 定义 rabbitTemplate 对象操作可以在代码中方便发送消息。-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!-- 消息可靠性投递。(生产端)。-->
    <rabbit:queue id="test_queue_confirm" name="test_queue_confirm"/>
    <rabbit:direct-exchange name="test_exchange_confirm">
        <rabbit:bindings>
            <rabbit:binding queue="test_queue_confirm" key="confirm"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!-- ttl。-->
    <rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="1000000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <rabbit:topic-exchange name="test_exchange_ttl">
        <rabbit:bindings>
            <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 死信队列。
        - 正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)。
        - 死信队列(queue_dlx)和死信交换机(exchange_dlx)。
        - 正常队列绑定死信交换机。
            参数。
                x-dead-letter-exchange      死信交换机名称。
                x-dead-letter-routing-key   发送给死信交换机的 routing key。
    -->
    <!-- 正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)。-->
    <rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
        <!-- 正常队列绑定死信交换机。-->
        <rabbit:queue-arguments>
            <!-- x-dead-letter-exchange ~ 死信交换机名称。-->
            <entry key="x-dead-letter-exchange" value="exchange_dlx"/>
            <!-- x-dead-letter-routing-key ~发送给死信交换机的 routing key。-->
            <entry key="x-dead-letter-routing-key" value="dlx.geek"/>

            <!-- 设置消息过期时间 ttl。-->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
            <!-- 设置消息队列的长度限制。。-->
            <entry key="x-max-length" value="10" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="test_exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    <!-- 死信队列(queue_dlx)和死信交换机(exchange_dlx)。-->
    <rabbit:queue name="queue_dlx" id="queue_dlx"/>
    <rabbit:topic-exchange name="exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.#" queue="queue_dlx"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

</beans>

package com.geek.rabbitmq.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class DlxListener implements ChannelAwareMessageListener {

    /**
     * Callback for processing a received Rabbit message.
     * <p>Implementors are supposed to process the given Message,
     * typically sending reply messages through the given Session.
     *
     * @param message the received AMQP message (never <code>null</code>)
     * @param channel the underlying Rabbit Channel (never <code>null</code>)
     * @throws Exception Any.
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 接收转换消息。
            System.out.println(message);
            // 处理业务逻辑。
            int i = 1 / 0;
            System.out.println("处理业务逻辑。");
            // 手动签收。(deliveryTag,签收多条消息);。
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("出现异常,拒绝接收。");
            // 不重回队列,到死信队列中。
            // 拒绝签收。(deliveryTag,签收多条消息,requeue ~ 消息重回队列,broker 会重新发送该消息给消费端);。
            channel.basicNack(deliveryTag, true, false);
//            channel.basicReject(deliveryTag, true);
        }
    }

    @Override
    public void onMessage(Message message) {

    }

    @Override
    public void onMessageBatch(List<Message> messages, Channel channel) {

    }

}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 监听器对象。-->
    <!--<bean id="springQueueListener" class="com.geek.rabbitmq.listener.SpringQueueListener"/>-->
    <!-- 使用包扫描。-->
    <context:component-scan base-package="com.geek.rabbitmq.listener"/>

    <!-- 监听器窗口。绑定队列。-->
    <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true" acknowledge="manual" prefetch="1">

        <!--<rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>-->
        <!--<rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>-->
        <!-- 定义监听器,监听正常的队列。-->
        <rabbit:listener ref="dlxListener" queue-names="test_queue_dlx"/>

        <!--<rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>-->
        <!--<rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
         <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
         <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
         <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
         <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>-->
    </rabbit:listener-container>

</beans>

package com.geek.rabbitmq.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class DlxListener implements ChannelAwareMessageListener {

    /**
     * Callback for processing a received Rabbit message.
     * <p>Implementors are supposed to process the given Message,
     * typically sending reply messages through the given Session.
     *
     * @param message the received AMQP message (never <code>null</code>)
     * @param channel the underlying Rabbit Channel (never <code>null</code>)
     * @throws Exception Any.
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 接收转换消息。
            System.out.println(message);
            // 处理业务逻辑。
            int i = 1 / 0;
            System.out.println("处理业务逻辑。");
            // 手动签收。(deliveryTag,签收多条消息);。
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("出现异常,拒绝接收。");
            // 不重回队列,到死信队列中。
            // 拒绝签收。(deliveryTag,签收多条消息,requeue ~ 消息重回队列,broker 会重新发送该消息给消费端);。
            channel.basicNack(deliveryTag, true, false);
//            channel.basicReject(deliveryTag, true);
        }
    }

    @Override
    public void onMessage(Message message) {

    }

    @Override
    public void onMessageBatch(List<Message> messages, Channel channel) {

    }

}

package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * 测试死信消息。
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-publisher.xml")
public class DlxTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 测试死信 ~ 过期时间。
     * // 一开始 test_exchange_dlx 有一条消息。
     * 10s 后 test_queue_dlx 消息到 queue_dlx。
     */
    @Test
    public void testDlx() {
        rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.geek", "我是一条消息,我会死吗。");
    }

    /**
     * 测试死信 ~ 长度限制。
     * // 10 条消息进入 test_exchange_dlx。
     * 10 条消息进入 queue_dlx。
     * 10 s 后全部到 queue_dlx。
     */
    @Test
    public void testDlx2() {
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.geek", "我是一条消息,我会死吗。");
        }
    }

    /**
     * 消息拒收。
     * <p>
     * queue_dlx 消息 +1。
     */
    @Test
    public void testDlx3() {
        rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.geek", "我是一条消息,我会死吗。");
    }

}



延迟队列。

即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

下单后,30 分钟未支付,取消订单,回滚库存。
新用户注册 7 天后,发送短信问候。

实现方式。

  • 定时器。
  • 延迟队列。

在这里插入图片描述
RabbitMQ 没有提供延迟队列功能。

↓ ↓ ↓

TTL + 死信队列组合实现延迟队列效果。

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"
                               publisher-confirms="true"
                               publisher-returns="true"/>

    <!-- 定义管理交换机、队列。-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!-- 定义持久化队列,不存在则自动创建。
    不绑定到交换机则默认绑定到默认交换机 direct,名称为:"",路由键为队列名称。
    -->
    <!--
        id ~ bean 的名称。
        name ~ queue 的名称。
        auto-declare ~ 自动创建。
        auto-delete ~ 自动删除。最后一个消费者和该队列断开连接后,自动删除队列。
        exclusive ~ 是否独占。
        durable ~ 是否持久化。
    -->
    <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>

    <!-- ~ ~ ~ ~ ~ ~ ~ ~ 广播;所有队列都能收到消息。 ~ ~ ~ ~ ~ ~ ~ ~ -->
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>

    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>

    <!-- 定义广播类型交换机,并绑定上述两个队列。-->
    <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding queue="spring_fanout_queue_1"/>
            <rabbit:binding queue="spring_fanout_queue_2"/>
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!--<rabbit:direct-exchange name="aa">-->
    <!--<rabbit:bindings>-->
    <!--&lt;!&ndash; direct 类型的交换机绑定队列。key ~ 路由 key。queue ~ 队列名称。&ndash;&gt;-->
    <!--<rabbit:binding queue="spring_queue" key="xxx"/>-->
    <!--</rabbit:bindings>-->
    <!--</rabbit:direct-exchange>-->

    <!-- ~ ~ ~ ~ ~ ~ ~ ~ 通配符;* 匹配一个单词,# 匹配多个单词。~ ~ ~ ~ ~ ~ ~ ~-->
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/>
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
    <!-- 定义广播交换机中的持久化队列,不存在则自动创建。-->
    <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/>

    <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="heima.*" queue="spring_topic_queue_star"/>
            <rabbit:binding pattern="heima.#" queue="spring_topic_queue_well"/>
            <rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 定义 rabbitTemplate 对象操作可以在代码中方便发送消息。-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!-- 消息可靠性投递。(生产端)。-->
    <rabbit:queue id="test_queue_confirm" name="test_queue_confirm"/>
    <rabbit:direct-exchange name="test_exchange_confirm">
        <rabbit:bindings>
            <rabbit:binding queue="test_queue_confirm" key="confirm"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!-- ttl。-->
    <rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="1000000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <rabbit:topic-exchange name="test_exchange_ttl">
        <rabbit:bindings>
            <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 死信队列。
        - 正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)。
        - 死信队列(queue_dlx)和死信交换机(exchange_dlx)。
        - 正常队列绑定死信交换机。
            参数。
                x-dead-letter-exchange      死信交换机名称。
                x-dead-letter-routing-key   发送给死信交换机的 routing key。
    -->
    <!-- 正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)。-->
    <rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
        <!-- 正常队列绑定死信交换机。-->
        <rabbit:queue-arguments>
            <!-- x-dead-letter-exchange ~ 死信交换机名称。-->
            <entry key="x-dead-letter-exchange" value="exchange_dlx"/>
            <!-- x-dead-letter-routing-key ~发送给死信交换机的 routing key。-->
            <entry key="x-dead-letter-routing-key" value="dlx.geek"/>

            <!-- 设置消息过期时间 ttl。-->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
            <!-- 设置消息队列的长度限制。。-->
            <entry key="x-max-length" value="10" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="test_exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    <!-- 死信队列(queue_dlx)和死信交换机(exchange_dlx)。-->
    <rabbit:queue name="queue_dlx" id="queue_dlx"/>
    <rabbit:topic-exchange name="exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.#" queue="queue_dlx"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 延迟队列。
            正常交换机(order_exchange)和队列(order_queue)。
            死信交换机(order_exchange_dlx)和队列(order_queue_dlx)。
            绑定 ~ 设置正常队列的过期时间为 30 分钟。
    -->
    <!-- 正常交换机(order_exchange)和队列(order_queue)。-->
    <rabbit:queue id="order_queue" name="order_queue">
        <!-- 绑定 ~ 设置正常队列的过期时间为 30 分钟。-->
        <rabbit:queue-arguments>
            <entry key="x-dead-letter-exchange" value="order_exchange_dlx"/>
            <entry key="x-dead-letter-routing-key" value="dlx.order.cancel"/>
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="order_exchange">
        <rabbit:bindings>
            <rabbit:binding pattern="order.#" queue="order_queue"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 死信交换机(order_exchange_dlx)和队列(order_queue_dlx)。-->
    <rabbit:queue id="order_queue_dlx" name="order_queue_dlx"/>
    <rabbit:topic-exchange name="order_exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.order.#" queue="order_queue_dlx"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
</beans>

  • 生产者。
package com.geek;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * 测试延时队列。
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-publisher.xml")
public class DelayTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testDelay() throws InterruptedException {
        // 发送订单消息。在订单系统中,下单成功后,发送消息。
        rabbitTemplate.convertAndSend("order_exchange", "order.msg", "订单信息:id = 1.");

        // 打印倒计时 10 s。
        for (int i = 10; i > 0; i--) {
            System.out.println(i);
            Thread.sleep(1000);
        }
    }

}

  • 消费者。
package com.geek.rabbitmq.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class OrderListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 接收转换消息。
            System.out.println(message);
            // 处理业务逻辑。
            System.out.println("处理业务逻辑。");
            System.out.println("根据订单 id 查询其状态。");
            System.out.println("判断状态是否为支付成功。");
            System.out.println("取消订单,回滚库存。");
            // 手动签收。(deliveryTag,签收多条消息);。
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("出现异常,拒绝接收。");
            // 不重回队列,到死信队列中。
            // 拒绝签收。(deliveryTag,签收多条消息,requeue ~ 消息重回队列,broker 会重新发送该消息给消费端);。
            channel.basicNack(deliveryTag, true, false);
//            channel.basicReject(deliveryTag, true);
        }
    }

    @Override
    public void onMessage(Message message) {

    }

    @Override
    public void onMessageBatch(List<Message> messages, Channel channel) {

    }

}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 加载配置文件。-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义 RabbitMQ ConnectionFactory。-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 监听器对象。-->
    <!--<bean id="springQueueListener" class="com.geek.rabbitmq.listener.SpringQueueListener"/>-->
    <!-- 使用包扫描。-->
    <context:component-scan base-package="com.geek.rabbitmq.listener"/>

    <!-- 监听器窗口。绑定队列。-->
    <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true" acknowledge="manual"
                               prefetch="1">

        <!--<rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>-->
        <!--<rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>-->
        <!-- 定义监听器,监听正常的队列。-->
        <!--<rabbit:listener ref="dlxListener" queue-names="test_queue_dlx"/>-->
        <!-- 延迟队列效果实现。监听死信队列。-->
        <rabbit:listener ref="orderListener" queue-names="order_queue_dlx"/>

        <!--<rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>-->
        <!--<rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
         <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
         <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
         <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
         <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>-->
    </rabbit:listener-container>

</beans>



日志与监控。

RabbitMQ 日志默认位置:/var/log/rabbitmq/rabbit@xxx.log

日志包含了 RabbitMQ 的版本号、Erlang 的版本号、RabbitMQ 服务节点名称、cookie 的 hash 值、RabbitMQ 配置文件地址、内存限制、磁盘限制、默认账户 guest 的创建以及权限配置等等。

Web 管控台。

ip:15672

rabbitctl 管理和监控。


消息可靠性分析与追踪。

在便用任何消息中间件的过程中,难免会出现某条消息异常丢失的情况。对于 RabbitMQ 而言,可能是因为生产者或消费者与 RabbitMQ 断开了连接,而它们与 RabbitMQ 又采用了不同的确认机制;也有可能是因为交焕器与队列之间不同的转发策略;甚至是交换器并没有与任何队列进行绑定,生产者又不感知或者没有采取相的措施;另外 RabbitMQ 本身的集群策略也可能导致消息的丢失。这个时候就需要有一个较好的机制跟踪记录清息的投递过程,以此协助开发和运维人员进行问题的定位。

在 RabbitMQ 中可以使用 Firehose 和 rabbitmq_tracing 插件功能来实现消息追踪。

消息追踪 ~ Firehose。

Firehose 的机制是将生产者投递给 RabbitMQ 的消息、RabbitMQ 投递给消费者的消息按照指定的格式发送到默认的 exchange 上。这个默认的 exchange 名称为 amq.rabbitmq.trace,ta 是一个 topic 类型的exchange。发送到这个 exchange 上的消息的 routingkey 为 publish.exchangename 和 deliver.queuenamne。其中 exhcnagename 和 queuename 为实际 exchange 和 queue 的名称,分别对应生产者投递到 exchange 的消息,和消费者从 queue 上获取的消息。

注意:打开 trace 会影响消息写入功能。适当打开后请关闭。

rabbitmqctl trace_on ~ 开启 Firehose。
rabbitmqctl trace_off ~ 关闭 Firehose。

在这里插入图片描述

将 amqp.rabbit.trace 交换机绑定到队列上,并开启 trace 功能。

[geek@localhost ~]$ sudo rabbitmqctl trace_on
[sudo] password for geek: 
Starting tracing for vhost "/" ...
Trace enabled for vhost /

再从队列中拿消息,会有不止一条消息。

在这里插入图片描述



消息追踪 ~ rabbitmq_tracing。

rabbitmq_tracing 和 firehose 在实现上如出一辙,只不过 rabbitmq_tracing 的方式比 Firehose 多了一层 GUI 的包装,更容易便用和管理。

  • 启用插件。

rabbitmq-plugins enable rabbitmq_tracing

在 admin 下右侧会多一个 Tracing。

在这里插入图片描述

发送消息后 mylog 中会记录。

================================================================================
2020-11-08 3:11:27:779: Message published

Node:         rabbit@localhost
Connection:   <rabbit@localhost.1604690264.4436.2>
Virtual host: /
User:         geek
Channel:      1
Exchange:     
Routing keys: [<<"test_trace">>]
Routed queues: [<<"test_trace">>]
Properties:   [{<<"delivery_mode">>,signedint,1},{<<"headers">>,table,[]}]
Payload: 
hahahah

性能受影响。



管理。



消息可靠性问题 ~ 消息补偿机制。



消息幂等性问题 ~ 乐观锁解决方案。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lyfGeek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值