转载

RabbitMQ特性及其在Java中的使用

RabbitMQ

1.RabbitMQ是什么?

RabbitMQ是一个基于AMQP协议的高级消息中间件,它主要的技术特点是可用性,安全性,集群,多协议支持,可视化的客户端,活跃的社区。

2.为什么选择RabbitMQ

  • 功能强大,支持死信队列,优先级队列,延迟队列,重试队列等多种功能无需二次开发。
  • 性能相对还算可以,一般单机的QPS在万级左右,可以满足一般的应用场景。
  • 文档说明非常丰富,社区活跃,上手容易。
  • 强大的可视化管理工具。

3.RabbitMQ模型

AMQP消息模型

  • Broker(消息代理) : 实际上就是消息服务器实体。

  • Exchange(交换机) : 用来发送消息的AMQP实体,它指定消息按什么路由规则,路由到哪个队列。

  • Queue(消息队列) :每个消息都会被投入到一个或多个队列。

  • Binding(绑定) : 它的作用就是把交换机(Exchange)和队列(Queue)按照路由规则绑定起来。

  • Routing Key(路由关键字) :路交换机(Exchange)根据这个关键字进行消息投递。

  • vhost(虚拟主机) : 虚拟主机,一个消息代理(Broker)里可以开设多个虚拟主机(vhost),用作不同用户的权限分离。

  • Connection(连接) :AMQP连接通常是长连接,Producer和Consumer都是通过TCP连接到RabbitMQ Server的。

  • Channel(通道) : AMQP通过通道(channels)来处理多连接,可以把通道理解成共享一个TCP连接的多个轻量化连接。

4.交换机(Exchange)和交换机类型

在RabbitMQ中消息并不会被直接投递到队列中去,而是有生产者将消息发布到交换机中,交换机和一个或多个队列绑定,通过不同的路由规则将消息路由到队列中去,供消费者消费,RabbitMQ中共提供了四种类型交换机,交换机可以有两个状态:持久(durable)、暂存(transient)。持久化的交换机会在消息代理(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在代理再次上线后重新被声明)。

  • 直连交换机(Direct) 
    直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的。直连交换机要求Publisher和Consumer的路由关键字(routingKey)完全相同才会将消息路由到绑定的队列。直连交换机用来处理消息的单播路由(unicast routing),虽然它可以进行多播。我们用几行伪代码来说明它是如何工作的可能更加直观:

Publisher:

// 路由关键字
private static final String[] routingKeys = new String[] {"info", "warn", "error",""debug};
// 声明一个交换机并指定它的类型为direct
channel.exchangeDeclare("exchange_direct", "direct")
//发布消息
for (String routingKey : routingKeys) {
            String message = "Send the message : " + severity;
            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());
   }

Consumer

// 路由关键字
private static final String[] routingKeys = new String[] {"info", "warn"};
// 声明一个交换机并指定它的类型为direct
channel.exchangeDeclare("exchange_direct", "direct")
// 声明一个临时队列
String queueName = channel.queueDeclare().getQueue();
// 根据路由关键字绑定队列
for (String routingKey : routingKeys) {
     channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
   }
  • 扇形交换机(Faount)

扇型交换机(funout exchange)会将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键,扇型用来交换机处理消息的广播路由(broadcast routing)。

Publisher

private static final String EXCHANGE_NAME = "exchange.fanout";
// 在发布端可以不声明队列
//channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
for(int i=0;i<5;i++){
            String message="this is number"+i;
            // 路由关键字不能为null,填写""
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
            System.out.println(" [x] sent ' " + message + " '");
        }

Consumer

 private static final String EXCHANGE_NAME = "exchange.fanout";
 private  final  static String QUEUE_NAME = "queue.fanout";
 channel.queueDeclare(QUEUE_NAME,true,false,false,null);
 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
 // 绑定队列
 channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");

和扇形交换机绑定的队列将全部收到收到发布端的消息。

  • 主题交换机(Topic)

主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列,属于多播路。,topic可以进行模糊匹配,可以使用星号和井号#这两个通配符来进行模糊匹配,其中 号可以代替一个单词 # 号可以代替任意个单词,但是需要注意的是topic交换机的路由键也不是可以随意设置的,必须是由点隔开的一系列的标识符组成。标识符一般和消息的某些特性相关,可以定义任意数量的标识符,上限为255个字节,当路由键可以模糊匹配上的时候就能将消息映射到绑定的队列中去。

  • 首部交换机(Header)

首部交换机和扇形交换机一样不需要路由关键字,交换机时通过headers来将消息映射到队列的,heders是一个hash结构求携带一个键“x-match”,这个键的value可以是any或者all,all代表消息携带的Hash是需要全部匹配,any代表仅匹配一个键就可以了。首部交换机的最大特点就是匹配规则不被限制为string,而是object。

Publish

        private static String EXCHANGE_NAME = "exchange.hearders";

        Map<String, Object> hearders = new HashMap<String, Object>();
        hearders.put("api", "login");
        hearders.put("version", 1.0);
        hearders.put("radom", UUID.randomUUID().toString());
        AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder()
                                                                            .headers(hearders)
                                                                            .build();

        String message = "Hello RabbitMQ!";
        channel.basicPublish(EXCHANGE_NAME, "", properties, message.getBytes("UTF-8"));

Consumer

        private static String EXCHANGE_NAME = "exchange.hearders";
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.HEADERS);
        String queueName = channel.queueDeclare().getQueue();
        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-match", "any");
        // arguments.put("x-match", "all");  此时将不能匹配,因为Publisher的头部中并没有dataType属性
        arguments.put("api", "login");
        arguments.put("version", 1.0);
        arguments.put("dataType", "json");

        // 队列绑定时需要指定参数,注意虽然不需要路由键但仍旧不能写成null,需要写成空字符串""
        channel.queueBind(queueName, EXCHANGE_NAME, "", arguments);
  • 默认交换机(Default)

默认交换机(default exchange)不是一个真正的交换机类型,实际上是一个由消息代理(Broker)预先声明好的没有名字(名字为空字符串)的直连交换机。它有一个特殊的属性:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。 
很多时候我们对于一些不复杂的场景都会使用这一特殊属性。

Publisher

private static final String QUEUE_NAME="task_queue";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());

Consumer

private static final String QUEUE_NAME="task_queue";
channel.queueDeclare(QUEUE_NAME, false, false, false, null);

上面的伪代码看起来我们并没有使用交换机,而是直接将消息投递到了队列中去,但实际上这个队列被绑定到了默认交换机上,而路由键就是队列名称。

队列及队列属性

channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments); 
  • queue : 队列名称,队列在声明(declare)后才能被使用。如果一个队列尚不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性与已存在队列的属性有差异,那么将会抛出一个406通道级异常。
  • durable:队列的声明默认是存放到内存中的,称为暂存队列,消息代理重启会丢失。如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库。但是队列持久化并不意味着消息持久化当消息代理重启后消息依旧会丢失。
  • exclusive :是否排外的,有两个作用,一:当连接关闭时connection.close()该队列是否会自动删除;二:该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外的,会对当前队列加锁,其他通道channel是不能访问的。
  • autoDelete :当最后一个消费者断开连接之后队列是否自动被删除。
  • arguments : 队列的高级属性

    1. x-message-ttl(Time-To-Live): 
    设置队列中的所有消息的生存周期(统一为整个队列的所有消息设置生命周期), 也可以在发布消息的时候单独为某个消息指定剩余生存时间,单位毫秒。生存时间到了,消息会被从队里中删除,注意是消息被删除,而不是队列被删除。

// 统一设置队列消息过期时间为10s
Map<String, Object> argumentsMap = new HashMap<>();
argumentsMap.put("x-message-ttl", 10000);
channel.queueDeclare(QUEUE_NAME, true, false, false, argumentsMap);       

// 单独设置某条消息过期时间
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder()
                                                                    .expiration("10000")
                                                                    .build(); 
channel.basicPublish(EXCHANGE_NAME, "", properties, message.getBytes(“UTF-8”));

2.x-expires : 当队列在指定的时间没有被访问则被删除。

Map<String, Object> argumentsMap = new HashMap<>();
// 设置队列消息过期时间 单位:毫秒
argumentsMap.put("x-expires", 10000);      
channel.queueDeclare(QUEUE_NAME, true, false, false, argumentsMap);      

3.x-max-length : 限定队列的消息的最大值长度,超过指定长度将会把最早的几条删除掉,遵循先进先出的原则。(要注意的是虽然消息队列是异步处理消息,但是消息几乎是被准实时消费的,这里只能保证消息队列的堆积消息不超过最大长度,使用时要特别注意)

Map<String, Object> argumentsMap = new HashMap<>();
// 设置队列消息对列的最大长度为5
argumentsMap.put("x-max-length", 5);      
channel.queueDeclare(QUEUE_NAME, true, false, false, argumentsMap);      

4.x-max-length-bytes :限定队列最大占用的内存空间大小。 
5.x-dead-letter-exchange : 将从队列中删除的消息(大于最大长度、或者过期的等)推送到指定的交换机中去而不是丢弃掉。 
6.x-dead-letter-routing-key :将删除的消息推送到指定交换机的指定路由键的队列中去。

Dead Letter Exchange(死亡交换机) 和 Dead Letter Routing Key(死亡路由键)用做死信队列(由于某些原因消息无法被正确的投递,为了确保消息不会被无故的丢弃,一般将其置于一个特殊角色的队列,这个队列一般称之为死信队列),当队列中的消息被删除时(大于最大长度、或者过期的等),可以将这些被删除的信息推送到其他交换机中,让其他消费者订阅这些被删除的消息,处理这些消息。

public class Publisher {
    private static final String DEAD_QUEUE_NAME = "dead_queue";
    private static final String DEAD_EXCHANGE_NAME = "exchange.dead";
    private static final String DEAD_ROUTING_KEY = "routingkey.dead";
    private static final String QUEUE_NAME = "general_queue";
    private static final String EXCHANGE_NAME = "exchange.general";
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明接收"死亡消息"的队列和交换机
        channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        channel.queueDeclare(DEAD_QUEUE_NAME, false, false, false, null);
        channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_ROUTING_KEY);

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-message-ttl", 15000);
        arguments.put("x-expires", 30000);
        arguments.put("x-max-length", 4);
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        arguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        for(int i = 1; i <= 5; i++) {
            String message = "Hello RabbitMQ: "+ i;
            channel.basicPublish(EXCHANGE_NAME, "", null, message .getBytes("UTF-8"));
            System.out.println("sent message : "+ message);
        }
        channel.close();
        connection.close();
    }
}

死信队列
刚开始由于队列长度是4,总共发送5条消息,所以最早进入队列的消息1将被删除掉,被推送到死亡队列中,所以看到普通队列的消息为4条,死亡队列的消息为1条。随着时间的流逝普通队列的消息全部过期,所有消息都被推送到死亡队列中,最后普通队列被删除。 
7.x-max-priority : 优先级队列,声明队列时先定义最大优先级值(定义最大值一般不要太大),在发布消息的时候指定该消息的优先级, 优先级更高的消息先被消费。

Publisher

private static final String QUEUE_NAME = "priority_queue";
    private static final String EXCHANGE_NAME = "exchange.priority";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-max-priority", 5);
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        for(int i = 1; i <= 5; i++) {
            String message = "Hello RabbitMQ: "+ i;
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder() .priority(i) .build();
            channel.basicPublish(EXCHANGE_NAME, "", properties, message .getBytes("UTF-8"));
            System.out.println("sent message : "+ message);
        }
        channel.close();
        connection.close();
    }

Consumer

private static final String QUEUE_NAME = "priority_queue";
    private static final String EXCHANGE_NAME = "exchange.priority";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-max-priority", 5);
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body,"UTF-8");
                System.out.println("Receive message : " + message);
            }
        };
        channel.basicConsume(QUEUE_NAME,true,consumer);
        channel.close();
        connection.close();
    }

8.x-queue-mode=lazy : 先将消息保存到磁盘上,不放在内存中,当消费者开始消费的时候才加载到内存中,默认为lazy。 
9.x-queue-master-locator : 配置镜像队列

消息确认

  • 消费者(Consumer) :在处理消息的时候偶尔会失败或者有时消息代理会直接崩溃掉。而且网络原因也有可能引起各种问题,那么为了保证消息不会丢失保证消息被正确处理,RabbitMQ提供了两种消息确认机制:

    1.自动应答 :

boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

自动应答表示当消费者连接上队列,因为没有指定消费者一次获取消息的条数,所以会把队列中的所有消息一下子推送到消费者端,当消息从队列被推出的时的那一刻就表示已经对消息进行自动确认了,消息就会从队列中删除。 
这里写图片描述

channel.basicConsume(QUEUE_NAME, true, consumer);

当队列被订阅后,队列中的消息全部被清空。 
2.手动应答 :

boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

Consumer:

com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
         public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("C [x] Received '" + message + "'");
            }

        };
 // 订阅消息    
channel.basicConsume(QUEUE_NAME, false, consumer);

这里写图片描述

每执行一次channel.basicAck(envelope.getDeliveryTag(), false);Unacked和Total就会减去1,直到两个值都为0 。 
注意:手动确认一定要channel.basicAck(envelope.getDeliveryTag(), false);否则会导致消息不被确认而一直堆积在队列中而不被删除。


本文转载自:https://blog.csdn.net/qq_25235807/article/details/80135088

正文到此结束
Loading...