消息中间件(RabbitMQ)入门到进阶

消息中间件​

简介:

消息队列中间件(Message Queue Middleware ,简称为MQ) 是指利用高效可靠的消息传递
机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传
递和消息排队模型,它可以在分布式环境下扩展进程间的通信。

目前开源的消息中间件有很多,比较主流的有RabbitMQ 、Kafka、ActiveMQ 、RocketMQ
等。

书签: Rabbitmq实战指南 第六章

RabbitMQ

生产和消费消息

目前最新的RabbitMQ Java 客户端版本为4 .2 .l ,相应的maven 构建文件如下:

<! -- https : //mvnrepository . com/artifact/com . rabbitmq/amqp-client -->
<dependency>
<groupld>com . rabbitmq</groupld>
<artifactld>amqp- client</artifactld>
<version>4 . 2 . 1</version>
</dependency>

默认情况下,访问RabbitMQ 服务的用户名和密码都是"guest " , 这个账户有限制,默认只能通过本地网络(如localhost) 访问,远程网络访问受限,所以在实现生产和消费消息之前,需要另外添加一个用户,并设置相应的访问权限。

消费者客户端代码

package com.zzh.rabbitmq.demo;
工mport com.rabbitmq.c1ient.Channe1;
import com.rabbitmq.c1ient.Connection ;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.c1ient.MessageProperties;
import java.io.IOException;
import java.uti1.concurrent.TimeoutException;

public c1ass RabbitProducer {
  
private static fina1 String EXCHANGE NAME = "exchange demo ";
private static fina1 String ROUTING KEY = " routingkey demo";
private static fina1 String QUEUE NAME = "queue demo ";
private static fina1 String IP ADDRESS = " 192.168.0 . 2";
private static fina1 int PORT = 5672;//RabbitMQ 服务端默认端口号为5672
  
public static void main(String[] args) throws IOException,
TimeoutException , InterruptedException (
ConnectionFactory factory = new ConnectionFactory() ;
factory . setHost(IP ADDRESS) ;
factory . setPort(PORT) ;
factory . setUsername( " root " );
factory . setPassword( " root123 ");
  
Connection connection = factory.newConnection();//创建连接
  
Channel channel = connection.createChannel();//创建信道
  
// 创建一个type="direct" 、持久化的、非自动删除的交换器
channel.exchangeDeclare(EXCHANGE_NAME, " direct" , true , false , null) ;
  
//创建一个持久化、非排他的、非自动删除的队列
channel. queueDeclare(QUEUE_NAME , true , false , false , null) ;
  
//将交换器与队列通过路由键绑定
channel.queueBind(QUEUE NAME , EXCHANGE NAME , ROUTING KEY);
  
//发送一条持久化的消息: hello world !
String message = "Hello World !";
channel.basicPublish(EXCHANGE NAME , ROUTING KEY ,
MessageProperties . PERSISTENT TEXT PLAIN ,
message.getBytes()) ;
  
//关闭资源
channel . close() ;
connection.close ();
}
}

客户端开发导向

连接RabbitM

ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(USERNAME);
factory.setPassword(PASSWORD);
factory.setVirtualHost(virtualHost) ;
factory.setHost(IP ADDRESS);
factory . setPort(PORT) ;
Connection conn = factory.newConnection();

//也可以选择使用URI 的方式来实现
ConnectionFactory factory =口ew ConnectionFactory();
factory.setUri( "amqp:lluserName : password@ipAddress:portNumber/virtualHost");
Connection conn = factory.newConnection();

//Connection 接口被用来创建一个Channel:
Channel channel = conn.createChannel();
//在创建之后, Channel 可以用来发送或者接收消息了。

注意要点:
Connection 可以用来创建多个Channel 实例,但是Channel 实例不能在线程问共享,
应用程序应该为每一个线程开辟一个Channel 。某些情况下Channel 的操作可以并发运行,但
是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响友送方确认( publisher
confrrm)机制的运行(详细可以参考4.8 节),所以多线程问共享Channel 实例是非线程安全的。

使用交换器和队列

声明一个交换器和队列:

channel.exchangeDeclare(exchangeName, "direct" , true) ;
String queueName = channel . queueDeclare() .getQueue( );
channel . queueBind (queueName , exchangeName , routingKey);

上面创建了一个持久化的、非自动删除的、绑定类型为direct 的交换器,同时也创建了一
个非持久化的、排他的、自动删除的队列(此队列的名称由RabbitMQ 自动生成)。这里的交换
器和队列也都没有设置特殊的参数。

如果要在应用中共享一个队列,可以做如下声明

channel .exchangeDeclare (exchangeName , "direct" , true) ;
channel . queueDeclare(queueName , true , false , false , null);
channel . queueBind(queueName , exchangeName, routingKey) ;

这里的队列被声明为持久化的、非排他的、非自动删除的,而且也被分配另一个确定的己
知的名称(由客户端分配而非RabbitMQ 自动生成)。

声明重复的队列或交换机

生产者和消费者都可以声明一个交换器或者队列。如果尝试声明一个已经存在的交换器或
者队列, 只要声明的参数完全匹配现存的交换器或者队列, RabbitMQ 就可以什么都不做, 并成
功返回。如果声明的参数不匹配则会抛出异常。

exchangeDeclare 方法详解

声明交换器参数解析

exchangeDeclare 有多个重载方法,这些重载方法都是由下面这个方法中缺省的某些参
数构成的。

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

这个方法的返回值是Exchange . Decla r eOK , 用来标识成功声明了一个交换器。

各个参数详细说明如下所述:

**~ exchange : **交换器的名称。

**~ type : **交换器的类型,常见的如fanout、direct 、topic 。

~ durable: 设置是否持久化。durable 设置为true 表示持久化, 反之是非持久化。

**~ autoDelete : **设置是否自动删除。autoDelete 设置为true 则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定, 之后所有与这个交换器绑定的队列或者交换器都与此解绑。

注意不能错误地把这个参数理解为: “当与此交换器连接的客户端都断开时, RabbitMQ 会自动删除本交换器” 。

**~ internal : **设置是否是内置的。如果设置为true ,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。

~ argument : 其他一些结构化参数,比如alternate - exchange 。

删除交换器

有声明创建交换器的方法,当然也有删除交换器的方法。相应的方法如下:

(1) Exchange.DeleteOk exchangeDelete(String exchange) throws IOException ;

(2) void exchangeDeleteNoWait(String exchange, boolean ifUnused) throws IOException ;

(3 ) Exchange . DeleteOk exchangeDelete(String exchange, boo1ean ifUnused) throws IOException;

其中exchange 表示交换器的名称,而ifUnused 用来设置是否在交换器没有被使用的情况下删除。

如果isUnused 设置为true ,则只有在此交换器没有被使用的情况下才会被删除:如果设置false ,则无论如何这个交换器都要被删除。

queueDeclare 方法详解

声明队列参数解析

queueDeclare 相对于exchangeDeclare 方法而言,重载方法的个数就少很多, 它只有两个重载方法:

(1) Queue . DeclareOk queueDeclare() throws IOException;

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

不带任何参数的queueDeclare 方法默认创建一个由RabbitMQ 命名的(类似这种amq.gen-LhQzlgv3GhDOv8PIDabOXA 名称,这种队列也称之为匿名队列〉、排他的、自动删除
的、非持久化的队列。

方法的参数详细说明如下所述:

~ queue : 队列的名称。

~ durable: 设置是否持久化。为true 则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。

~ exclusive : 设置是否排他。为true 则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。

这里需要注意三点:

排他队列是基于连接( Connection) 可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列。

"首次"是指如果一个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同。

即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列
适用于一个客户端同时发送和读取消息的应用场景。

~ autoDelete: 设置是否自动删除。为true 则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。

不能把这个参数错误地理解为: “当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。

消费者订阅队列,是否能声明队列?

生产者和消费者都能够使用queueDeclare 来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道直为"传输"。

队列是否存在?

同样这里还有一个queueDeclarePassive 的方法,也比较常用。这个方法用来检测相应的队列是否存在。如果存在则正常返回,如果不存在则抛出异常: 404 channel exception ,同时Channel 也会被关闭。方法定义如下:

Queue.DeclareOk queueDeclarePassive(String queue) throws IOException;

删除队列

与交换器对应,关于队列也有删除的相应方法:
(1) Queue . DeleteOk queueDelete(String queue) throws IOException ;

(2) Queue . DeleteOk queueDelete(String queue, boolean ifUnused, boolean ifEmpty )throws IOException;

(3 ) void queueDeleteNoWait(String queue , boolean ifUnused , boolean ifEmpty)throws IOException ;

其中queue 表示队列的名称;

ifUnused 可以参考上一小节的交换器;

ifEmpty 设置为true 表示在队列为空(队列里面没有任何消息堆积)的情况下才能够删除。

清空队列queuepurge

与队列相关的还有一个有意思的方法一-queuepurge ,区别于queueDelete ,这个方
法用来清空队列中的内容,而不删除队列本身,具体定义如下:

Queue.PurgeOk queuePurge(String queue ) throws IOException;

queueBind 方法详解

绑定队列参数详解

将队列和交换器绑定的方法如下,可以与前两节中的方法定义进行类比。

(1) Queue . BindOk queueBind(String queue , String exchange , String routingKey)throws IOException ;

(2) Queue . BindOk queueBind(String queue , String exchange , String routingKey,Map arguments) throws IOException;

(3) void queueBindNoWait(String queue , String exchange, String routingKey,Map arguments) throws IOException ;

方法中涉及的参数详解。

**~ queue: **队列名称;

~exchange: 交换器的名称;

**~routingKey: **用来绑定队列和交换器的路由键;

**~argument: **定义绑定的一些参数。

解绑参数详解

不仅可以将队列和交换器绑定起来,也可以将已经被绑定的队列和交换器进行解绑。具体方法可以参考如下(具体的参数解释可以参考前面的内容,这里不再赘述):

(1) Queue . UnbindOk queueUnbind (String queue , String exchange , String routingKey)throws IOException;

(2) Queue. UnbindOk queueUnbind (String queue , String exchange, String routingKey,Map arguments) throws IOException;

exchangeBind 方法详解

我们不仅可以将交换器与队列绑定,也可以将交换器与交换器绑定,后者和前者的用法如
出一辙,相应的方法如下:

(1) Exchange.BindOk exchangeBind(String destination , String source , StringroutingKey) throws IOException;

(2) Exchange . BindOk exchangeBind(String destination , String source, String routingKey, Map arguments) throws IOException ;

(3 ) void exchangeBindNoWait(String destination, String sour ce , String routingKey,Map arguments) throws IOException;

发送消息

一般发送消息

如果要发送一个消息,可以使用Channel 类的basicPublish 方法,比如发送一条内容
为"Hello World! "的消息,参考如下:

byte[] messageBodyBytes = "Hello , world! ". getBytes();
channel.basicPublish(exchangeName , routingKey , null , messageBodyBytes);

发送特定属性消息

为了更好地控制发送,可以使用mandatory 这个参数, 或者可以发送一些特定属性的信息:

channel.basicPub1ish(exchangeName , routingKey , mandatory,
MessageProperties.PERSISTENT TEXT PLAIN ,
messageBodyBytes) ;

下面这行代码发送了一条消息,这条消息的投递模式( delivery mode ) 设直为2 ,即消息会被持久化(即存入磁盘)在服务器中。同时这条消息的优先级( priority )设置为1 , content句pe为" text/plain" 。可以自己设定消息的属性:

channe1.basicPub1ish(exchangeName , routingKey ,
new AMQP . BasicPropertieS.Builder ()
. contentType( " text/plain" )
. deliveryMode(2)
.priority (1)
.userld( "hidden " )
.build()) ,
messageBodyBytes) ;

发送带消息头的消息

也可以发送一条带有headers 的消息:

Map<String , Object> headers = new HashMap<String, Object>() ;
headers.put( " loca1tion" , "here " );
headers . put( " time " , " today" );
channel .basicPublish(exchangeName , routingKey ,
new AMQP.BasicPropertieS . Builder ()
.headers(headers)
.build()) ,
messageBodyBytes) ;

消息过期机制

还可以发送一条带有过期时间(expiration ) 的消息:

channel . basicPub1ish(exchangeName, routingKey,
new AMQP.BasicProperties.Bui1der()
. expiration( " 60000 " )
.build () ) ,
messageBodyBytes) ;

消息在超过过期时间期限,任然没有被消费则会被删除。

重载方法以及参数详解

对于basicPublish而言,有几个重载方法:

(1) void basicPublish (String exchange , String routingKey, BasicProperties props ,byte[) body) throws IOException ;

(2) void basicPublish(String exchange , String routingKey, boolean mandatory,BasicProperties props , byte[] body) throws IOException;

(3) void basicPublish(String exchange, String routingKey, boolean mandatory,boolean immediate ,BasicProperties props , byte[] body) throws IOException ;

对应的具体参数解释如下所述。

~ exchange: 交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ 默认的交换器中。

**~ routingKey : **路由键,交换器根据路由键将消息存储到相应的队列之中。

~ props : 消息的基本属性集。

其包含14 个属性成员,分别有contentType 、contentEncoding 、headers ( Map) 、deliveryMode 、priority 、correlationld 、replyTo 、expiration 、messageld、timestamp 、type 、userld 、
appld、clusterld。

~byte [] body : 消息体( payload ) ,真正需要发送的消息。

消费消息

RabbitMQ 的消费模式分两种: 推( Push )模式和拉( Pull )模式。推模式采用Basic . Consume
进行消费,而拉模式则是调用Basic . Get 进行消费。

推模式——持续订阅

在推模式中,可以通过持续订阅的方式来消费消息,使用到的相关类有:
import com . rabbitmq.client . Consumer;
import com.rabbitmq.client.DefaultConsumer;

接收消息一般通过实现Consumer 接口或者继承DefaultConsumer 类来实现。当调用与Consumer 相关的API 方法时, 不同的订阅采用不同的消费者标签(consumerTag) 来区分彼此,在同一个Channel中的消费者也需要通过唯一的消费者标签以作区分。

boolean autoAck = false ;
channel . basicQos(64);
channel . basicConsume(queueName , autoAck, "myConsumerTag" ,
new DefaultConsumer(channel) {
	@Override
	public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties 	properties ,byte [] body)throws IOException{
		String routingKey = envelope . getRoutingKey( );
		String contentType = properties.getContentType() ;
		long deliveryTag = envelope.getDeliveryTag() ;
		// (process the message components here . .. )
		channel . basicAck(deliveryTag, false );
	}
} ) ;

注意, 上面代码中显式地设置autoAck 为false , 然后在接收到消息之后进行显式ack 操作(channel . basicAck ), 对于消费者来说这个设置是非常必要的, 可以防止消息不必要地丢失。

Channel 类中basicConsume 方法有如下几种形式:

(1) String basicConsume(String queue , Consumer callback) throws IOException ;

(2) String basicConsume(String 守ueue , boolean autoAck, Consumer callback) throws IOException ;

(3) String bas 工cConsume(String queue , boolean autoAck, Maparguments , Consumer callback) throws IOException ;

(4) String basicConsume(String queue , bool ean autoAck, String consumerTag,Consumer callback) throws IOException ;

(5) String basicConsume(String queue , boolean autoAck, String consumerTag,boolean noLocal , boolean exclusive, Map arguments , Consumer callback)throws IOException ;

线程安全

和生产者一样,消费者客户端同样需要考虑线程安全的问题。消费者客户端的这些callback会被分配到与Channel 不同的线程池上, 这意味着消费者客户端可以安全地调用这些阻塞方法,比如channel . queueDeclare 、channel . basicCancel 等。

每个Channel 都拥有自己独立的线程。最常用的做法是一个Channel 对应一个消费者,也就是意味着消费者彼此之间没有任何关联。当然也可以在一个Channel 中维持多个消费者,但是要注意一个问题,如果Channel 中的一个消费者一直在运行,那么其他消费者的callback会被"耽搁"。

拉模式——获得单条消息

这里讲一下拉模式的消费方式。通过channel . basicGet 方法可以单条地获取消息,其返回值是GetResponeo Channel 类的basicGet 方法没有其他重载方法,只有:

GetResponse basicGet(String queue , boolean autoAck) throws IOException;

其中queue 代表队列的名称,如果设置autoAck 为false , 那么同样需要调用channel . basicAck 来确认消息己被成功接收。

GetResponse response = channel.basicGet(QUEUE NAME , false) ;
System.out.println(new String(response.getBody()));
channel .basicAck(response . getEnvelope() . getDeliveryTag() , false);

注意要点:

Basic . Consume 将信道(Channel) 直为接收模式,直到取消队列的订阅为止。在接收模式期间, RabbitMQ 会不断地推送消息给消费者,当然推送消息的个数还是会受到Basic.Qos的限制。

如果只想从队列获得单条消息而不是持续订阅,建议还是使用Basic.Get 进行消费.但是不能将Basic.Get 放在一个循环里来代替Basic.Consume ,这样做会严重影响RabbitMQ的性能.如果要实现高吞吐量,消费者理应使用Basic.Consume 方法。

消息的确认与拒绝

消息确认

autoACK

为了保证消息从队列可靠地达到消费者, RabbitMQ 提供了消息确认机制( messageacknowledgement) 。

消费者在订阅队列时,可以指定autoAck 参数,当autoAck 等于false时, RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除) 。

当autoAck 等于true 时, RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。

采用消息确认机制后,只要设置autoAck 参数为false ,消费者就有足够的时间处理消息(任务) ,不用担心处理消息过程中消费者进程挂掉后消息丢失的问题, 因为RabbitMQ 会一直等待持有消息直到消费者显式调用Basic.Ack 命令为止。

当autoAck 参数置为false ,对于RabbitMQ 服务端而言,队列中的消息分成了两个部分:

一部分是等待投递给消费者的消息。

一部分是己经投递给消费者,但是还没有收到消费者确认信号的消息。

RabbtiMQ 的Web 管理平台上的Ready &Unacknowledged

如果RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者己经断开连接,则RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能还是原来的那个消费者。

RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否己经断开,这么设计的原因是RabbitMQ 允许消费者消费一条消息的时间可以很久很久。

RabbtiMQ 的Web 管理平台(详细参考第5. 3 节)上可以看到当前队列中的" Ready" 状态和"Unacknowledged" 状态的消息数,分别对应上文中的等待投递给消费者的消息数和己经投递给消费者但是未收到确认信号的消息数。

消息拒绝

在消费者接收到消息后,如果想明确拒绝当前的消息而不是确认,那么应该怎么做呢?

RabbitMQ 在2 .0.0 版本开始引入了Basi c .Reject 这个命令,消费者客户端可以调用与其对
应的channel.basicReject 方法来告诉RabbitMQ 拒绝这个消息。

Channel 类中的basicReject 方法定义如下:

void basicReject(long deliveryTag, boolean requeue) throws IOException;

其中**deliveryTag **可以看作消息的编号,它是一个64 位的长整型值,最大值是9223372036854775807 。

如果requeue 参数设置为true ,则RabbitMQ 会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者;

如果requeue 参数设置为false ,则RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。

批量拒绝消息

Basic.Reject 命令一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用
Basic.Nack 这个命令。消费者客户端可以调用channel.basicNack 方法来实现,方法定
义如下:

void basicNack(long deliveryTag, boolean multiple , boolean requeue) throws IOException;

其中deliveryTag 和requeue 的含义可以参考basicReject 方法。

multiple 参数设置为false 则表示拒绝编号为deliveryTag的这一条消息,这时候basicNack 和
basicReject 方法一样;

multiple 参数设置为true 则表示拒绝deliveryTag 编号之前所有未被当前消费者确认的消息。

重入队列

对于requeue , AMQP 中还有一个命令Basic.Recover 具备可重入队列的特性。其对
应的客户端方法为:

(1) Basic.RecoverOk basicRecover() throws IOException;

(2) Basic.RecoverOk basicRecover(boolean requeue) throws IOException;

这个channel.basicRecover 方法用来请求RabbitMQ 重新发送还未被确认的消息。

如果requeue 参数设置为true ,则未被确认的消息会被重新加入到队列中,这样对于同一条消息
来说,可能会被分配给与之前不同的消费者。

如果requeue 参数设置为false ,那么同一条消息会被分配给与之前相同的消费者。

默认情况下,如果不设置requeue 这个参数,相当于channel.basicRecover(true) ,即requeue 默认为true。

RabbitMQ进阶

mandatory 和immediate 是channel . basicPublish 方法中的两个参数,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。

RabbitMQ 提供的备份交换器(Altemate Exchange) 可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定〉存储起来,而不用返回给客户端。

消息何去何从

mandatory

当mandatory 参数设为true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ 会调用Basic.Return 命令将消息返回给生产者。

当mandatory 参数设置为false 时,出现上述情形,则消息直接被丢弃。

那么生产者如何获取到没有被正确路由到合适队列的消息呢?

这时候可以通过调用channel . addReturnListener 来添加ReturnListener 监昕器实现。

channel.basicPublish(EXCHANGE NAME , "", true,MessageProperties . PERSISTENT TEXT PLAIN ,
"mandatory test" . getBytes());

channel.addReturnListener(new ReturnListener() {
	public void handleReturn(int replyCode , String replyText ,String exchange , String routingKey,AMQP.BasicProperties basicProperties ,byte[] body) throws IOException {
	String message = new String(body);
	System.out.println( "B asic.Return 返回的结果是: "+message );
	}
} );

上面代码中生产者没有成功地将消息路由到队列,此时RabbitMQ 会通过Basic . Return
返回" mandatory test " 这条消息,之后生产者客户端通过ReturnListener 监昕到了这个事
件,上面代码的最后输出应该是" Basic.Retum 返回的结果是: mandatory test "。

immediate

当immediate 参数设为true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。

当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic . Return 返回至生产者。概括来说, mandatory 参数告诉服务器至少将该消息路由到一个队列中, 否则将消息返回给生产者。

immediate 参数告诉服务器, 如果该消息关联的队列上有消费者, 则立刻投递:如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者, 不用将消息存入队列而等待消费者了。

RabbitMQ 3 .0 版本开始去掉了对immediate 参数的支持,对此RabbitMQ 官方解释是:immediate 参数会影响镜像队列的性能, 增加了代码复杂性, 建议采用TTL 和DLX 的方法替代。

过期时间(TTL)

TTL, Time to Live 的简称,即过期时间。RabbitMQ 可以对消息和队列设置TTL 。

目前有两种方法可以设置消息的TTL。

第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。

第二种方法是对消息本身进行单独设置,每条消息的TTL 可以不同。

如果两种方法一起使用,则消息的TTL 以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的TTL 值时,就会变成"死信" (Dead Message) ,消费者将无法再收到该消息这点不是绝对的。

队列属性设置消息过期

通过队列属性设置消息TTL 的方法是在channel.queueDeclare 方法中加入x-message -ttl 参数实现的,这个参数的单位是毫秒。

Map<String, Object> argss = new HashMap<String , Object>();
argss.put("x-message-ttl " , 6000);
channel . queueDeclare(queueName , durable , exclusive , autoDelete , argss) ;

如果不设置TTL.则表示此消息不会过期;如果将TTL 设置为0 ,则表示除非此时可以直接将消息投递到消费者.

否则该消息会被立即丢弃,这个特性可以部分替代RabbitMQ 3.0 版本之前的immediate 参数.

之所以部分代替,是因为immediate 参数在投递失败时会用Basic . Return 将消息返回.

消息本身单独设置过期

针对每条消息设置TTL 的方法是在channel.basicPublish 方法中加入expiration的属性参数,单位为毫秒。

//第一种
AMQP . BasicProperties.Builder builder = new AMQP.BasicProperties . Builder();
builder . deliveryMode(2); / / 持久化消息
builder . expiration( " 60000 " );/ / 设置TTL=60000ms
AMQP.BasicProperties properties = builder . build() ;
channel.basicPublish(exchangeName , routingKey, mandatory, properties ,
" ttlTestMessage".getBytes());

//第二种
AMQP . BasicProperties properties = new AMQP . BasicProperties() ;
Properties.setDeliveryMode(2);
properties.setExpiration("60000");
channel . basicPublish (exchangeName , routingKey, mandatory , propert 工es ,
"ttlTestMessage".getBytes());
3.两种消息过期机制的区别

对于第一种设置队列TTL 属性的方法,一旦消息过期,就会从队列中抹去。

而在第二种方法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费
者之前判定的。

为什么这两种方法处理的方式不一样?

因为第一种方法里,队列中己过期的消息肯定在队列头部, RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可。

而第二种方法里,每条消息的过期时间不同,如果要删除所有过期消息势必要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期, 如果过期再进行删除即可。

设置队列的过期机制

通过channel . queueDeclare 方法中的x - expires 参数可以控制队列被自动删除前处于未使用状态的时间。

未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未调用过Basic . Get 命令。

RabbitMQ 会确保在过期时间到达后将队列删除,但是不保障删除的动作有多及时。

在RabbitMQ 重启后, 持久化的队列的过期时间会被重新计算。

用于表示过期时间的x-expires 参数以毫秒为单位, 井且服从和x-message-ttl 一样的约束条件,不过不能设置为0 。比如该参数设置为1 000 ,则表示该队列如果在1 秒钟之内未使用则会被删除。

Map<String, Object> args =new HashMap<String, Object>{) ;
args . put( "x-expires" , 1800000);
channel . queueDeclare("myqueue " , false , false , false , args) ;

死信队列

DLX ,全称为Dead-Letter-Exchange ,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX ,绑定DLX 的队列就称之为死信队列。

消息变成死信一般是由于以下几种情况:

~消息被拒绝(Basic.Reject/Basic .Nack) ,井且设置requeue 参数为false;

~消息过期

~队列达到最大长度

DLX 也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定, 实际上就是设置某个队列的属性。

当这个队列中存在死信时, RabbitMQ 就会自动地将这个消息重新发布到设置的DLX 上去,进而被路由到另一个队列,即死信队列

可以监听这个队列中的消息、以进行相应的处理,这个特性与将消息的TTL 设置为0 配合使用可以弥补imrnediate 参数的功能。

设置死信队列

通过在channel.queueDeclare 方法中设置x-dead-letter-exchange 参数来为这个队列添加DLX

//创建DLX: dlx_exchange
channel . exchangeDeclare("dlx_exchange " , "dir ect "); 
Map<String, Object> args = new HashMap<String, Object>();
args . put("x-dead-letter-exchange" , " dlx exchange ");

//也可以为这个DLX 指定路由键,如果没有特殊指定,则使用原队列的路由键:
args.put("x-dead-letter-routing-key" , "dlx-routing-key");

//为队列myqueue 添加DLX
channel . queueDeclare("myqueue" , false , false , false , args);

下面创建一个队列,为其设置TTL 和DLX

channel .exchangeDeclare("exchange.dlx" , "direct " , true);
channel . exchangeDeclare( "exchange.normal " , " fanout " , true);
Map<String , Object> args = new HashMap<String, Object>( );
args . put("x-message- ttl " , 10000);
args . put( "x-dead-letter-exchange " , "exchange . dlx");
args . put( "x-dead-letter-routing-key" , " routingkey");
channe1 .queueDec1are( "queue.norma1 " , true , fa1se , fa1se , args);
channe1 . queueBind( "queue.normal " , "exchange .normal" , "");
channe1 . queueDec1are( "queue.d1x " , true , false , false , null) ;
channel .queueBind( "queue.dlx" , "exchange.dlx " , Wr outingkey");
channel . basicPublish( "exchange.normal" , " rk " ,
MessageProperties.PERSISTENT_TEXT_PLAIN, "dlx " .getBytes()) ;

这里创建了两个交换器exchange.normal 和exchange.dlx , 分别绑定两个队列queue.normal
和queue.dlx 。

由Web 管理页面可以看出,两个队列都被标记了"D 飞这个是durable 的缩写,即设置了队列持久化。queue.normal 这个队列还配置了TTL 、DLX 和DLK ,其中DLK 指的是x-dead-letter-routing-key 这个属性。

在这里插入图片描述

生产者首先发送一条携带路由键为" rk " 的消息,然后经过交换器exchange .normal 顺利地存储到队列queue.normal 中。

由于队列queue.normal 设置了过期时间为10s , 在这10s 内没有消费者消费这条消息,那么判定这条消息为过期。

由于设置了DLX , 过期之时, 消息被丢给交换器exchange.dlx 中,这时找到与exchange.dlx 匹配的队列queue . dlx , 最后消息被存储在queue.dlx 这个死信队列中。

对于RabbitMQ 来说, DLX 是一个非常有用的特性。它可以处理异常情况下,消息不能够被消费者正确消费(消费者调用了Basic.Nack 或者Basic.Reject) 而被置入死信队列中的情况.

后续分析程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统。DLX 配合TTL 使用还可以实现延迟队列的功能.

消息中间件(RabbitMQ)入门到进阶_第1张图片

延迟队列

延迟队列存储的对象是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

延迟队列的使用场景有很多,比如:

在订单系统中, 一个用户下单之后通常有3 0 分钟的时间进行支付,如果30 分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延迟队列来处理这些订单了。

用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将用户指令发送到延迟队列,当指令设定的时间到了再将指令推送到智能设备。

设置延迟队列

在图4-4 中,不仅展示的是死信队列的用法,也是延迟队列的用法,对于queue.dlx 这个死信队列来说,同样可以看作延迟队列。

假设一个应用中需要将每条消息都设置为10 秒的延迟,生产者通过exchange.normal 这个交换器将发送的消息存储在queue.normal 这个队列中。

消费者订阅的并非是queue.normal 这个队列,而是queue.dlx 这个队列。当消息从queue.normal 这个队列中过期之后被存入queue.dlx 这个队列中,消费者就恰巧消费到了延迟10 秒的这条消息。

在AMQP 协议中,或者RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过前面
所介绍的DLX 和TTL 模拟出延迟队列的功能。

消息中间件(RabbitMQ)入门到进阶_第2张图片

优先级队列

配置队列最大优先级

具有高优先级的队列具有高的优先权,优先级高的消息具备优先被消费的特权。

可以通过设置队列的x - max - priority 参数来实现。

Map<String, Object> args =new HashMap<String, Object>() ;
args . put( "x-rnax-priority " , 10) ;
channel.queueDeclare( " queue . priority" , true , fa1se , false , args) ;

通过Web 管理页面可以看到" Pri" 的标识, 如图4-6 所示。

在这里插入图片描述

配置消息最大优先级

AMQP . BasicProperties . Bui1der builder = new AMQP . BasicProperties . Builder() ;
builder . priority(5) ;
AMQP . BasicProperties properties = builder . build () ;
channel.basicPub1ish( " exchange_priority" , " rk_priority" , properties , ("messages") . getBytes () ) ;

上面的代码中设置消息的优先级为5 。默认最低为0 ,最高为队列设置的最大优先级。

优先级高的消息可以被优先消费,这个也是有前提的: 如果在消费者的消费速度大于生产者的速度且Broker 中没有消息堆积的情况下, 对发送的消息设置优先级也就没有什么实际意义。

因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker 中至多只有一条消息,对于单条消息来说优先级是没有什么意义的。

消费端要点介绍

消息分发

1.basicQos简介

当RabbitMQ 队列拥有多个消费者时,队列收到的消息将以轮询(round-robin )的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者。这种方式非常适合扩展,而且它是专门为并发程序设计的。如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可。

很多时候轮询的分发机制也不是那么优雅。默认情况下,如果有n 个消费者,那么RabbitMQ会将第m 条消息分发给第m%n (取余的方式)个消费者, RabbitMQ 不管消费者是否消费并己经确认(Basic.Ack) 了消息。试想一下,如果某些消费者任务繁重,来不及消费那么多的消息,而某些其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了所分配到的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降。

那么该如何处理这种情况呢?这里就要用到channel.basicQos(int prefetchCount)这个方法,如前面章节所述, channel.basicQos 方法允许限制信道上的消费者所能保持的最大未确认消息的数量。

举例说明,在订阅消费队列之前,消费端程序调用了channel.basicQos(5) ,之后订阅了某个队列进行消费。RabbitMQ 会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,如果达到了所设定的上限,那么RabbitMQ 就不会向这个消费者再发送任何消息。直到消费者确认了某条消息之后, RabbitMQ 将相应的计数减1,之后消费者可以继续接收消息,直到再次到达计数上限。这种机制可以类比于TCP-IP中的"滑动窗口"。

2.channel.basicQos 有三种类型的重载方法

Basic.Qos 的使用对于拉模式的消费方式无效.

(1) void basicQos(int prefetchCount) throws IOException;

(2) void basicQos( int prefetchCount , boo1ean global) throws IOException;

(3) void basicQos(int prefetchSize , int prefetchCount , boolean global) throws IOException ;

前面介绍的都只用到了prefetchCount 这个参数,当prefetchCount 设置为0 则表示
没有上限。还有prefetchSize 这个参数表示消费者所能接收未确认消息的总体大小的上限,
单位为B ,设置为0 则表示没有上限。

消息中间件(RabbitMQ)入门到进阶_第3张图片

同时使用两种global 的模式,则会增加RabbitMQ的负载,因为RabbitMQ 需要更多的资源来协调完成这些限制。如无特殊需要,最好只使用global 为false 的设置,这也是默认的设置。

生产者确认——发送方确认机制

生产者将信道设置成confirm (确认)模式,一旦信道进入confmn 模式,所有在该信道上
面发布的消息都会被指派一个唯一的IDC 从l 开始),一旦消息被投递到所有匹配的队列之后,
RabbitMQ 就会发送一个确认CBasic.Ack) 给生产者(包含消息的唯一ID) ,这就使得生产
者知晓消息已经正确到达了目的地了。

异步confirm

在客户端Channel 接口中提供的addConfirmListener 方法可以添加ConfirmListener 这个回调接口,这个ConfirmListener 接口包含两个方法: handleAck 和handleNack ,分别用来处理RabbitMQ 回传的Basic.Ack 和Basic.Nack 。

在这两个方法中都包含有一个参数deliveryTag (在publisher confirm 模式下用来标记消息的唯一有序序号)。我们需要为每一个信道维护一个" unconfirm " 的消息序号集合, 每发送一条消息,集合中的元素加1 。每当调用ConfirmListener 中的h andleAck 方法时, " uncon曲m " 集合中删掉相应的一条(multiple 设置为false ) 或者多条(mu l tipl e 设置为true ) 记录。从程序运行效率上来看,这个" unconfrrm "集合最好采用有序集合Sorted S et 的存储结构。

channel . confirmSelect() ;
channel . addConfirmListener(new ConfirmListener ( ) (
}) ;
public void handleAck(long deliveryTag , boolean multiple)
throws IOException (
System.out . println( "Nack, SeqNo : " + deliveryTag
+ ", multiple : " + multiple) ;
if (multiple) (
confirmSet . headSet(deliveryTag - 1 ) . clear( );
} else (
confirmSet . remove(deliveryTag) ;
public void handleNack (long deliveryTag, boolean multiple)
throws IOException {
if (multiple) (
confirmSet . headSet (deliveryTag - 1 ) . c l ear ();
} e1se (
confirmSet.remove(deliveryTag ) ;
//注意这里需要添加处理消息重发的场景
//下面是演示一直发送消息的场景
while (true) (
long nextSeqNo = channel . getNextPublishSeqNo ( ) ;
channel . basicPublish (ConfirmConfig . e xchangeName, Conf 工rmConfig . routingKey ,
MessageProperties . PERSISTENT_TEXT_PLAIN,
ConfirmConfig .msg 10B . getBytes ());
confirmSet . add(nextSeqNo) ;
}

RabbitMQ管理

权限&用户管理

创建一个新的vhost

rabbitmqctl add vhost {vhost}

大括号里的参数表示vhost 的名称。

罗列vhost

rabbitmqctl list vhosts [vhostinfoitem...]
[root@nodel -]# rabbitmqctl list vhosts name tracing
Listing vhosts
vhostl false
/ false
目前vhostinfoitem 的取值有2 个。
name: 表示vhost 的名称。
tracing: 表示是否使用了RabbitMQ 的trace 功能

删除vhost

rabbitmqctl delete_vhost {vhost}

,其中大括号里面的参数表示vhost 的名称。删除一个vhost 同时也会删除其下所有的队列、交换器、绑定关系、用户权限、参数和策略等信息。

权限控制

rabbitmqctl set permissions [-p vhost] {user} {conf} {write) {read}

授予root 用户可访问虚拟主机vhost1,并在所有资源上都具备可配置、可写及可读的权限,
示例如下:
[root@nodel -]# rabbitmqctl set_permissions -p vhost1 root ".*" ".*" ".*"

授予root 用户可访问虚拟主机vhost2 , 在以"queue" 开头的资源上具备可配置权限, 并在
所有资源上拥有可写、可读的权限, 示例如下:
[root@node l -]# rabbitmqctl set_permi ssions -p vhost2 root " ^ queue.*" " .*" ".*"
其中各个参数的含义如下所述。

vhost : 授予用户访问权限的 vhost 名称,可以设置为默认值,即 vhost 为 “/”

user: 可以访问指定vhost 的用户名。

~conf: 一个用于匹配用户在哪些资源上拥有可配置权限的正则表达式。

~write: 一个用于匹配用户在哪些资源上拥有可写权限的正则表达式。

~read : 一个用于匹配用户在哪些资源上拥有可读权限的正则表达式。

清除权限的命令

rabbitmqctl clear_permissions [-p vhost] {username}

清除权限也是在vhost 级别对用户而言的。

列举权限信息。

第一个命令是 ,用来显示虚拟主机上的权限。

rabbitmqctl list permissions [-p vhost]

第二个命令是,用来显示用户的权限。

rabbitmqctl list user permissions {username}

创建用户

rabbitmqctl add user {username} {password} 

用户密码

更改指定用户的密码

rabbitmqctl change_password {username} {newpassword}

清除密码

rabbitmqctl clear password {username}

用密码验证用户

rabbitmqctl authentiçate_user {userηame} {password}

用户删除

rabbitmqctl delete user {username)

罗列当前用户

rabbitmqctl list users
示例如下:
[root@nodel - ]# rabbitmqctl list users
Listing users
guest [administrator]
root []

每个结果行都包含用户名称,其后紧跟用户的角色(tags) 。

用户的角色

用户的角色分为5 种类型。
~none: 无任何角色。新创建的用户的角色默认为none 。

~management: 可以访问Web 管理页面。Web 管理页面在5.3 节中会有详细介绍。
policymaker: 包含management 的所有权限,并且可以管理策略( Policy) 和参数( Parameter )。

~monitoring: 包含management 的所有权限,并且可以看到所有连接、信道及节点相关的信息。

~administartor: 包含monitoring 的所有权限,井且可以管理用户、虚拟主机、权限、策略、参数等。administator 代表了最高的权限。

用户角色的设置:tag 参数用于设置0 个、1个或者多个的角色,设置之后任何之前现有的身份都会被删除。

rabbitmqctl set user tags {username} {tag ...}

Web端管理

RabbitMQ management 插件可以提供Web 管理界面用来管理如前面所述的虚拟主机、用
户等,也可以用来管理队列、交换器、绑定关系、策略、参数等,还可以用来监控RabbitMQ
服务的状态及一些数据统计类信息。

在使用Web 管理界面之前需要先启用RabbitMQ management 插件。RabbitMQ 提供了很多
的插件,默认存放在$RABBITMQ HOME /plugins 目录下。

启动插件是使用rabbitmq-plugins enable [plugin - name]

关闭插件的命令是rabbitmq - plugins disable [plugin-name]

RabbitMQ managmenet插件启动

执行rabbitmq-plugins enable rabbitmq_management 命令来开启RabbitMQ
managmenet 插件:

[root@node1 -]# rabbitmq-plugins enable rabbitmq_management
The fol1owing plugins have been enabled:
amqp client
cowlib
cowboy
rabbitmq_web_dispatch
rabbitmq_management_agent
rabbitmq_management
Applying p1ugin configuration to rabbit@node1... started 6 plugins .

可以通过rabbitmq-plugins list 命令来查看当前插件的使用情况, 如下所示。其中标记为[E * ]的为显式启动,而[e*] 为隐式启动, 如显式启动。

开启rabbitmq_management 插件之后还需要重启RabbitMQ 服务才能使其正式生效。之后就可以通过浏览器访问http://localhost: 15672。这样会出现一个认证登录的界面,可以通过默认的guestlguest 的用户名和密码来登录。

RabbitMQ management 插件关闭

rabbitmq_management 插件对应的关闭命令是rabbitmq-plugins disable rabbitmq_management ,示例参考如下:

[root@nodel - ]# rabbitmq-plugins disable rabbitmq management
The following plugins have been disabled:
amqp client
cowlib
cowboy
rabbitmq web dispatch
rabbitmq management agent
rabbitmq management
Applying plugin configuration to rabbit@nodel... stopped 6 plugins.

应用与集群管理

应用服务

停止运行RabbitMQ 的Erlang 虚拟机和RabbitMQ 服务应用

rabbitmqctl stop [pid_file]

如果指定了pid_file ,还需要等待指定进程的结束。

其中pid file 是通过调用rabbitmq-server 命令启动RabbitMQ 服务时创建的,默认情况下存放于Mnesia 目录中,可以通过RABBITMQ_PID_FILE这个环境变量来改变存放路径。

注意,如果使用rabbitmq -ser ver -detach 这个带有-detach 后缀的命令来启动RabbitMQ 服务则不会生成pid file 文件。

rabbitmqctl shutdown
用于停止运行RabbitMQ 的Erlang 虚拟机和RabbitMQ 服务应用。执行这个命令会阻塞直到Erlang 虚拟机进程退出。如果RabbitMQ 没有成功关闭,则会返回一个非零值。这个命令和rabbitmqctl stop 不同的是,它不需要指定pid file 而可以阻塞等待指定进程的关闭。

RabbitMQ 服务应用

rabbitmqctl stop_ app

停止RabbitMQ 服务应用,但是Erlang 虚拟机还是处于运行状态。此命令的执行优先于其他管理操作(这些管理操作需要先停止RabbitMQ 应用),比如rabbitmqctl reset 。

rabbitmqctl start_ app

启动RabbitMQ 应用。此命令典型的用途是在执行了其他管理操作之后,重新启动之前停止的RabbitMQ 应用,比如rabbitmqctl reset 。

rabbitmqctl wait [pid_file]

等待RabbitMQ 应用的启动。它会等到pid_file 的创建,然后等待pid_file 中所代表的进程启动。当指定的进程没有启动RabbitMQ 应用而关闭时将会返回失败

rabbitmqctl reset

将RabbitMQ 节点重置还原到最初状态。

包括从原来所在的集群中删除此节点,从管理数据库中删除所有的配置数据,如己配置的用户、vhost 等,以及删除所有的持久化消息。

执行rabbi tmqctl reset 命令前必须停止RabbitMQ 应用(比如先执行rabbitmqctlstop_app) 。

rabbitmqctl force reset

强制将RabbitMQ 节点重置还原到最初状态。

不同于rabbitmqctl reset 命令,rabbitmqctl force reset 命令不论当前管理数据库的状态和集群配置是什么,都会无条件地重直节点。它只能在数据库或集群配置己损坏的情况下使用。

与rabbitmqctl reset命令一样,执行rabbitmqctl force reset 命令前必须先停止RabbitMQ 应用。

rabbitmqctl rotate_logs {suffix}

指示RabbitMQ 节点轮换日志文件。RabbitMQ 节点会将原来的日志文件中的内容追加到"原始名称+后缀"的日志文件中,然后再将新的日志内容记录到新创建的日志中(与原日志文件同名)。

当目标文件不存在时,会重新创建。如果不指定后缀suffix. 则日志文件只是重新打开而不会进行轮换。

rabbitmqctl hipe_compile {directory}

将部分RabbitMQ 代码用HiPE (HiPE 是指High Performance Erlang ,是Erlang 版的JIT)编译,井且将编译的.beam 文件( beam 文件是Erlang 编译器生成的文件格式,可以直接加载到Erlang 虚拟机中运行的文件格式〉保存到指定的文件目录中。

如果这个目录不存在则会自行创建。如果这个目录中原本有任何. beam 文件,则会在执行编译前被删除。

如果要使用预编译的这些文件,则需要设置RABBITMQ SERVER CODE PATH 这个环境变量来指定hipe_ compile
调用的路径。

集群管理

rabbitmqctl joio_cluster {cluster_node} [--ram]

将节点加入指定集群中。在这个命令执行前需要停止RabbitMQ 应用井重置节点

rabbitmqctl cluster_status

显示集群的状态

rabbitmqctl change_cluster_node_type {disclram}

修改集群节点的类型

rabbitmqctl forget_cluster_ node [--offiine]

将节点从集群中删除,允许离线执行。

Rabbitmq插件服务

stomp——创建客户端消费消息

websocket是实现客户端和服务端全双工通信的底层协议,sockjs是实现websocket的底层协议,属于js类库,stompjs是高层协议,在底层协议的基础上定义消息语义,通过amqp提供的stomp插件,与rabbitmq服务端建立连接,在客户端消费消息。

更多详情参考:

stompjs: https://segmentfault.com/a/1190000006617344

js连接RabbitMQ: https://www.cnblogs.com/puyangsky/p/6666624.html

在前端建立websocket连接,与rabbitmq相连前,要启动rabbitmq的stomp插件服务

shell命令:

rabbitmq-plugins enable rabbitmq_management rabbitmq_web_stomp rabbitmq_stomp rabbitmq_web_stomp_examples

//重启rabbitmq服务
rabbitmq-server restart -detached

前端建立websocket连接,调用后端接口动态创建队列,使用Stompjs创建client,用client来消费队列中的消息。

安装 sockjs-client、stompjs;在这儿要注意一下,在"stompjs": "^2.3.3"这个版本发现,引入stompjs会报一个
net模块找不到,需要在stompjs模块根目录下执行npm install net

对于node.js,在导入两个model之前需要安装model

npm install stompjs --save
npm install sockjs-client --save
npm install net --save

展示路灯页面功能实现的ts文件,建立websocket连接,订阅队列中的消息并打印出来。

注意New Websocket()中的url是以ws开头结尾,ip地址是rabbitmq安装终端的ip,连接stomp客户端的默认端口是15674。后端连接rabbitmq的默认端口是5672。

client.connect()最后一位参数是rabbitmq上设置的虚拟机,如果没有设置,可以不填,默认为‘/’。

/queue/queuename:使用默认转发器订阅/发布消息,默认由stomp自动创建一个持久化队列

/amq/queue/queuename:与/queue/queuename的区别在于队列不由stomp自动进行创建,队列不存在失败

/topic/routing_key:通过amq.topic转发器订阅/发布消息,订阅时默认创建一个临时队列,通过routing_key与topic进行绑定

/temp-queue/xxx:创建一个临时队列(只能在headers中的属性reply-to中使用),可用于发送消息后通过临时队列接收回复消息,接收通过client.onreceive

/exchange/exchangename/[routing_key]:通过转发器订阅/发布消息,转发器需要手动创建

client.subscribe(destination,callback,headers) :订阅消息

client.send(destination,headers,body):发布消息

client.unsubscribe(id):取消订阅,id为订阅时返回的编号

client.onreceive:默认接收回调从临时队列获取消息

light-home.component.ts

import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';//导入两个model

//2019-04-30 lanseer : create queue dynamically
  createQueue(){
    const that=this;
    let consumerId = Math.round(Math.random()*10);
    const body = {
      'infoType' : 'light',
      'consumerId' : consumerId
    };
    
    that.rabbitmqService.createQueue(body)
    .subscribe(p => {
      that.queueName = p.queueName;
      console.log(that.queueName);
    });
  }

  //2019-4-30 lanseer : subscribe info from queue
  subscribeInfo(){
    const that=this;
    const url = 'ws://172.18.1.88:15674/ws';
    that.ws = new WebSocket(url);
    that.client = Stomp.over(that.ws);
    console.log(that.client);
    that.client.heartbeat.incoming=100;

    const connectCallback = function() {
      console.log(">>>connected success!");
      const subscribeCallback = function(data){
        if(data ! = null){
          console.log("[x] Received %s",JSON.parse(data));
          data.ack({ receipt: 'my-receipt' });
        }
      };
    that.client.subscribe('/amq/queue/'+that.queueName, subscribeCallback);
    };

    const errorCallback =  function(error) {
      console.log(error);
    };

    that.client.connect('guest', 'guest', connectCallback, errorCallback,'siid');
  

  }

rabbitmq.service.ts

调用后端接口服务,动态创建队列

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';


@Injectable()
export class RabbitmqService {
    constructor (private http : HttpClient){

    }

    

    createQueue(body): Observable {
        return this.http.post(`/msg_api/webSocket/createQueue`,body);
    }
}

如果当websockt连接打开后,却一直连接不上,可能是前端的url不对,或者后端在创建连接通道时没有使用正确的协议,如使用stomp做客户端连接,但是创建的的连接是基于AMQP协议。

打开rabbitmq web管理界面,进入overview,查看以下信息:

消息中间件(RabbitMQ)入门到进阶_第4张图片

如果是用stomp做客户端,消费消息,则后端在配置端口号(listening port)是61613,可以在application.propertities文件中设置spring.rabbitmq.port=61613。或自己自定义的属性也可以,如spring.amqp.port=61613。

前端创建Sockjs,这里的ip地址是安装rabbitmq的ip地址,假如是在本机安装,则是localhost,如果在linux虚拟机上安装,就是linux虚拟机的ip地址。前端对应的端口号在web contexts,web-stomp的端口号是15670。建立websocket连接时的url:http://172.18.1.88:15670/stomp。

如果后端显示连接不上,则重启rabbitmq服务

rabbitmq-server restart

在这里可以看见目前rabbitmq创建连接所使用的协议是AMQP协议,而非stomp协议。

消息中间件(RabbitMQ)入门到进阶_第5张图片

Firehose——追踪消息

在RabbitMQ 中可以使用Firehose 功能来实现消息追踪, Firehose 可以记录每一次发送或者消费消息的记录,方便RabbitMQ 的使用者进行调试、排错等。

Firehose 的原理是将生产者投递给RabbitMQ 的消息,或者RabbitMQ 投递给消费者的消息按照指定的格式发送到默认的交换器上。

这个默认的交换器的名称为amq.rabbitmq.trace ,它是一个topic 类型的交换器。

发送到这个交换器上的消息的路由键为publish.{ exchangenamel } 和deliver.{ queuenamel }。

其中exchangename 和queuename 为交换器和队列的名称,分别对应生产者投递到交换器的消息和消费者从队列中获取的消息。

开启Firehose 命令:

rabbitmqctl trace_on [-p vhost] 。

其中[-p vhost] 是可选参数,用来指定虚拟主机vhost。对应的关闭命令为rabbitmqctl trace off [-p vhost] 。

Firehose 默认情况下处于关闭状态,并且Firehose 的状态也是非持久化的,会在RabbitMQ 服务重启的时候还原成默认的状态。Firehose 开启之后多少会影响RabbitMQ 整体服务的性能,因为它会引起额外的消息生成、路由和存储。

在Firehose 开启状态下, 当有客户端发送或者消费消息时, Firehose 会自动封装相应的消息体,并添加详细的headers 属性。对于前面的将" 位ace test pay load. "这条消息发送到交换器exchange 来说, Firehore 会将其封装成如图内容。

消息中间件(RabbitMQ)入门到进阶_第6张图片

在消费queue 时, 会将这条消息封装成如图的内容:

消息中间件(RabbitMQ)入门到进阶_第7张图片

rabbitmq_tracing插件

rabbitrnq_tracing 插件相当于Firehose 的GUI 版本, 它同样能跟踪RabbitMQ 中消息的流入流出情况。rabbitrnq tracing 插件同样会对流入流出的消息进行封装, 然后将封装后的消息日志存入相应的trace文件之中。

可以使用rabbitrnq - plugins enable rabbitrnq tracing命令来启动rabbitrnqtracing 插件:

[root@nodel -]# rabbitmq- plugins enable rabbitmq_tracing
The following plugins have been enabled:
rabbitmq tracing
Applying plugin configuration to rabbit@node3 . . . started 1 plugin .

对应的关闭插件的命令是rabbitrnq-plugins disable rabbitrnq_tracing

在Web 管理界面" Admin " 右侧原本只有" Users"、" Virtual Hosts " 和" Policies" 这个三Tab 项, 在添加rabbitmq_tracing 插件之后,会多出" Tracing " 这一项内容, 如图所示。

消息中间件(RabbitMQ)入门到进阶_第8张图片

消息中间件(RabbitMQ)入门到进阶_第9张图片

在添加完trace 之后,会根据匹配的规则将相应的消息日志输出到对应的trace 文件之中,文件的默认路径为/var/tmp/rabbitmq-tracingo 可以在页面中直接点击"Trace log files"下面的列表直接查看对应的日志文件。

如图,添加了两个trace 任务。

消息中间件(RabbitMQ)入门到进阶_第10张图片

与其相对应的trace 文件如图所示。

消息中间件(RabbitMQ)入门到进阶_第11张图片

再添加完相应的trace 任务之后,会发现多了两个队列,如图所示。

消息中间件(RabbitMQ)入门到进阶_第12张图片

就以第一个队列amq.gen-MoyvSKQau9udet141UdQZw 而言,其所绑定的交换器就是
amq.rabbitmq.trace ,如图所示。

消息中间件(RabbitMQ)入门到进阶_第13张图片

由此可以看出整个rabbitmq_tracing 插件和Firehose 在实现上如出一辙,只不过rabbitmq tracing 插件比Firehose 多了一层GUI 的包装,更容易使用和管理。

再来补充说明Name 、Format 、Max payload bytes 、Pattern的具体含义。
Name ,顾名思义,就是为即将创建的trace 任务取个名称。
Format 表示输出的消息日志格式,有Text 和JSON 两种, Text 格式的日志方便人类阅读,
JSON 的格式方便程序解析。

Text 格式的消息日志参考如下:

====================================================================================
2017 - 10-24 9 : 37 : 04:412 : Message pub1ished
Node : rabbit@node1
Connection: 
Virtua1 host : /
User : root
Channe1 : 1
Exchange : exchange
Routing keys : [<< " rk " >>]
Routed queues: [<< " queue " >>]
Properties: [ (<< "de1ivery_mode " >> , signedint , 1) , {<< "headers " >> , tab1e , [] }]
Pay1oad :
trace test pay1oad .

JSON 格式的消息日志参考如下:

{
    " timestamp " : " 2017 - 10 - 24 9 : 37 : 04 : 412 " ,
	" type " : "published" ,
	" node " : " rabbit@node1 " ,
	" connection ": " " ,
	" vhost ": " /" ,
	" user " : " root " ,
	" channe1": 1 ,
	" exchange " : "exchange " ,
	" queue ": "none" ,
	" routed queues" : [ "queue" ] ,
	" routing keys " : [ " rk" ] ,
	" properties ": { "de1ivery mode ": 1 , "headers " : {} } ,
	" pay1oad" : "dHJhY2UgdGVzdCBwYXlsb2FkLg=="
}

JSON 格式的pay10ad C 消息体〉默认会采用Base64 进行编码,如上面的" trace test pay1oad."
会被编码成" dHJhY2UgdGVzdCBwYX1sb2FkLg=="。

Max payload bytes 表示每条消息的最大限制,单位为B 。比如设置了此值为10 ,那么当有超过10B 的消息经过RabbitMQ 流转时, 在记录到trace 文件时会被截断。如上Text 日志格式中" trace test payload." 会被截成"trace test " 。

Pattern 用来设置匹配的模式, 和Firehose 的类似。如"#"匹配所有消息流入流出的情况,即当有客户端生产消息或者消费消息的时候, 会把相应的消息日志都记录下来; " publish.俨匹配所有消息流入的情况; "deliver.#"匹配所有消息流出的情况。

Rabbitmq运维

服务日志

RabbitMQ 的日志默认存放在$RABBITMQ HOME/var/log/rabbitmq 文件夹内。在这个文件夹内Rabbi tMQ 会创建两个日志文件: RABBITMQ_NODENAME-sasl.log 和RABBITMQNODENAME.log 。

当RabbitMQ 记录Erlang 相关信息时,它会将日志写入文件RABBITMQ_NODENAME-sasl.log 中。举例来说,可以在这个文件中找到Erlang 的崩横报告,有助于调试无法启动的RabbitMQ 节点。

如果想查看RabbitMQ应用服务的日志,则需要查阅RABBITMQ_NODENAME.log 这个文件,所谓的RabbitMQ 服务日志指的就是这个文件。

在执行任何RabbitMQ 操作之前,都会打开一个新的窗口运行于

tail -f $RABBITMQ HOME/var/log/rabbitmq/rabbit@$HOSTNAME.log -n 200

命令来实时查看相应操作所对应的服务日志是什么.

RabbitMQ 中可以通过rabbitmqctl rotate l ogs {suffix} 命令来轮换日志.

比如手工切换当前的日志 : rabbitmqct1 rotate 10gs .bak

之后可以看到在日志目录下会建立新的日志文件,并且将老的日志文件以添加".bak" 后缀的方式进行区分保存 :
[root@nodel rabbitmql# ls - a1
-rw-r–r-- 1 root root 0 Ju1 23 00 : 50 [email protected]
-rw-r–r-- 1 root root 22646 Ju1 23 00 : 50 [email protected]
-rw-r–r-- 1 root root o Ju1 23 00:50 [email protected]
-rw-r–r-- 1 root root o Ju1 23 00 : 50 [email protected]

RabbitMQ 默认会创建一些交换器,其中arnq.rabbitrnq.log 就是用来收集RabbitMQ 日志的,集群中所有的服务日志都会发往这个交换器中。这个交换器的类型为topic ,可以收集如前面所说的debug 、info 、waming 和error这4 个级别的日志。

我们创建4 个日志队列queue.debug 、queue .info 、queue.waming 和queue.eηor ,分别采用debug 、info 、waming 和error 这4 个路由键来绑定arnq.rabbitrnq.log 。如果要使用一个队列来收集所有级别的日志,可以使用"#"这个路由键。

public class ReceiveLog {
	public static void main(String[] args) {
		try {
			//省略创建connection
			Channel channelDebug = conncection.createChannel();
			Channel channellnfo = conncection.createChannel() ;
			Channel channelWarn = conncection.createChannel() ;
			Channel channelError = conncection.createChannel() ;

			// 省略channel.basicQos( int prefetch_count) ;
			channelDebug.basicConsume( " queue . debug " , false , " DEBUG" ,
new ConsumerThread(channelDebug ) ) ;

			channellnfo.basicConsume( "queue . info " , false , " INFO " ,
new ConsumerThread(channellnfo)) ;

			channelWarn.basicConsume( " queue . warning" , false , "WARNING " ,
new ConsumerThread(channelWarn)) ;

			channelError.basicConsume( "queue.error" , false , "ERROR" ,
new ConsumerThread(channelError)) ;

		} 
		catch (IOException e) {
			e . printStackTrace() ;
		} 
		catch (TimeoutException e) {
			e.printStackTrace() ;
		}
	}
	public static class ConsumerThread extends DefaultConsumer {
		public ConsumerThread(Channel channel ) {
			super(channel) ;
		}
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope ,
AMQP.BasicProperties properties ,byte[] body) throws IOException {
				String log = new String(body) ;
				System . out.println( "=" +consumerTag+ " REPORT====\n " +log );
				//对日志进行相应的处理
				getChannel().basicAck(envelope.getDeliveryTag( ) , false) ;
			}
	}
}

你可能感兴趣的:(java,web)