rabbitMq

731 阅读12分钟

rabbitmq的基础知识

参考资料: 很丰富的面试题

rabbitmq.png

基本概念

broker: 实体服务器 VirtualHost:缩小版的RabbitMq服务器,拥有自己独立的交换器、消息队列和相关的对象 Exchange:接受生产者的消息,并将这些消息路由到具体的Queue中 Binding:Exchange和Queue之间的关联 Queue:用来保存消息直到发送给消费者,它是消息的容器。 Channel:多路复用连接中的独立的双向数据流通道,因为建立和销毁TCP的Connection开销

rabbitmq对消息的保存方式

  1. disk 后缀.rdp a. 在发送时指定需要持久化或者服务器内存紧张时会将部分中的内存消息保存到磁盘中 b. 单个文件增加到16M后会生成新的文件 c. 文件中的消息被标记删除的比例达到阈值时会触发文件的合并,提高磁盘的利用率
  2. RAM,内存保存,效率高

Exchange 类型

订阅模式(Fanout Exchange):

会将消息放到所有绑定到该exchange的队列上

rabbitMq_fanOut.png

public class ConnectionUtils {

    private static Connection connection;
    private static String lock = "aaa";

  /**
     * 获取rabbitmq连接
     * @return
     */
    public static Connection getConnection() {
        if (null != connection) {
            return connection;
        }
        synchronized (lock) {
            if (null != connection) {
                return connection;
            }

            ConnectionFactory connectionFactory = new ConnectionFactory();
            connectionFactory.setHost("host");
            connectionFactory.setUsername("userName");
            connectionFactory.setPassword("password");
            connectionFactory.setVirtualHost("/vhost");
            try {
                connection = connectionFactory.newConnection();
            } catch (IOException e) {
                throw new RuntimeException("IoException", e);
            } catch (TimeoutException e) {
                throw new RuntimeException("timeOutException", e);
            }
            return connection;
        }
    }

}
public class FanOutProducer {

    private static final String EXCHAGE_NAME = "test_exchange_li_fanout";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明一个exchange,fanout类型,不持久化
        channel.exchangeDeclare(EXCHAGE_NAME, "fanout", false);

        //发送消息
        StringBuilder stringBuilder = new StringBuilder("message");
        for (int i = 0; i < 10; i++) {
            channel.basicPublish(EXCHAGE_NAME, "", null, stringBuilder.append(i).toString().getBytes("utf-8"));
        }

        //关闭连接
        channel.close();
        connection.close();
    }
}
public class FanOutConsumer01 {

    private static final String EXCHAGE_NAME = "test_exchange_li_fanout";
    private static final String QUEUE_NAME = "test_queue_Name_li_fanout";

    public static void main(String[] args) throws IOException {
        //建立链接
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明exchange和queue--exchange要和生产者一致
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.exchangeDeclare(EXCHAGE_NAME, "fanout");

        //将queue绑定到exchange上
        channel.queueBind(QUEUE_NAME, EXCHAGE_NAME, "");

        //定义消费者
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
                throws IOException {
                String message = new String(body, "utf-8");
                System.out.println(message);
            }
        };

        //开始消费
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
public class FanOutConsumer02 {

    private static final String EXCHAGE_NAME = "test_exchange_li_fanout";
    private static final String QUEUE_NAME = "test_queue_Name_li_fanout02";
....
}
两个消费者都能打印如下
message0
message01
message012
message0123
message01234
message012345
message0123456
message01234567
message012345678
message0123456789

Direct Exchange

只会把消息routingkey一致的queue中

rabbitMq_binding.png

rabbitMq_Direct.png

public class DirectProducer {

    private static final String EXCHAGE_NAME = "test_exchange_li_direct";
    private static final String ROUTING_KEY = "direct_routing_key";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明一个exchange,direct类型,不持久化
        channel.exchangeDeclare(EXCHAGE_NAME, "direct", false);

        //发送消息
        StringBuilder stringBuilder = new StringBuilder("message");
        for (int i = 0; i < 10; i++) {
            channel.basicPublish(EXCHAGE_NAME, ROUTING_KEY, null, stringBuilder.append(i).toString().getBytes("utf-8"));
        }

        //关闭连接
        channel.close();
        connection.close();
    }
}
public class DirectConsumer01 {

    private static final String EXCHAGE_NAME = "test_exchange_li_direct";
    private static final String QUEUE_NAME = "test_queue_Name_li_direct";
    private static final String ROUTING_KEY = "direct_routing_key";

    public static void main(String[] args) throws IOException {
        //建立链接
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明exchange和queue--exchange要和生产者一致
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.exchangeDeclare(EXCHAGE_NAME, "direct");

        //将queue绑定到exchange上
        channel.queueBind(QUEUE_NAME, EXCHAGE_NAME, ROUTING_KEY);

        //定义消费者
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
                throws IOException {
                String message = new String(body, "utf-8");
                System.out.println(message);
            }
        };

        //开始消费
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
public class DirectConsumer02 {

    private static final String EXCHAGE_NAME = "test_exchange_li_direct";
    private static final String QUEUE_NAME = "test_queue_Name_li_direct01";
    private static final String ROUTING_KEY = "direct_routing_key02";
....
}
运行完成,只有DirectConsumer01能收到消息
message0
message01
message012
message0123
message01234
message012345
message0123456
message01234567
message012345678
message0123456789

Topic Exchange

对key进行模式匹配后进行投递,符号”#”匹配一个或多个词,符号””匹配正好一个词。例如”abc.#”匹配”abc.def.ghi”,”abc.”只匹配”abc.def”

rabbitMq_Topic.png

public class TopicProducer {

    private static final String EXCHAGE_NAME = "test_exchange_li_topic";

    private static final String ROUTING_KEY = "test.routingkey.01";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明一个exchange,topic类型,不持久化
        channel.exchangeDeclare(EXCHAGE_NAME, "topic", false);

        //发送消息
        StringBuilder stringBuilder = new StringBuilder("message");
        for (int i = 0; i < 10; i++) {
            channel.basicPublish(EXCHAGE_NAME,ROUTING_KEY, null, stringBuilder.append(i).toString().getBytes("utf-8"));
        }
        //关闭连接
        channel.close();
        connection.close();
    }
}
public class TopicConsumer01 {

    private static final String EXCHAGE_NAME = "test_exchange_li_topic";
    private static final String QUEUE_NAME = "test_queue_Name_li_topic_01";
    private static final String ROUTING_KEY = "test.routingkey.#";

    public static void main(String[] args) throws IOException {
        //建立链接
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明exchange和queue--exchange要和生产者一致
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.exchangeDeclare(EXCHAGE_NAME, "topic");

        //将queue绑定到exchange上
        channel.queueBind(QUEUE_NAME, EXCHAGE_NAME, ROUTING_KEY);

        //定义消费者
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
                throws IOException {
                String message = new String(body, "utf-8");
                System.out.println(message);
            }
        };

        //开始消费
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
为了节省篇幅,
public class TopicConsumer02 {

    private static final String EXCHAGE_NAME = "test_exchange_li_topic";
    private static final String QUEUE_NAME = "test_queue_Name_li_topic_02";
    private static final String ROUTING_KEY = "test.routingkey.*";
......
}

public class TopicConsumer03 {

    private static final String EXCHAGE_NAME = "test_exchange_li_topic";
    private static final String QUEUE_NAME = "test_queue_Name_li_topic_03";
    private static final String ROUTING_KEY = "test.routingkey";
....
}
输出结果只有TopicConsumer01和TopicConsumer02有日志输出
message0
message01
message012
message0123
message01234
message012345
message0123456
message01234567
message012345678
message0123456789

内容的持久化durable 表示持久化

  1. Queue的持久化
com.rabbitmq.client.Channel

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                 Map<String, Object> arguments) throws IOException;

  1. Message的持久化
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;


public static class BasicProperties extends com.rabbitmq.client.impl.AMQBasicProperties {
        private String contentType;
        private String contentEncoding;
        private Map<String,Object> headers;
        private Integer deliveryMode;
        private Integer priority;
        private String correlationId;
        private String replyTo;
        private String expiration;
        private String messageId;
        private Date timestamp;
        private String type;
        private String userId;
        private String appId;
        private String clusterId;

其中deliveryMode=2表示持久化
  1. Exchange的持久化
com.rabbitmq.client.Channel

Exchange.DeclareOk exchangeDeclare(String exchange,
                                              String type,
                                              boolean durable,
                                              boolean autoDelete,
                                              boolean internal,
                                              Map<String, Object> arguments) throws IOException;

  1. binding是如何持久化的 当queueexchange都被定义为持久化的情况下,两种个相关联的binding也会被保存下来,如果queue或者exchange被删除,这个绑定关系也会被删除

就算上面三个的持久化方式都开启了,也不能保证消息在使用过程中完全不丢失,如果消费者autoAck=true,在收到消息之后自动确认了,但是处理时服务崩了,就会导致消息的丢失,所以需要确认机制的支持

消息投递的确认模式

  1. 默认情况下,生产者投递消息后,broker时不会做出任何返回的
  2. 解决方式如下:
    1. 使用Amqp协议中的事务机制效率低,影响吞吐量
    2. 将信道channel设置成确认模式

channel信道的确认模式

channel设置成确认模式之后,所有提交的消息都会被分配一条唯一的ID,当消息被投递到匹配的队列中,信道会向生产者发出确认消息,并且消息中带上这个Id。确认模式是异步的,生产者可以发送完一条消息后继续发送下一条消息。调用channel的confirmSelect方法开启确认模式

  1. 普通方式,发送完成之后调用waitForConfirms
  2. 异步回调模式,addConfirmListener注册回调函数

消息消费的应答模式

  1. autoAck,如果等于true,会在消息发送过来之后自动响应--队列会将该消息删除,可能会导致消息消费失败了,但是消息已经被删除的情况
  2. autoAck=false,需要业务逻辑在处理完成之后,调用channel.basicAck做显示的响应

消费者获取消息时,可以指定预取的消息数量 通过channel的basicQos方法设置

rabbitmq 的死信队列相当于为一个队列设置一个备用的队列,在出现以下情况的时候将所谓的死亡信息推送到死亡信息队列

  1. 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
  2. 消息超期 (rabbitmq Time-To-Live -> messageProperties.setExpiration())
  3. 队列超载 具体内容参考:my.oschina.net/u/2948566/b…

rabbitmq的blackhole问题

什么是blackhole问题 生产者向exchange投递message,而由于各种原因导致该 message 丢失,但发送者却不知道

导致balckhole问题的原因
  1. 向未绑定 queue 的 exchange 发送 message
  2. exchange 以 binding_key key_A绑定了 queue queue_A,但向该 exchange 发送 message 使用的 routing_key 却是 key_B。使用了不存在的绑定关系

如何防止blackhole 没有特别好的办法,只能在具体实践中通过各种方式保证相关 fabric 的存在。另外,如果在执行 Basic.Publish 时设置 mandatory=true ,则在遇到可能出现 blackholed 情况时,服务器会通过返回 Basic.Return 告之当前 message 无法被正确投递(内含原因 312 NO_ROUTE)。

Consumer Cancellation Notification 机制

channel消费queue的时候,可能因为某些原因导致消费停止,

  1. 消费者发起 basic.cacncel命令
channel.basicCancel(consumerTag);
  1. 队列被删除或者节点失败也有可能导致消费被取消

正常情况下,第一种情况,因为是消费者自己发起的,自己可以感知到,但是第二种情况如果没有一个通知机制的话,可能会导致消费者一直傻傻的在等一个不可能来的消息 所以为了避免上面这些情况出现,RabbitMQ引入了扩展特性:由于消息中间件代理出现的异常或者正常情况导致消费者取消,会向对应的消费者(信道)发送basic.cancel,但是由客户端信道主动向消息中间件代理发送basic.cancel以取消消费者的情况下不会受到消息中间件代理的basic.cancel回复。

channel.basicConsume("throwable.queue.direct", new DefaultConsumer(channel) {

				@Override
				public void handleCancelOk(String consumerTag) {
					System.out.println("收到来自消息中间件代理的basic.cancel-ok回复,consumerTag=" + consumerTag);
				}

				@Override
				public void handleCancel(String consumerTag) throws IOException {
					System.out.println("收到来自消息中间件代理的basic.cancel回复,consumerTag=" + consumerTag);
				}
			});

rebbitmq的集群知识

参考资料: www.cnblogs.com/xishuai/p/r… www.ywnds.com/?p=4741 blog.csdn.net/zhu_tianwei… 对异常情况解释的比较多

为什么需要集群

  1. 摆脱单机资源上的限制,提供更大的吞吐量
  2. 提供更加稳定的更高可用的服务

集群中节点之间需要同步的信息rabbitmq的元数据

a. 队列元数据:队列名称和它的属性; b. 交换器元数据:交换器名称、类型和属性; c. 绑定元数据:一张简单的表格展示了如何将消息路由到队列; d. vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性; e. 元数据信息需要保存的磁盘中,所以每个集群中至少需要一个disk节点

因此,当用户访问其中任何一个RabbitMQ节点时,通过rabbitmqctl查询到的queue/user/exchange/vhost等信息都是相同的。

rabbitmq集群中同步元数据.png

rabbitmq 的集群模式

集群内节点的类型

磁盘节点

保存数据到磁盘和内存中,如果集群中群都是内存节点,那就不能停止他们,否则元数据就会丢。

RabbitMQ只要求集群中至少有一个磁盘节点,如果只有一个磁盘节点,刚好又崩溃了,集群可以继续路由消息,但不能创建队列、交换器、绑定、添加用户、更改权限等操作。所以,建议设置两个磁盘节点,当内存节点重启后,会连接到预先配置的磁盘节点,下载当前集群元数据拷贝,所以要将所有磁盘节点告诉内存节点。

内存节点

数据只保存到内存中,除非遇到

  1. publish消息的时候指定需要持久化
  2. 内存吃紧的时候,会把部分消息持久化到磁盘

内存节点的特点就是执行效率高

普通模式

不是每个节点都有所有队列的完全拷贝,如果在集群中创建队列,只会在单个节点上创建完整的队列信息(元数据、状态、内容),所有其他节点只知道队列的元数据和指向该队列的节点指针。

既然一个队列的数据只存在一个节点上,那么在连接集群内其他节点的时候,是如何进行发布消息和消费消息的呢? 如果消息生产者所连接的是节点2或者节点3,此时队列1的完整数据不在该两个节点上,那么在发送消息过程中这两个节点主要起了一个路由转发作用,根据这两个节点上的元数据(也就是上文提到的:指向queue的owner node的指针)转发至节点1上,最终发送的消息还是会存储至节点1的队列1上。 同样,如果消息消费者所连接的节点2或者节点3,那这两个节点也会作为路由节点起到转发作用,将会从节点1的队列1中拉取消息进行消费。

如果节点崩溃了,附加在队列上的消费者也就无法接收新的消息了。可以让消费者重连到集群并重新创建队列,这种做法仅当队列没设置持久化时才可行,如果做了队列持久化或消息持久化,必须等到对应的节点恢复了才能被消费,这是为了确保当失败的节点恢复后加入集群,节点上的队列消息不会丢失。

为什么不将队列内容和状态复制到所有节点:

  1. 存储空间,如果每个集群节点都拥有所有队列的完全拷贝,添加新节点不会带来更多存储空间;
  2. 性能,消息的发布者需要将消息复制到每一个集群节点,对于持久化消息,网络和磁盘复制都会增加。

优点:

  1. 使用集群能很好的实现服务能力的水平拓展

缺点:

  1. 因为单个队列只维持在单个节点上,也很难认为是高可用
  2. 如果是不持久化的消息和队列,单机宕机后消息会丢失

镜像模式 把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案

根据策略可以为节点定义镜像节点,镜像节点之间可以实现队列中消息实体的同步。 对于发送方确认消息,Rabbit会在所有队列和队列的从拷贝安全地接收到消息时,才会通知发送方。

rabbitmq镜像集群模式的策略.jpg

优点:

  1. 因为能对节点维护的队列中的消息实体做了同步,可以保证

缺点:

  1. 因为要进行消息实体的复制,所以势必会影响系统的性能
  2. 网络通信也会加大,如果消息量比较大话