RabbitMQ 学习笔记

相较于 Kafka,RabbitMQ学习更简单了一些,官网上的7大金刚,也就是七个例子做一遍大概就点感觉了,我把一些细节记录一下。

背景知识

为什么要使用消息中间件以及使用场景

传统模式的缺点:

  • 系统间耦合性太强
  • 一些非必要的业务逻辑以同步的方式运行,太耗费时间。
  • 并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

中间件模式的的优点(异步 解耦 流量削峰):

  • 将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。
  • 系统根据数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。

使用场景:
消息队列,是分布式系统中重要的组件,其通用的使用场景可以简单地描述为:当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候

在项目中,将一些无需即时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。

AMQP & JMS

MQ是消费者-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取或者订阅队列中的消息。
MQ 和 JMS类似,但不同的是JMS是SUN JAVA消息中间件服务的一个标准和API定义,而MQ则是遵循了AMQP协议的具体实现和产品

  1. AMQP ,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。

  2. JMS ,Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。 Java消息服务 是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。常见的消息队列,大部分都实现了JMS API,如 ActiveMQ , Redis 以及 RabbitMQ 等。

为什么使用 RabbitMQ

RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如: Python 、 Ruby 、 .NET 、 Java 、 JMS 、 C 、 PHP 、 ActionScript 、 XMPP 、 STOMP 等,支持 AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗

Kafka 常用于日志收集,性能特别好,可靠性这点待议

RocketMQ 性能好可靠性也强,但是有些功能不开源,需要购买商业版

七大金刚

HelloWorld

就是一个简单的单生产者单消费者通过一个Queue传递消息的实例。
在这里插入图片描述

/*
 * 简单队列 消息生产者
 * */
class Send {
    //    定义队列名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        // 创建连接工厂 连接工厂 -- 连接 -- 信道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.125.71");
        factory.setUsername("guest");
        factory.setVirtualHost("/");
        factory.setPassword("guest");
        factory.setPort(5672);

        try (
//                连接工厂创建连接
                Connection connection = factory.newConnection();
//                创建信道
                Channel channel = connection.createChannel()) {
            /**
             * 声明队列
             * 第一个参数queue:队列名称
             *
             * 第二个参数durable:是否持久化
             *
             * 第三个参数Exclusive:排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。
             * 这里需要注意三点:
             * 1. 排他队列是基于连接可见的,同一连接的不同通道是可以同时访问同一个连接创建的排他队列的。
             * 2. "首次",如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同。
             * 3. 即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的。
             * 这种队列适用于只限于一个客户端发送读取消息的应用场景。
             *
             * 第四个参数Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。
             * 这种队列适用于临时队列。
             */

            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Hello World!";
            // 第一个参数是交换机
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}
/*
 * 简单队列 消费者
 * */
public class Recv {
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] argv) throws Exception {
        // 创建连接工厂 连接工厂 -- 连接 -- 信道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.125.71");
        factory.setUsername("guest");
        factory.setVirtualHost("/");
        factory.setPassword("guest");
        factory.setPort(5672);
        // 创建连接,连接中创建信道
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
//        信道绑定队列 是否持久化 排他队列 是否自动删除
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        // 接受消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}
  • 生产者的 channel 用完就没有了,但是消费者需要一直监听队列,所以连接和信道还保留着

轮询/公平模式

现在是多个消费者,以轮询的方式进行消费,这里不涉及到订阅,或者说全部都订阅。

在这里插入图片描述
Producer 是不变的,现在是有多个 consumer 的 channel 绑定同一个 queue,代码实际上也是没有什么变化的,但是为了模拟这个不同速度的消费者,加入了 sleep。

public class Recv1 {
	// 不允许声明同名队列,会返回 error
    private final static String QUEUE_NAME = "work_rr";

    public static void main(String[] argv) throws Exception {
        // 创建连接工厂 连接工厂 -- 连接 -- 信道
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.125.71");
        factory.setUsername("guest");
        factory.setVirtualHost("/");
        factory.setPassword("guest");
        factory.setPort(5672);

        // 创建连接,连接中创建信道
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
//        信道绑定队列 是否持久化 排他队列 是否自动删除
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        // 接受消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            // 模拟消费耗时
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            // 关闭 channel.basicConsume 的自动回执后 进行手动确认
            // 最后一个参数multiple 指收到多条要不要一条条确认
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

        };
        // 监听队列消费信息 自动回执
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}

与上面不同的一点在于,关闭了消费者的自动确认,改为手动确认。官方文档中有对确认机制介绍:

In order to make sure a message is never lost, RabbitMQ supports message acknowledgments. An acknowledgement is sent back by the consumer to tell RabbitMQ that a particular message has been received, processed and that RabbitMQ is free to delete it.

  • 消息确认机制确保 worker 的意外死亡不会导致消息lost,生产者会重新发布没有收到确认的信息到队列中。

We have learned how to make sure that even if the consumer dies, the task isn’t lost. But our tasks will still be lost if RabbitMQ server stops.

When RabbitMQ quits or crashes it will forget the queues and messages unless you tell it not to. Two things are required to make sure that messages aren’t lost: we need to mark both the queue and messages as durable.

First, we need to make sure that the queue will survive a RabbitMQ node restart. In order to do so, we need to declare it as durable:

channel.queueDeclare("hello", durable, false, false, null);
channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN, // 
            message.getBytes());
  • 可以在声明队列的时候 开启 Message durability,在 RabbitMQ 重启的情况下,降低(不是保证) 队列信息丢失的概率,但是这个只是保存在 cache 中,可能没有写到硬盘中,如果想保证完全的安全,可以使用 punisher confirm 发布者确认
  • 轮询模式存在问题:在不同 worker 或者说是 consumer 速度不同的情况下,会分配给相同数量的任务,快的 worker 闲置了,慢的worker还有堆积,因此可以开启 Fair Dispatch,限制消费者接收消息的数量 注意,basecQos 只有在消费者自动确认关闭,手动确认开启的情况下才生效
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            int prefetchCount = 1;
            // 限制消费者每次只能接受1条消息,处理完才能接受下一条
            channel.basicQos(prefetchCount);

Publish/Subscribe 发布订阅模式 广播模式

在这里插入图片描述

当然以上的模式不能满足我们的需求,比如我们现在想发布一条消息,所有的消费者都能收到,那么就需要引入发布,订阅模式,或者说广播模式,这个也是Kafka 默认的模式。

这里也引入了一个新的概念,交换机 exchange

public class EmitLog {
    // 这里声明的是交换机的名字
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.125.71");
        factory.setUsername("guest");
        factory.setVirtualHost("/");
        factory.setPassword("guest");
        factory.setPort(5672);
        // 创建连接
        try (Connection connection = factory.newConnection();
             // 创建信道
             Channel channel = connection.createChannel()) {
            // 绑定交换机
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

            String message = argv.length < 1 ? "info: Hello World!" :
                    String.join(" ", argv);

            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

发布订阅模式,每个消费者有一个通道自动生成的排他队列,绑定到交换机上,如果不是排他队列,我们需要一个一个解绑十分麻烦。

public class ReceiveLogs1 {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.125.71");
        factory.setUsername("guest");
        factory.setVirtualHost("/");
        factory.setPassword("guest");
        factory.setPort(5672);

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        String queueName = channel.queueDeclare().getQueue();
        channel.queueBind(queueName, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

交换机没有存储信息的能力,只有队列有,这点非常关键,因此如果发布信息的时候队列没有绑定到交换机上,就收不到了,RabbitMQ似乎是不能 from beginning 获取历史的消息的

Routing 路由模式

我们想给每个人发送内容,但是不同的人(会员和非会员)能收到不同的内容,或者只给特殊的人发送内容
在这里插入图片描述
我们需要给消息加入 key 来控制这一个过程

       // 通过工厂创建连接
            connection = factory.newConnection();
            // 获取通道

            channel = connection.createChannel();
            // 绑定交换机 direct:路由模式
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            // 创建消息,模拟收集不同级别日志
            String infoMessage = "INFO消息";
            String warningMessage = "WARNING消息";
            String errorMessage = "ERROR消息";
            // 设置路由routingKey
            String routingKey = "info";
            String warningroutingKey = "warning";
            String errorroutingKey = "error";
            // 将产生的消息发送至交换机
            channel.basicPublish(EXCHANGE_NAME, routingKey, null,
                    infoMessage.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + infoMessage + "'");
            channel.basicPublish(EXCHANGE_NAME, warningroutingKey, null,
                    warningMessage.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + warningMessage + "'");
            channel.basicPublish(EXCHANGE_NAME, errorroutingKey, null,
                    errorMessage.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + errorMessage + "'");

给消费者队列绑定不同的 routingKey 就能接收对应的 key 的信息,当然,可以绑定多个 key

    		// 绑定交换机 direct:路由模式
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            // 获取队列名称
            String queueName = channel.queueDeclare().getQueue();
            // 设置路由routingKey
            String routingKeyInfo = "info";
            String routingKeyWarning = "warning";
            // 绑定队列
            channel.queueBind(queueName, EXCHANGE_NAME, routingKeyInfo);
            channel.queueBind(queueName, EXCHANGE_NAME, routingKeyWarning);

Topics 主题队列(重要)

使用 通配符 替代 精确的 key 可以降低发送接收难度,是比较常用的用法。

// 发送方是精确的 key
 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
 String routingKey = "select.goods.byId";
 channel.basicPublish(EXCHANGE_NAME, routingKey, null,infoMessage.getBytes("UTF-8"));
// 接收方是通配符写的 key
 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); 
 // 设置路由routingKey
 String routingKey = "select.goods.*";
 // 绑定队列
 channel.queueBind(queueName, EXCHANGE_NAME, routingKey);

RPC (比较少用)

在这里插入图片描述

RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。

具体步骤:

  • 当客户端启动时,创建一个匿名的回调队列。
  • 客户端为RPC请求设置2个属性:replyTo,设置回调队列名字,我们需要用这个队列接收server返回的消息;correlationId,标记request,可能同时有多个返回,我们要用这个 id 区分。
  • 请求被发送到 rpc_queue 队列中。
  • RPC服务器端(提前启动的)监听 rpc_queue 队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就replyTo设定的回调队列。
  • 客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了

RPC模式 和 RabbitMQ 强调的异步有理念上的冲突,所以是不常用的。

至少我看的视频上是这样说的。

直接抄的代码,我也没细看。

public class RPCServer {

    private static final String RPC_QUEUE_NAME = "rpc_queue";

    private static int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
            channel.queuePurge(RPC_QUEUE_NAME);

            channel.basicQos(1);

            System.out.println(" [x] Awaiting RPC requests");

            Object monitor = new Object();
            // 接收
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();

                String response = "";

                try {
                    String message = new String(delivery.getBody(), "UTF-8");
                    int n = Integer.parseInt(message);

                    System.out.println(" [.] fib(" + message + ")");
                    response += fib(n);
                } catch (RuntimeException e) {
                    System.out.println(" [.] " + e.toString());
                } finally {
                    // 发送 
                    channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // RabbitMq consumer worker thread notifies the RPC server owner thread
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            };

            channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
public class RPCClient implements AutoCloseable {

    private Connection connection;
    private Channel channel;
    private String requestQueueName = "rpc_queue";

    public RPCClient() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        connection = factory.newConnection();
        channel = connection.createChannel();
    }

    public static void main(String[] argv) {
        try (RPCClient fibonacciRpc = new RPCClient()) {
            for (int i = 0; i < 32; i++) {
                String i_str = Integer.toString(i);
                System.out.println(" [x] Requesting fib(" + i_str + ")");
                String response = fibonacciRpc.call(i_str);
                System.out.println(" [.] Got '" + response + "'");
            }
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String call(String message) throws IOException, InterruptedException {
        final String corrId = UUID.randomUUID().toString();

        String replyQueueName = channel.queueDeclare().getQueue();
        // 把 corrId replyQueueName 携带
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();

        channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

        final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
		// 同时也接收消息
        String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
            // 比较 corrId
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                response.offer(new String(delivery.getBody(), "UTF-8"));
            }
        }, consumerTag -> {
        });

        String result = response.take();
        channel.basicCancel(ctag);
        return result;
    }

    public void close() throws IOException {
        connection.close();
    }
}

Publisher Confirm 发布者确认模式(重要)

默认情况下,我们不知道生产者发送的消息有没有到达队列,可以使用事务,通过AMQP协议层面为我们提供了事务机制解决了这个问题,但是采用事务机制实现会降低 RabbitMQ的消息吞吐量,但是类似Redis的事务,性能比较弱,似乎不被广泛使用,除了AMQP协议层面能够实现消息事物控制外,我们还有第二种方式 即:Publisher Confirm模式。

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始)一旦消息被投递到所有匹配的队列之后,broker(似乎没提到这个词,大概和Kafka 中的 broker一个意思吧,就是一台服务器吧)就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消 息都已经得到了处理

confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。

在channel 被设置成 confirm 模式之后,所有被 publish 的后续消息都将被 confirm(即 ack) 或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被 confirm又被 nack 。

注意:两种事务控制形式不能同时开启!

confirm 模式 还分为 同步异步 两种模式,同步的意义不是很大,降低了性能,违背了 RabbitMQ 异步的优点

异步confirm模式的编程实现最复杂,Channel对象提供的 ConfirmListener() 回调方法 只包含 deliveryTag (当前Chanel发出的消息序号),我们需要自己为每一个Channel维护一个 unconfirm 的消息序号集合每 publish一条数据,集合中元素加1,每回调一次 handleAck 方法, unconfirm 集 合删掉相应的一条 (multiple=false) 或多条 (multiple=true) 记录。 从程序运行效率上看,这 个 unconfirm 集合最好采用有序集合SortedSet存储结构。实际上, waitForConfirms() 方法也是通过SortedSet维护消息序号的。

异步模式的优点就是执行效率高,不需要等待消息执行完,只需要监听消息即可。

Publisher confirms are enabled at the channel level with the confirmSelect method:

// 从信道开启的	
Channel channel = connection.createChannel();
channel.confirmSelect();

This method must be called on every channel that you expect to use publisher confirms. Confirms should be enabled just once, not for every message published.

 static void handlePublishConfirmsAsynchronously() throws Exception {
        try (Connection connection = createConnection()) {
            Channel ch = connection.createChannel();

            String queue = UUID.randomUUID().toString();
            ch.queueDeclare(queue, false, false, true, null);
			// 开启 confirm 模式
            ch.confirmSelect();
			// 创建一个存储 消息序号 和对应消息 的 map
			// 会在发送消息时一一put
            ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
			// 这个是把对 ack 进行的处理 抽取出来
            ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
                if (multiple) {
                	// 批量确认 从map中取出
                    ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
                            sequenceNumber, true
                    );
                    confirmed.clear();
                } else {// 单个取出
                    outstandingConfirms.remove(sequenceNumber);
                }
            };
			// 把 定制化的 ack 和 nak 处理加入 ConfirmListener
			// 把 ConfirmListener 加入 channel
            ch.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
                String body = outstandingConfirms.get(sequenceNumber);
                System.err.format(
                        "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
                        body, sequenceNumber, multiple
                );// 然后还是从map中取出,这个你根据自己的逻辑可以进一步处理
cleanOutstandingConfirms.handle(sequenceNumber, multiple);
            });

            long start = System.nanoTime();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String body = String.valueOf(i);
                outstandingConfirms.put(ch.getNextPublishSeqNo(), body);
                ch.basicPublish("", queue, null, body.getBytes());
            }

            if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
                throw new IllegalStateException("All messages could not be confirmed in 60 seconds");
            }

            long end = System.nanoTime();
            System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
        }
    }

详细代码详见官网给的例子吧
https://www.rabbitmq.com/tutorials/tutorial-seven-java.html

汇总

  • 简单的点对点模式中,生产者的 channel 用完就没有了,但是消费者需要一直监听队列,所以连接和信道还保留着,这点可以在 RabbitMQ Manager 上进行验证。

  • 不允许声明同名queue,会返回 error

  • 消息确认机制 确保 worker 的意外死亡不会导致消息lost,生产者会重新发布没有收到确认的信息到队列中。但是这样会有重复消费问题吧,根据手动提交ack的时间的不同,甚至可能有漏消费问题吧?这点是怎么解决的?

  • 可以在声明队列的时候开启 Message durability,在 RabbitMQ 重启的情况下,**降低(不是保证)**队列信息丢失的概率,但是这个只是保存在 cache 中,可能没有写到硬盘中,如果想保证完全的安全,可以使用 publisher confirm 发布者确认

  • 轮询模式存在问题:在不同 worker 或者说是 consumer 速度不同的情况下,会分配给相同数量的任务,快的 worker闲置了,慢的worker还有堆积,因此可以开启 Fair Dispatch,限制消费者接收消息的数量 注意,basecQos 只有在消费者自动确认关闭,手动确认开启的情况下才生效

  • 发布订阅模式(fanout),我们呢声明的是交换机 Exchange,每个消费者有一个通道自动生成的排他队列,绑定到交换机上,如果不是排他队列,我们需要一个一个解绑十分麻烦,排他队列自动解绑删除

  • 发布订阅模式(fanout)中使用到的交换机没有存储信息的能力,只有队列有,因此如果发布信息的时候队列没有绑定到交换机上,就收不到了

  • 路由模式 中给消费者队列绑定不同的 routingKey 就能接收携带对应的 key 的信息

  • 主题模式 路由模式存在问题:过多的key会让消息发送非常麻烦,主题模式中 使用通配符 替代精确的 key 可以解决这个麻烦

  • 主题模式 可以替代以上所有模式

  • 发布者确认模式 默认情况下,我们不知道生产者发送的消息有没有到达队列,可以使用事务,但是类似Redis的事务,性能比较弱,似乎不被广泛使用。一般采用 异步的确认模式给发送的每一条消息一个序号,使用 ConfirmListener() 监听返回的确认消息,同时维护一个 Unconfirm 有序集合 SortedSet用于保存没有确认的序号。

  • 简单说以上用到的安全级别有三种

    • 半裸奔,因为无论如何 消费者 ack 都是开启的,自动或手动,类似kafka 的提交 offset,但是区别在于 kafka 是默认持久化的,但是RabbitMQ 不开启就没有。
    • 声明队列的时候开启 Message durability,会将消息持久化先放到 cache,再择时放到硬盘,这个和 Kafka 的区别在于消息被正常消费完,还是会删除的,kafka 不会,有一个默认7天的存储时间。
    • Publisher confirm 发布者确认,确认每个消息是否被发送到了队列中,如果没有会重新发送。

啊,刚发现 channel 有的地方写的通道,有的地方是信道,懒得改了,虽然俺转行了,也毕竟在通信这么多年,虽然啥也没学会,但是信道这个词是我最后的倔强。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值