Java消息服务(Java Message Service,JMS)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间或分布式系统中发送消息,并进行异步通信,Java 消息服务是一个与具体平台无关的API,绝大多数MON提供商都对JMS提供了支持。
Java 消息服务的规范包括两种消息模式,点对点和发布者/订阅者,许多提供商都支持这一通用框架,因此 ,程序员可以在他们的分布式软件中实现面向消息的操作,这些操作将具有不同面向消息中间件产品的可移植性。
Java消息服务支持同步和异步的消息处理,在某种场景下,异常消息是必要的,而且比同步消息操作更加便利。
Java消息服务支持面向事件的方法接收消息,事件驱动的程序设置现在被广泛认为是一种富有成效的程序设计范例,程序员们对其都相当的熟悉。
在应用系统开发时,Java消息服务可以推迟选择面对消息中间件产品,也可以在不同的面向消息中间件之间进行切换。
JMS 的独立使用
尽管大多数的Java消息服务的使用都会跟Spring相结合,但是我们还是非常有必要了解消息的独立使用方法,这对于我们理解消息的实现原理以及后续与Spring整合实现的分析都非常重要,当然在消息服务的使用前,需要先开启消息服务器,如果是Windows系统,则可以直接双击ActiveMQ安装目录下的bin目录中的activemq.bat文件来启动消息服务器。
消息服务的使用除了要开启消息服务器外,还需要构建消息的发送端和接收端,发送端主要用来将包含业务逻辑的消息发送到消息服务器,而消息接收端则用于将服务器中的消息提取并进行相应的处理。
- 发送端实现
发送端主要用于发送消息到消息服务器,以下为发送消息测试,尝试发送3条消息到消息服务器,消息的内容为"大家好这是在测试"
public class Producer { public static void main(String[] args) throws IOException, Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); channel.exchangeDeclare("producer-customer-test", BuiltinExchangeType.DIRECT,true); //5 创建队列Queen /**参数介绍: * queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * autoDelete: 当没有consumer时候是否删除队列 * internal:设置是否是内置的,如果设置为true,则表示是内置的交换器,审稿意见程序无法直接 * 把消息发送到这个接口中,只能通过交换器路由到交换器这种方式 * arguments:配置的基本参数 */ channel.queueDeclare("hello_world", true, false, false, null); channel.queueBind("hello_world", "producer-customer-test", "routing_key_1"); //6 发送消息到队列 /**参数介绍: * basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body) *exchange: 交换机的名称,简单模式下交换机会使用默认的 routingKey:路由名称,如果使用默认交换机要和队列名相同 *props 配置信息 body:真是发送的消息数据 */ String constant = "大家好这是在测试"; for(int i = 0 ;i < 3;i ++){ String body = constant + i; channel.basicPublish("producer-customer-test", "routing_key_1", null, body.getBytes()); } //7 释放连接资源 channel.close(); connection.close(); } }
上面的函数实现很容易让我们联想到数据库的实现,在函数开始时需要一系列冗余但是又必不可少的用于连接的代码,而其中真正用于发送消息的代码其实很简单。
RabbitMQ生产者的运转整个流程。
- 生产者连接RabbitMQ Broker建立一个连接(connection),开启一个信道(Channel)
- 生产者声明一个交换器,并设置相关属性,比如交换器类型,是否持久化等。
- 生产者声明一个队列并设置相关属性,比如是否排他,是否持久化,是否自动删除。
- 生产者通过路由键将交换器和队列绑定起来。
- 生产者发送消息至RabbitMQ Broker,其中包含了路由键,交换器等信息。
- 相应的交换器根据接收到的路由键查找匹配的队列。
- 如果没有找到,则根据生产者配置的属性选择性的丢弃还是回退给生产者
- 关闭信道
- 关闭连接
- 接收端实现
接收端主要用于连接消息服务器并接收服务器上的消息。
public class Consumer { public static void main(String[] args) throws IOException, Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); channel.exchangeDeclare("producer-customer-test", BuiltinExchangeType.DIRECT,true); //5 创建队列Queen /**参数介绍: * queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * autoDelete: 当没有consumer时候是否删除队列 * arguments:配置的基本参数 */ channel.queueDeclare("hello_world",true,false,false,null); channel.queueBind("hello_world", "producer-customer-test", "routing_key_1"); //6 从消息队列中消费 /**参数介绍: *basicConsume(String queue, boolean autoAck, Consumer callback) * queue: 消费的队列名称 * autoAck:是否自动消费确认,收到消息确认 * callback:回调对象 */ com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel){ //这是一个回调方法,当收到消息后会自动执行该方法 /** * @param consumerTag :标识 * @param envelope: 获取交换机,路由等信息 * @param properties:配置信息 * @param body:真是的数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println(""+consumerTag); System.out.println("getExchange+"+envelope.getExchange()); System.out.println("getRoutingKey+"+envelope.getRoutingKey()); System.out.println("properties"+properties); System.out.println("body"+new String(body,"utf-8")); } }; channel.basicConsume("hello_world",true,consumer); //7 消费者不关闭连接 } }
消费者接收消息的过程
- 消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
- 消费者向RabbitMQ Broker请求消费者相应的队列的消息,可能会设置相应的回调函数做一些准备。
- 等待RabbitMQ Broker 回应并投递相应的队列的消息,消费者接收消息。
- 消费者确认(ack)接收到消息
- RabbitMQ从队列中删除相应的己经被确认的消息。
- 关闭信道。
- 关闭连接。
整个消息的发送与接收过程非常的简单,但是其中却掺杂着大量的冗余代码,比如Connection的创建与关闭,Session的创建与关闭等。
程序的测试是顺序的, 首先开启发送端,然后向服务器发送消息,接收再开启接收端,不出意外,就会接收到发送端发出的消息。
上述过程中,我们模似的就是一个生产者消费者案例,什么是队列,什么是交换机,这又是rabbitMQ中的什么概念呢?
整个过程其实很简单,生产者创建消息,消费者接收消息,你的应用程序要么作为生产者,向其他应用发送消息,或者作为消费者,接收消息,也可以是两者之间进行切换,不过在此之前,它必需先建立一条信道(chnnel)等,什么是信道(channel)呢?
如图2-9所示,我们引入了两个新的概念,Connection和Channel,我们知道无论是生产者还是消费者,都需要和RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection,一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID,信道是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP都是通过信道完成的。
你必需首先连接Rabbit,才能消费或者发布消息,你在应用程序和Rabbit代理服务器之间创建一条TCP连接,一旦TCP连接打开(你通过了认证),应用程序就可以创建一条AMQP信道,信道是建立在一个真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发送出去的,每条信道都会被指派一个唯一的ID(AMQP库会帮你记住这个ID的),无论是发布消息,订阅队列或者接收消息,这些动作都是通过信道完成的,你也许会问为什么我们需要信道呢,为什么不直接通过TCP连接发送AMQP命令呢?主要原因在于对操作系统来说建立和销毁TCP会话是非常昂贵的开销,假设应用程序从队列消费消息,并根据服务需要合理的调度线程,假设你只进行TCP连接,那么每个线程都需要自行连接到Rabbit,也就是说高峰期有每秒成百上千连接,这不仅造成TCP连接的巨大浪费,而且操作系统每秒也就只能建立这点数量的连接了,因此,你的应用可能很快就达到性能瓶颈了,如果我们为所有的线程只使用一条TCP连接以满足性能方面的要求,但又能确保每个线程的私密性,就像拥有独立连接一样的话,那不就非常完美了吗?这就是要引入信息概念的原因,线程启动后,会在现在的连接上创建一条信道,也就获得了连接到Rabbit 上的私密通信路径,而不会给操作系统TCP栈造成额外的负担,如下图所示
因此,你可以每秒成百上千次的创建信道而不会影响操作系统,在一条TCP连接上创建多少条信道是没有限制的,把它想象成一束光纤电缆就可以了。
每条电缆的光纤束都可以传输(就像一条信道)。一条电缆有许多光纤束,允许所有的连接的线程通过多条光纤束同时进行传输和接收,TCP连接就像电缆,而AMQP信道就像一条条独立的光纤束。
重要的是记住消费者和生产者是消息发送和消息接收概念的体现,而非客户端和服务端,从总体来讲,消息通信,特别是AMQP,可以被当作加强版的传输层,使用信道,你能够根据应用需要,尽可能多的创建并传输传输层,而不会被TCP连接约束限制,当你理解了这些概念时,你就能把RabbitMQ看成是软件路由器了。
你己经对消费者和生产者有了一定的了解,但是你还需要理解什么是队列,从概念上来讲,AMQP消息路由必需有三个部分,交换器,队列和绑定,生产者把消息发布到交换器上,消息最终到达队列,并被消费者接收,绑定决定了消息如何从路由器到特定的队列,在你研究交换器和绑定之前,需要先理解队列的概念和工作原理。
队列
Queue:队列,是RabbitMQ的内部对象,用于存储消息,请看下图
RabbitMQ中的消息只能存储在队列中,生产者生产消息最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都接收到所有的消息并处理。
Rabbit是不支持队列层面的广播消费,如果需要广播消费,需要在其上进行二次开发,处理逻辑会变得异常复杂,同时也不建义这么做。
交换器,路由键,绑定
Exchange: 交换器,我们暂时可以理解成生产者将消息投递到队列中,实际上这个在RabbitMQ中不会发生,真实情况是,生产者将消息发送到Exchange,由交换器将消息路由到一个或多个队列中,如果路由不到,或许会返回给生产者,或者直接丢弃,这里可以将RabbitMQ中的交换器看作是一个简单的实体。
RoutingKey: 路由键,生产者将消息发送给交换器的时候,一般会指定一个RountingKey,用来指定这个消息的路由规则,而这个RountingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里。
Binding : 绑定,RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就知道如何正确的将消息路由到队列了
生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会将路由到对应的队列中,在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindKey,BindKey并不是在所有的情况下都生效的,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中。
以上代码声明了一个direct类型的交换器,然后将交换器和队列绑定起来,注意上面使用的参数"routingKey",本该使用BindingKey和channel.queueBind方法和channel.basicPublish方法同样使用了RoutingKey,在direct交换器的类型下,我们认为RoutingKey和BindingKey是同一个东西,需要完全匹配才能使用,但是在topic交换器的类型下,RoutingKey和BindingKey之间需要做模糊匹配,两者并不是相同的。
BindingKey其实也属于路由键中的一种,官方解释:the routing key to use for the binding。
可以翻译为:在绑定的时候使用路由键,大多数时候,包括官方文档和RabbitMQ Java API都把BindingKey和RoutingKey 看作是RountingKey,为了避免混淆,可以这么理解 。
- 在使用绑定的时候,其中需要的路由键是BindingKey,涉及客户端方法如:channel.exchangeBind,channel.queueBind,对应的AMQP命令。为Exchange.Bind,Queue.Bind。
- 在发送消息的时候,其中需要的路由键是RoutingKey,涉及的客户端方法如下channel.basicPublish,对应的AMQP命令为Basic.Publish。
就像我们之前在讨论生产者和消费时说的那样,队列就如同具名邮箱,消息最终达到队列中并等待消费,消费者通过以下两种方式从特定的队列中接收消息:
(1)通过AMQP的basic.consume命令订阅,这样做会将信道置为接收模式,直到取消队列的订阅为止,订阅消息后,消费者在消费(或者拒绝)最近接收的那条消息后,就能从队列中(可用的)自动接收下一条消息,如果消费者处理队列消息,并且/或者需要在消息一到队列时就自动接收的话,那你应该使用basic.consume。
(2)某些时候,你只想从队列中获得单条消息而不是持续订阅,向队列请求单条消息是通过AMQP的basic.get命令实现的,那样做可以让消费者接收队列中的下一条消息,如果要获得更多消息的话,需要再次发送basic.get命令,你不应该将basic.get放在一个循环里来代替basic.consume。因为这样做会影响Rabbit的性能,大致上来讲basic.get命令会订阅消息,获得单条消息,然后取消订阅,消费者始终使用basic.consume来实现高吞吐量。
如果至少有一个消费者订阅了队列的话,消息会立即发送给这些订阅的消费者,但是如果消息到达了无人订阅的队列呢?在这种情况下,消息会在队列中等待,一旦有消费者订阅到该队列,那么队列上的消息就会发送给消费者,更有趣的问题是,当有多个消费者订阅到同一队列时,消息是如何分发的。
当Rabbit队列拥有多个消费者时,队列收到的消息将以循环(round-robin)的方式发送给消费者,每条消息会发送给一个订阅的消费者,假设有seed_bin队列,消费者Farmer Bob和消费者Farmer Esmeralda订阅到seed_bin队列,
- 消息Message_A 到达seed_bin队列。
- RabbitMQ 把消息Message_A发送给Farmer Bob 。
- Farmer Bob确认接收到了消息Message_A。
- RabbitMQ把消息Message_A从seed_bin中删除。
- 消息Message_B到达seed_bin队列。
- RabbitMQ把消息Message_B发送给Farmer Esmeralda。
- Farmer Esmeralda 确认接收到消息Message_B。
- RabbitMQ把消息Message_B从seed_bin中删除。
你可能注意到了Farmers Bob 和Esmeralda做了一些我们还未讨论过的事情,他们对消息进行了确认,消费者接收到一条消息都必需进行确认,消费者必需通过AMQP的basic.ack命令显式的向RabbitMQ发送一个确认,或者在订阅到队列时候就将auto_ack参数设置为true,当设置了auto_ack时,一旦消费者接收到了消息后,RabbitMQ会自动视其确认了消息,需要记住的是,消费者对消息的确认和告诉生产者消息己经被接收了这两件事情毫不相关,因此,消费都通过确认命令告诉RabbitMQ它己经正确的接收到了消息,同时RabbitMQ才能安全的把消息从队列中删除。
如果消费者接收到一条消息,然后确认之前从Rabbit断开连接(或者从队列上取消订阅),RabbitMQ会认为这条消息没有分发,然后重新分发给下一个订阅的消费者,如果你的应用程序崩溃了,这样做可以确保消息会被发送给另一个消费者进行处理。另一方面,如果应用程序有bug,而忘记确认消息的话,Rabbit将不会给消费者发送更多消息了,这是因为在上一条消息被确认之前,Rabbit 会认为这个消费者并没有准备好接收下一条消息,你可以好好的利用这一点,如果处理消息内容非常耗时,则你的应用程序可以延迟确认该消息,直到消息处理完成,这样可以防止Rabbit持续不断的消息涌向你的应用导致过载。
在收到消息后,如果你想要明确拒绝而不是确认收到该消息的话,该如何处理呢?举例来说,假设在处理消息的时候遇到了不可恢复的错误,但是由于硬件问题,只影响到当道的消费者(这就是一个很好的例子,直到消息处理完成之前,你绝不进行确认),只要消息尚未确认,你有以下的两个选择。
(1)把消费者从RabbitMQ服务器断开连接,这会导致RabbitMQ自动重新把消息入队并发送给另一个消费者,这样做的好处是所有的RabbitMQ版本都支持,缺点是,这样连接/断开连接的方式会额外的增加RabbitMQ的负担 (如果消费者在处理每条消息时都遇到错误的话,会导致潜在的重大负荷)
(2)如果你正在使用RabbitMQ 2.00 或者更新的版本,那就使用AMQP的basic.reject命令,顾名思义,basic.reject允许消费者拒绝RabbitMQ发送的消息,如果把reject命令的requeue参数设置成true的话,RabbitMQ会将消息重新发送给下一个订阅的消费者,如果设置为false的话,RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者,你也可以通过对消息的确认的方式来简单的忽略该消息(这种忽略消息的方式优势在于所有的版本的RabbitMQ都支持),如果你检测到一条格式错误的消息而任何一个消费者都无法处理的时候,这样做就十分有用。
【注意】:当丢弃一条消息时,为什么要使用basic.reject命令,并将requeue参数设置成false来替代确认消息呢?在将来的RabbitMQ版本中会支持一个特殊的“死信”(dead letter)队列,用来存放那些被拒绝而不重入队列的消息,死信队列让你通过检测拒绝/未送达消息来发现问题,如果应用程序想自动从死信队列功能中获益的话,需要使用reject命令并将requeue参数设置为false。
- 10条消息,2个普通消费者,1个拒绝消费者,requeue=true,消费前
- 消费后,经过轮询消费者A消费了1、4、7、10,消费者B消费了2、5、8,拒绝消费者消费了3、6、9
- 3、6、9被拒绝之后,返回队列中重新投递
- 3进入消费者B,6进入拒绝消费者,9进入消费者A;然后6再次被拒绝,进入队列再次重新投递,进入消费者B
- 创建生产者代码
public class Producer { public final static String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); //在信道中设置交换器(这里选择直接交换器direct) channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //在消费者中申明队列 //申明路由键及消息体 for (int i = 0; i < 10; i++) { String route = "error"; String msg = "Hello World" + (i + 1); //发布消息(设置交换器、路由键、参数、消息内容) channel.basicPublish(EXCHANGE_NAME, route, null, msg.getBytes()); System.out.println("Send:" + route + ";" + msg); } //关闭信道 channel.close(); //关闭连接 connection.close(); } }
- 创建消费者AConsumerA
/** * direct类型交换器的消费者 */ public class ConsumerA { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); //在信道中设置交换器 channel.exchangeDeclare(Producer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //申明队列 String queueName = "rejectqueue"; channel.queueDeclare(queueName, false, false, false, null); //绑定:将队列与交换器通过路由键绑定 String routeKey = "error"; channel.queueBind(queueName, Producer.EXCHANGE_NAME, routeKey); System.out.println("waiting for message ......"); //申明一个消费者 final 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[" + envelope.getRoutingKey() + "]" + message); channel.basicAck(envelope.getDeliveryTag(), false); } }; channel.basicConsume(queueName, false, consumer); } }
- 创建消费者B
public class ConsumerB { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); //在信道中设置交换器 channel.exchangeDeclare(Producer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //申明队列 String queueName = "rejectqueue"; channel.queueDeclare(queueName, false, false, false, null); //绑定:将队列与交换器通过路由键绑定 String routeKey = "error"; channel.queueBind(queueName, Producer.EXCHANGE_NAME, routeKey); System.out.println("waiting for message ......"); //申明一个消费者 final 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[" + envelope.getRoutingKey() + "]" + message); channel.basicAck(envelope.getDeliveryTag(), false); } }; channel.basicConsume(queueName, false, consumer); } }
- 创建拒绝消费者RejectConsumer,使用Reject方式channel.basicReject(envelope.getDeliveryTag(), true);或使用Nack方式channel.basicNack(envelope.getDeliveryTag(), true, true);效果一样
/** * 拒绝消息的消费者 */ public class ConsumerReject { public static void main(String[] args) throws IOException, TimeoutException { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); //在信道中设置交换器 channel.exchangeDeclare(Producer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //申明队列 String queueName = "rejectqueue"; channel.queueDeclare(queueName, false, false, false, null); //绑定:将队列与交换器通过路由键绑定 String routeKey = "error"; channel.queueBind(queueName, Producer.EXCHANGE_NAME, routeKey); System.out.println("waiting for message ......"); //申明一个消费者 final Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { String message = new String(body, "UTF-8"); System.out.println("Receive[" + envelope.getRoutingKey() + "]" + message); throw new RuntimeException("异常处理" + message); } catch (RuntimeException e) { e.printStackTrace(); //Reject方式拒绝(第二个参数决定是否重新投递,设置为true表示重新投递,设置false则消息丢失) channel.basicReject(envelope.getDeliveryTag(), true); //Nack方式拒绝(第二个参数决定是否批量,第三个参数决定是否重新投递) //channel.basicNack(envelope.getDeliveryTag(), true, true); } } }; channel.basicConsume(queueName, false, consumer); } }
【测试结果】
生产者生产了10个消息
消费者拒绝消息1,4,7,10 消息
被拒的消息被ConsumerA消费
被拒的消息被ConsumerB消费
还有一件更加重要的事情,如何创建队列,消费者和生产者都能使用AMQP queue.declare命令为创建队列,但是如果消费者在同一条信息上订阅了另一个队列的话,就无法声明队列了,必需首先取消订阅,将信道置为"传输"模式,当创建队列时,你常常想要指定队列名称,消费者订阅队列时需要队列名称,并在创建绑定时也需要指定队列名称,如果不指定队列名称的话,Rabbit会分配一个随机的名称并queue.declare命令的响应中返回(对于构建在AMQP上的RPC应用来说),使用临时匿名队列很有用,以下是队列设置中另一些有用的参数:
-
durable: 是否持久化, 队列的声明默认是存放到内存中的,如果rabbitmq重启会丢失,如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库
-
exclusive: 有两个作用,一:当连接关闭时connection.close()该队列是否会自动删除;该队列是否是私有的private ,如果设置为true的话,队列将变成私有,此时只有你的应用程序才能消费队列消息,当你想要限制一个队列只有一个消费者的时候很有帮助。如果是private ,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常: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 ‘queue_name’ in vhost ‘/’, class-id=50, method-id=20)一般等于true的话用于一个队列只能有一个消费者来消费的场景
-
auto-delete- 当最后一个消费者取消订阅的时候,队列就会被自动移除了,如果你需要临时队列只为一个消费者服务的话,请结合使用auto-delete和exclusive,当消费者断开连接时,队列就被移除了。也可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除
-
arguments:
队列中的消息什么时候会自动被删除?
- Message TTL(x-message-ttl):设置队列中的所有消息的生存周期(统一为整个队列的所有消息设置生命周期), 也可以在发布消息的时候单独为某个消息指定剩余生存时间,单位毫秒, 类似于redis中的ttl,生存时间到了,消息会被从队里中删除,注意是消息被删除,而不是队列被删除, 特性Features=TTL, 单独为某条消息设置过期时间AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder().expiration(“6000”);
channel.basicPublish(EXCHANGE_NAME, “”, properties.build(), message.getBytes(“UTF-8”)); - Auto Expire(x-expires): 当队列在指定的时间没有被访问(consume, basicGet, queueDeclare…)就会被删除,Features=Exp
- Max Length(x-max-length): 限定队列的消息的最大值长度,超过指定长度将会把最早的几条删除掉, 类似于mongodb中的固定集合,例如保存最新的100条消息, Feature=Lim
- Max Length Bytes(x-max-length-bytes): 限定队列最大占用的空间大小, 一般受限于内存、磁盘的大小, Features=Lim B
- Dead letter exchange(x-dead-letter-exchange): 当队列消息长度大于最大长度、或者过期的等,将从队列中删除的消息推送到指定的交换机中去而不是丢弃掉,Features=DLX
- Dead letter routing key(x-dead-letter-routing-key):将删除的消息推送到指定交换机的指定路由键的队列中去, Feature=DLK
- Maximum priority(x-max-priority):优先级队列,声明队列时先定义最大优先级值(定义最大值一般不要太大),在发布消息的时候指定该消息的优先级, 优先级更高(数值更大的)的消息先被消费,
- Lazy mode(x-queue-mode=lazy): Lazy Queues: 先将消息保存到磁盘上,不放在内存中,当消费者开始消费的时候才加载到内存中
- Master locator(x-queue-master-locator)
上述过程中,我们来看看队列参数综合使用的例子。
public class Producer { public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); // 声明一个接收被删除的消息的交换机和队列 channel.exchangeDeclare("exchange.dead", BuiltinExchangeType.DIRECT); channel.queueDeclare("queue_dead", false, false, false, null); channel.queueBind("queue_dead", "exchange.dead", "routingkey.dead"); channel.exchangeDeclare("exchange.fanout", BuiltinExchangeType.FANOUT); Map<String, Object> arguments = new HashMap<String, Object>(); // 设置队列中的所有消息的生存周期(统一为整个队列的所有消息设置生命周期), 也可以在发布消息的时候单独为某个消息指定剩余生存时间, // 单位毫秒, 类似于redis中的ttl,生存时间到了,消息会被从队里中删除,注意是消息被删除,而不是队列被删除 arguments.put("x-message-ttl", 15000); //当队列在指定的时间没有被访问(consume, basicGet, queueDeclare…)就会被删除 arguments.put("x-expires", 30000); //限定队列的消息的最大值长度,超过指定长度将会把最早的几条删除掉, 类似于mongodb中的固定集合,例如保存最新的100条消息, Feature=Lim arguments.put("x-max-length", 4); //限定队列最大占用的空间大小, 一般受限于内存、磁盘的大小, Features=Lim B arguments.put("x-max-length-bytes", 1024); //当队列消息长度大于最大长度、或者过期的等,将从队列中删除的消息推送到指定的交换机中去而不是丢弃掉 arguments.put("x-dead-letter-exchange", "exchange.dead"); // 将删除的消息推送到指定交换机的指定路由键的队列中去 arguments.put("x-dead-letter-routing-key", "routingkey.dead"); channel.queueDeclare("queue_name", true, false, false, arguments); channel.queueBind("queue_name", "exchange.fanout", ""); String message = "Hello RabbitMQ: "; for(int i = 1; i <= 5; i++) { channel.basicPublish("exchange.fanout", "", null, (message + i).getBytes("UTF-8")); } channel.close(); connection.close(); } }
如果尝试声明一个己经存在的队列会发生什么样的情况呢?只要声明参数是完全匹配的队列的话,Rabbit就什么都不做。并成功返回,就好像这个队列己经创建成功一样(如果参数不匹配的话,队列声明尝试会失败),如果你只想检测队列是否存在,则可以设置queue.declare的passive选项为true,在该设置下,如果队列存在,那么queue.declare命令就会返回成功,如果队列不存在,queue.declare命令不会创建队列而返回一个错误。
当设计应用程序时,你最有可能会问自己,是该由生产者还是消费者来创建所需要的队列呢?看起来最自然的答案是消费者来创建队列,毕竟,消费者需要订阅队列,而且不能订阅一个不存在的队列吧,先别这么快的下结论,你首先需要清楚消息是生产都能否承担得起丢失消息,发送出去的消息如果路由到了不存在的队列的话,Rabbit会忽略它们,因此,如果你不能承担得起消息进入“黑洞”,而丢失的话,你的生产都和消费者就都应该尝试去创建队列,另一方面,如果你承担得起丢失消息,或者你实现一种方法来重新发布未处理的消息的话,(我们会向你展现如何做到这一点),你可以只让自己的消费者来声明队列。
队列是AMQP消息通信的基础模块。
- 为消息提供了处所,消息在此等待消费。
- 对负载均衡来说,队列是绝佳的方案,只需要附加一堆消费者,并让RabbitMQ以特别的方式均匀的分配发来的消息。
- 队列是Rabbit中消息的最后终点(除非消息进入了黑洞)
在掌握了队列之后,你己经准备好了进入下一个Rabbit构造块,交换器的绑定。
就像在之前几节看到的那样,你想让消费者从队列中获取消息,现在的问题是,消息是如何到达队列的呢?让我们来认识一下AMQP的交换器的绑定,当你想要将消息投递到队列时,你通过把消息发送给交换器来完成,然后,根据确定的规则 ,RabbitMQ将会决定消息该投递到哪个队列,这些规则被称作路由键(routing key )。队列通过路由键绑定到交换器,当你把消息发送到代理服务器时,消息将拥有一个路由键,即便是空的,RabbitMQ也会将其和绑定使用的路由键进行匹配,如果相匹配的话,那么消息将会投递到该队列,如果路由消息不匹配任何绑定的模式的话,消息将进入黑洞。
为什么要如此大费周章呢?你也许会这么说“我只想让消息到达队列而已”,让我们来看一个例子,以便理解上述概念的优势。
你可以将这种场景和邮件进行比较,如果人多想把一条消息发送到任何一个联系人,则需要把消息发送到对方的邮件地址,SMTP服务器会检查消息是发送给谁的并会负责投递到用户的收件箱,但是,如果你的联系人想要把来自你的每条消息都归档到商务文件夹下的话,会如何呢?为了能达成这个目标,他们需要根据消息内容设置明确的规则,举个例子来说,他们可能也想通过设置基于主机名的规则,将某些商业供应商归类到同一个文件夹下,通过交换器绑定和队列的概念,AMQP实现上述以及更多的使用场景,因此你能将队列绑定到交换器上,而不是使用路由键,然后你发送给交换器的每一条没有路径键的消息会投递到上述队列中去,这一点和邮件系统非常类似,如果你需要复杂的使用案例,例如发布/订阅或者多播,则也可能轻易完成,一会你就能看到了。
除了可以使用交换器和绑定来完成不同的使用场景之外 ,还有另一个好处就是对于发送消息给服务器的发布者来说,它不需要关心服务器的另一端(整个消息的处理环节中队列和消费者)的逻辑,就如你将看到的那样,这可以导致有趣的消息通信场景,如果服务器只允许你直接发布消息到队列的话,是无法实现这些场景的或者说非常难实现的。
就像你之前看到的那样,服务器会根据路径键将消息从交换器路由到队列,但是他如何投递到多个队列的情况呢?协义中定义了不同的类型交换器发挥作用了,一共有四种类型,direct,fanout,topic和headers,每一种类型实现了不同的路由算法,我们会讲解除了headers交换器外的其他三种,headers交换器允许你匹配AMQP消息的header而非路径键,headers类型的交换器不依赖于路由键匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配,在绑定队列和交换器时制定了一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的headers(也是一组键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列,除此之外,headers交换器和direct交换器完全是一致的,但是性能会差很多,因此它并不太实用,而且几乎再也不用到了,让我们仔细看其他几种类型的交换器吧。
1、Exchange类型direct
direct交换器非常简单,如果路由键匹配的话,消息就被投递到对应的队列中
如下图所示,Queue2绑定了BindingKey为info 或warning或debug,此时Queue2能接收到信道basicPublish 路由键为warning,info,debug的消息,而Queue1 只绑定了路由键为 warning,因此只能接收到信道上basicPublish路由键为warning的消息。
他是根据交换器名称与routingkey来找队列的。
Note:消息从client发出,传送给交换器ChangeA,RoutingKey为routingkey.ZLH,那么不管你发送给Queue1,还是Queue2一个消息都会保存在Queue1,Queue2,Queue3,三个队列中。这就是交换器的direct类型的路由规则。只要找到路由器与routingkey绑定的队列,那么他有多少队列,他就分发给多少队列。
服务器必需实现direct类型交换器,包含了一个空白的字符串名称的默认交换器,当声明一个队列时,它会自动绑定到默认的交换器,并以队列名称作为路由键,这意味着你可以使用如下代码发送消息到之前声明的队列中去,前提是你己经获得了信道实例。
c
h
a
n
n
e
l
−
>
b
a
s
i
c
p
u
b
l
i
s
h
(
channel->basic_publish(
channel−>basicpublish(msg,’’,‘queue-name’);
第一个参数是你想要发送的消息内容,第二个参数是一个空字符串,指定的默认交换器,而第三个参数就是路由键了,这个路由键就是之前用来声明队列名称,之后,你会看如何使用默认交换器和临时队列来实现RPC消息通信模式。
当默认的direct交换器无法满足应用程序的需求时,你可以声明你自己的交换器,只需要发送exchange.declare命令并设置合适的参数就行了。
2、Exchange类型fanout
它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
接下来我们要讨论的是fanout交换器了,就如你看到的下图那样,这种类型的交换器会将收到的消息广播到绑定的队列上,消息通信模式很简单,当你发送一条消息到fanout交换器时,它会把消息投递给所有附加在此交换器上的队列,这允许你对单条消息做不同的方式的反应,一个Web应用程序可能需要在用户传新的图片时,用户相册必需清除缓存,同时用户应该得到些积分奖励,你可以将两个队列张鹏宇到图片上传的交换器上,一个用于清除缓存,另一个用于增加用户积分,从这个场景中你可以了解到,使用交换器,绑定和队列比直接指向指定的队列发送消息要有优势,假设应用程序的第一个需求是在图片上传到网站上后,需要清除用户相册缓存,你可以通过使用一个队列就能轻易完成,但是当产品负责人让你实现一个新的功能时,即上传完成后就给用户一点奖励,你该怎么办呢?如果你是直接将消息发送给队列的话,就不得不修改发送方的代码,以将消息发送给新的用户积分(point)队列,如果你使用的是fanout交换器的话,你唯一需要做的就是为新的消费者写一段 代码,然后声明新的队列并将其绑定到fanout交换器上
消息从客户端发出,只要queue与exchange有绑定,那么他不管你的Routingkey是什么他都会将消息分发给所有与该exchang绑定的队列中。
这个类型忽略Routingkey,他为广播模式。
就如同我们之前讲的那样,发送方代码和消费者的代码两者之间完全解耦了,这允许你轻而易举的添加应用程序的功能。
那我们来看个例子吧。
- 创建生产者
//广播模式-消息生产者 public class FanoutBoss { private static final Logger logger = Logger.getLogger(FanoutBoss.class); public static void main(String[] args) { try { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = null; connection = connectionFactory.newConnection(); //4 创建channel Channel channel = null; channel = connection.createChannel(); String message = "当前时间为:2018年8月6日14:25:14"; //声明交换机(参数为:交换机名称; 交换机类型,广播模式) channel.exchangeDeclare("fanoutLogs", BuiltinExchangeType.FANOUT); //消息发布(参数为:交换机名称; routingKey,忽略。在广播模式中,生产者声明交换机的名称和类型即可) channel.basicPublish("fanoutLogs","", null,message.getBytes()); logger.info("********Message********:发送成功"); channel.close(); connection.close(); } catch (IOException |TimeoutException e) { e.printStackTrace(); } } }
- 创建消费者1 2
//广播模式-消息消费者 public class FanoutWorker1 { private static final Logger logger = Logger.getLogger(FanoutWorker.class); public static void main(String[] args) { try { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = null; connection = connectionFactory.newConnection(); //4 创建channel Channel channel = null; channel = connection.createChannel(); //交换机声明(参数为:交换机名称;交换机类型) channel.exchangeDeclare("fanoutLogs",BuiltinExchangeType.FANOUT); //获取一个临时队列 String queueName = channel.queueDeclare().getQueue(); //队列与交换机绑定(参数为:队列名称;交换机名称;routingKey忽略) channel.queueBind(queueName,"fanoutLogs",""); logger.info("********Waiting for messages********"); //这里重写了DefaultConsumer的handleDelivery方法,因为发送的时候对消息进行了getByte(),在这里要重新组装成String Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); String message = new String(body,"UTF-8"); logger.info("received:" + message); } }; //声明队列中被消费掉的消息(参数为:队列名称;消息是否自动确认;consumer主体) channel.basicConsume(queueName,true,consumer); //这里不能关闭连接,调用了消费方法后,消费者会一直连接着rabbitMQ等待消费 } catch (IOException |TimeoutException e) { e.printStackTrace(); } } } //广播模式-消息消费者 public class FanoutWorker2 { private static final Logger logger = Logger.getLogger(FanoutWorker.class); public static void main(String[] args) { try { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = null; connection = connectionFactory.newConnection(); //4 创建channel Channel channel = null; channel = connection.createChannel(); //交换机声明(参数为:交换机名称;交换机类型) channel.exchangeDeclare("fanoutLogs",BuiltinExchangeType.FANOUT); //获取一个临时队列 String queueName = channel.queueDeclare().getQueue(); //队列与交换机绑定(参数为:队列名称;交换机名称;routingKey忽略) channel.queueBind(queueName,"fanoutLogs",""); logger.info("********Waiting for messages********"); //这里重写了DefaultConsumer的handleDelivery方法,因为发送的时候对消息进行了getByte(),在这里要重新组装成String Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { super.handleDelivery(consumerTag, envelope, properties, body); String message = new String(body,"UTF-8"); logger.info("received:" + message); } }; //声明队列中被消费掉的消息(参数为:队列名称;消息是否自动确认;consumer主体) channel.basicConsume(queueName,true,consumer); //这里不能关闭连接,调用了消费方法后,消费者会一直连接着rabbitMQ等待消费 } catch (IOException |TimeoutException e) { e.printStackTrace(); } } }
【测试结果】:
生产者
2021-05-07 13:09:16.022 【INFO】 [FanoutBoss.java:33] 658 10.10.0.19 Message:发送成功
消费者1
2021-05-07 13:08:36.764 【INFO】 [FanoutWorker1.java:38] 813 10.10.0.19 Waiting for messages
消费者2
2021-05-07 13:08:41.101 【INFO】 [FanoutWorker2.java:38] 889 10.10.0.19 Waiting for messages
3、Exchange类型topic
前面讲到direct类型的交换器路由规则是完全匹配BindingKey和RoutingKey,但是这种严格的匹配方式在很多的情况下不能满足实际业务的需求,topic类型的交换器在匹配规则上进行了扩展,它与direct类型接口相类似,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,但是这里匹配的规则有些不同,它约定
- RoutingKey 为一个点号"." ,分隔的字符串(被点号".“分隔开的每一段独立的字符串称为一个单词),如"com.rabbbitmq.client”,“java.util.concurrent”,“com.hidden.client”
- BindingKey和RoutingKey一样也是点号,"."分隔的字符串。
这个类型的路由规则如果你掌握啦,那是相当的好用,更灵活。他是根据RoutingKey的设置,来做匹配的,其中这里还有两个通配符为:
,代表任意的一个词。例如topic.zlh.,他能够匹配到,topic.zlh.one ,topic.zlh.two ,topic.zlh.abc, …
#,代表任意多个词。例如topic.#,他能够匹配到,topic.zlh.one ,topic.zlh.two ,topic.zlh.abc, …
先来看一个例子
- 路由键"com.rabbitmq.client"的消息会同时路由到Queue1和Queue2.
- 路由键"com.hidden.client"的消息只会路由到Queue2中
- 路由键"com.hidden.demo"的消息只会路由到Queue2中。
- 路由键"java.rabbitmq.demo"的消息只会路由到Queue1中。
- 路由键为"java.util.concurrent"的消息将会被丢弃或者返回给生产者(需要设置mandatory参数,后面有例子)因为它没有匹配到任何路由键。
最后,我们将讨论topic交换器,这类交换器允许你实现有趣的消息通信场景,它使得来自不同源头的消息能够达到同一队列,让我们用web应用程序日志系统作为示例,你拥有多个不同的日志级别,例如error,info和warning,与此同时,你的应用程序分为以下几个模块,user-profile,image-gallery,msg-inbox等,如下图所示,如果在发送消息的动作失败时,你想要报告一个error的话,则可以编写以下代码:
c
h
a
n
n
e
l
−
>
b
a
s
i
c
p
u
b
l
i
s
h
(
channel->basic_publish(
channel−>basicpublish(msg,‘log-exchange’,‘error-msg-inbox’);
然后,假设你声明了一个msg-inbox-errors队列,你可以将其绑定到交换器上来接收消息,如下所示:
KaTeX parse error: Expected 'EOF', got '&' at position 70: …or.msg-inbox') &̲emsp; 到目前为…channel->queue-bind(‘msg-inbox-logs’,‘log-exchange’,’*.msg-inbox’);
msg-inbox-logs队列将会接收从msg-inbox模块发来的所有的error,warning和info日志信息,那么如何接收所有的日志呢?这实现起来也很简单,你可以在队列绑定到交换器上的时候使用通配符,从之前的例子可以看到,单个".“把路由键分为了几个部分,“ * ” 匹配特定的位置的任意文本,为了实现匹配所有的规则,你可以使用”#“字符 。
$channel->queue_bind(‘all-logs’,‘logs-exchange’,’#’)
通过这样的绑定方式,all-logs队列将会接收所有的从Web应用程序发布的日志,当然,要使得之前的示例能够运行,你必需在绑定之前对队列进行声明,“ * ”操作符将” . “视为分隔符,与之不同的是,“#”操作符没有分块的概念,它将什么问题的”."字符均视为关键字的匹配部分。
到目前为止,你应该己经理解了这三种交换器类型,并能体会AMQP强大之处,你可以对服务器的行为编程以满足自己的需求,它既以发布/订阅的模式设置方式作为队列服务器使用,也可以作为RPC服务器,这取决于你如何组织这些功能。
主题模式是模糊匹配
接下来,我们来对topic生产者和消费者模式进行测试。
- 创建生产者
public class LogProducer { public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); String message = "hello"; channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC, true); channel.basicPublish("topic-exchange", "error.log", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); Thread.sleep(Integer.MAX_VALUE); } }
- 创建消费者1
public class LogConsumer1 { public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); channel.queueDeclare("error-queue", false, false, true, null); channel.queueBind("error-queue", "topic-exchange", "error.*"); channel.basicConsume("error-queue", new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("[error]body:" + new String(body)); //发送确认消息 channel.basicAck(envelope.getDeliveryTag(), true); } }); Thread.sleep(Integer.MAX_VALUE); } }
- 创建消费者2
public class LogConsumer2 { public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); channel.queueDeclare("log-queue", false, false, true, null); channel.queueBind("log-queue", "topic-exchange", "*.log"); channel.basicConsume("log-queue", new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("[log] body:" + new String(body)); //发送确认消息 channel.basicAck(envelope.getDeliveryTag(), true); } }); Thread.sleep(Integer.MAX_VALUE); } }
【测试结果】:
生产者发布了一个路由键为error.log的消息,分别被LogConsumer1中路由键为error.*的队列(error-queue)和LogConsumer2中路由键为 *.log的队列(log-queue)匹配到。因此消息同时被LogConsumer1和LogConsumer2的消费者消息掉。匹配规则如下图所示:
让我们回顾一下本节所学到的知识。
- AMQP 架构 中最关键的几个组件分别是交换器,队列和绑定。
- 根据绑定规则将队列绑定到交换器上。
- 消息发布到交换器上。
- 有三种类型的交换器:direct,fanout和topic。
- 基于消息的路由键和交换器类型,服务器会决定将消息投递到哪个队列中去。
在理解了交换器,绑定和队列之后,你可能会认为自己己经掌握了Rabbit的所有特性,但是随着深入使用Rabbit之后,你会发现有一个概念我们没有讨论过:vhost,每一个RabbitMQ服务器都能创建虚拟消息服务器,我们称为虚拟主机(vhost)每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的队列交换器和绑定…更重要的是,它拥有自己的权限机制,这便利你能够安全地使用一个RabbitMQ服务器来服务众多的应用程序,而不用担心你的Sudoku(数独)应用可能会删除正在使用的队列,vhost之于Rabbit就像虚拟之于物理服务器上的一样,它们通过各个实例间提供的逻辑上分离,允许你为不同的应用程序安全保密的运行数据,这很有用,它既能将同一个Rabbit的人多客户区分开来,又可以避免队列和交换器的命名冲突,否则你可能不得不运行多个Rabbit,并忍受随之而来的头疼管理问题。相反。你可以只运行一个Rabbit,然后按需要启动或关闭vhost。
vhost是AMQP的一个概念的基础,你必需在连接时进行指定,由于RabbitMQ包含了开箱即用的默认vhost:"/",因此 使用起来非常的简单,如果你不需要多个vhost的话,那就使用默认的吗?通过使用缺省的guest用户名和密码guest就可以访问默认的vhost,为了安全起见,你应该更改,AMQP的一个有趣的地方在于它并没有指定权限控制是在vhost级别还是在服务端级别实现,这留给服务器的开发者去决定,在RabbitMQ的例子中,权限控制是以vhost为单位的。
当你在Rabbit中创建一个用户时,用户通常会被指派至少一个vhost,并且只能访问被指派vhost内的队列,交换器和绑定,当你在设计消息通信架构时,记住vhost之间是绝对隔离的,你无法将vhost banana_tree上的交换器绑定到vhost oak_tree中队列中去,事实上,这既保证了安全性,又保证了可移植性,假设你为重要的银行设计了兑现层,并使用了自己的vhost,最初,你可能把这个vhost同其他的vhost一起运行在同一个Rabbit上,直到有一天客户们开始兑换数以百计的支票,对你来说这是好事,但是对于Rabbit服务器就惨了,支票产品的需要运行在轻负载的Rabbit服务器上,如果支票兑现层使用的是默认vhost的话,则你不得不担心命名冲突(队列和交换器),不过既然它有自己的vhost,你就能安全的迁移到新的Rabbit服务器上,然后马上开始处理新的负载均衡,而不会有任何命名冲突,因此,我们强烈推荐你仔细甄别框架(譬如Web日志)中的通用功能群组,并为它们分别配置各自的vhost,同时请记住,当你的RabbitMQ集群上创建vhost,整个集群上都会创建vhost,vhost不仅消除了为基础架构中的每一层运行一个RabbitMQ服务器的需要,同时避免了为每一层创建不同的集群。
我们讨论了vhost带来的所有巨大益处,那么如何创建它们呢?vhost和权限控制非常独特,它们是AMQP中唯一无法通过AMQP协义创建的基元(不同于队列,交换器和绑定),对于RabbitMQ来说,你需要通过RabbitMQ的安装路径下的./sbin/目录中的rabbitmqctl工具来创建,通过简单的运行rabbitmqctl add_vhost[vhost_nam]就可以创建一个vhost,其中[vhost_name]就是你想要创建的vhost,删除vhost同样简单:rabbitmqctl delete_vhost[vhost_name],一旦vhost创建成功之后,你就可以连接上去开始添加队列到交换器了,如果你想要知道特定Rabbit服务器上运行着哪些vhost时,运行rabbitmqctl list_vhost即可,之后,你就会看见如下所示的内容。
$ ./sbin/rabbitmqctl list_vhosts
Listing vhost …
/
oak
sycanore
… done注意,通常情况下,你将在服务器上直接运行rabbitmqctl来管理自己的RabbitMQ节点,不过你也可以通过指定 -n rabbit@[service_name]选项来管理远程RabbitMQ节点,@符号将节点标识符(rabbit@[server_name])分成两部分,左边的是Erlang_应用程序名称,在这里永远都是rabbit,右边是服务器主机名或者IP地址,你需要确保运行的Rabbit节点是服务器和运行rabbitmqctl的工作站,安装了相同的Erlang cookie。
当我们在代码中,添加xxxx 的vhost时,提示用户没有相应权限。如下图所示。
到目前为止,通过vhost你保障了队列和交换器的安全,现在我们来讨论当Rabbit崩溃或者重启时,如何确保关键消息的不丢失。
关于在Rabbit里创建队列和交换器有个不可告人的秘密,默认情况下它们无法幸免于服务器的重启,没错,重启RabbitMQ服务器后,那些队列和交换器都消失了(随同 里面的消息),原因在于每个队列和交换器的durable属性,该属性默认情况下为false,它决定了RabbitMQ是否需要在崩溃或者重启之后重新创建队列(或者交换器),将它设置为true,这样你就不需要在服务器断电后重新创建队列和交换器了,你也许会认为把队列和交换器的durable属性设置为true就足够可以让消息幸免于重启,像是你错了,队列和交换器当然必需被设置成true,但光这样做还不够。
能从AMQP服务器崩溃中恢复的消息,我们称为持久化消息,在消息发布之前,通过把它的“投递模式”(delivery mode)选项设置为2(AMQP客户端可能会使用人性化的常量来代替数值)把消息标记成持久化,到目前为止,消息还只是被表示为持久化,但是它还必需被发布到持久化的交换器中并到达持久化的队列中都行,如果不是这样的话,则包含持久化消息的队列(或者交换器)会在Rabbit崩溃重启后不复存在,从而导致消息成为孤儿,因此,如果消息想要从Rabbit崩溃中恢复,那么消息必需:
- 把它的投递模式设置为2(持久)
- 发送到持久化的交换器
- 到达持久化队列。
能做到以上三点,你就不用和你的关键消息玩“躲猫猫了”
RabbitMQ确保持久化消息能从服务器重启中恢复的方式是,将它们写入磁盘上一个持久化日志文件,当发布一条持久化消息到持久交换器上时,Rabbit会在消息提到日志文件后才发送响应。记住,之后这条消息如果路由到了非持久队列的话,它会自动从持久性日志中移除,并且无法从服务器重启中恢复,如果你使用持久性中消费了一条持久性消息的话(并且确认么强调也不为过),一旦你从持久化队列中消费了一条持久性消息的话(并且确认了它),RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集,在你消费持久性消息前,如果RabbitMQ重启的话,服务器会自动重建交换器和队列(以及绑定),重播持久性日志文件中的消息到合适的队列或者交换器上(取决于Rabbit服务器宕机的时候,消息处在路由过程中的哪个环节)
你可能会认为自己应该所有的消息都启用持久化消息通信,你可以这样做,但是同时你也要为此付出代价,性能,写入磁盘要比存入内存慢不止这一点点,而且会极大的减少RabbitMQ服务器每秒可以处理消息总数,使用持久化机制而导致消息吞吐量降低至少10倍的情况并不少见,另外还有一点就是,持久性消息在RabbitMQ内建集群环境下工作得并不好,虽然RabbitMQ集群允许你和集群中的任一队列进行通信,但是事实上的那些队列均匀地分布在各个节点而没有冗余(在集群中任何一个队列都没有备份的),如果运行seed_bin队列的集群节点崩溃了,那么直到节点恢复前,这个队列也就从整个集群中消失了,(如果队列是可持久化的)更重要的是,当节点宕机时,其上的队列也都是不可用的,而且持久化队列也无法重建,这就会导致消息丢失。
权衡取舍,什么情况下你应该使用持久化/持久化消息通信呢?首先,你需要分析,(并测试)性能需求,你是否需要单台Rabbit服务器每秒处理10000条消息呢?如果是这样的话,你应该寻找其他方式来保证消息的投递,(或者使用更快的存储系统)举个例子说,生产者可以单独的在信道上监听应答队列,每次发送消息的时候,都包含应答队列的名称,这样消费者就可以回发应答确认接收到了,如果消息应答未在合理的时候范围内到达,生产者就重新发送消息,也就是说,要保证消息的投递这一关键本质决定范围内到达,生产者就重新发送消息,也就是说,要保证消息的投递这一关键本质决定了相对于其他类型的消息(例如日志消息)会有更低的吞吐量,因此如果持久性消息通信能能够满足性能需求,那么用这种机制确保消息投递是极佳的方式,我们更多的是为了关键消息使用控制的化机制,我们只是对何种类型的内容使用使用持久性消息通信举棋不定,举个例子,我们运行两种类型的Rabbit集群,非持久化消息通信的传统RabbitMQ集群和持久化消息通信的活动/热备非集群Rabbit服务器(使用负载均衡),这样做确保了持久化消息通信处理负载不会减慢非持久化消息的处理,这也意味着Rabbit内建集群在节点宕机时不会持久性消息消失,请记住Rabbit能帮助确保投递,但是并不是万无一失的,硬盘崩溃,充满bug的消费者或者其他极端事件都能导致持久化消息丢失,最终确保消息安全到达将取决于你的策略,持久化消息通信是一个很好的工具,可以帮助你完成这一点。
和消息持久化相关的一个概念是AMQP事务transaction,到目前为止,我们讨论的是将消息,队列和交换器设置为持久化,这一切都工作得非常好,并且RabbitMQ也负责保证消息的安全,但是由于发布操作并不返回任何信息给生产者,那你怎么知道服务器是否 持久化了消息到硬盘呢?服务器可能会在把消息写入磁盘前就宕机了,消息因此丢失,而你却不知道,这就是事务发挥作用的地方。当继续处理其他任务前,你必确保代理接收到的消息(并且己经将消息路由给所有匹配的订阅队列),你必需要把这些行为包装到一个事务中,如果你有数据库背景的话,不要把AMQP事务和大多数数据库的事务概念搞混,在AMQP中,在把信道设置成事务模式后,你通过信道恢复原状那些想要确认的消息,之后还有多个其他的AMQP命令,这些命令是执行还是忽略,取决于第一条消息发送是否成功,一旦你发送完所有的命令,就可以提交事务了,如果事务中的首次发布成功,那么信道在事务中完成其他AMQP命令,如果发送失败,其他的AMQP命令将不会执行,事务填补了生产者发布消息及RabbitMQ将它们提交到磁盘上的两者之间,最后一英里,不过还有更好的方法来填补差距。
假如淘宝中下单和发货是在不同的系统中完成,用户下单后,即发送一条RabbitMQ消息,提示发货系统给用户发货,但是如果在处理下单的过程中,处理失败了,事务回滚,但是此时发送给发货系统的消息己经发送出去,该怎么办呢?难道用户没有下单成功,就给用户发货吗?其实使用RabbitMQ不用担心此类问题,在事务处理方面和数据库事务类似。下面我们来看一则例子。看RabbitMQ是如何处理事务消息的。
- 创建生产者
public class TxProducer { private static final String QUEUE_NAME = "test_queue_tx"; public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); for(int i = 0 ;i < 3 ;i ++ ){ try { String msgString = "hello tx message ! " + i ; channel.txSelect(); channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes()); if(i ==1 ){ int a = 1 / 0; } channel.txCommit(); System.out.println("producer : " + msgString + " finished!"); } catch (Exception e) { e.printStackTrace(); channel.txRollback();//消息回滚 System.out.println("producer message txRollback"); } } channel.close(); connection.close(); } }
上述过程中,我们需要注意的一点是,我们循环发布3个消息,当i=1时,抛出异常,信道调用txRollback()方法。下面我们来看看消费者是否只收到两条消息。
public class TxConsumer { private static final String QUEUE_NAME = "test_queue_tx"; public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); DefaultConsumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "utf-8"); System.out.println("consumer: " + msg); } }; //监听 channel.basicConsume(QUEUE_NAME, true, consumer); } }
【测试结果】:
生产者日志打印
消费者日志打印
事务带来的问题?
虽然事务是正式规范的一部分,几乎吸干了Rabbit的性能,使用事务不但会降低大约2-10倍的消息吞吐量,而且会使生产应用程序产生同步,而你使用消息通信就是想要避免同步,知晓了所有的这一切之后,RabbitMQ团队决定拿出更好的方案来保证消息投递,发送方确认模式,和事务相仿,你需要告诉Rabbit将信道设置为confirm模式,而且你只能通过重新创建信道来关闭该设置,一旦信道进入confirm模式,所有的信道上发布消息都会被指派一个唯一的id号(从1)开始,一旦消息被投递给所有匹配的队列后,信道会发送一个发送方确认模式给生产者应用程序(包含消息唯一的ID),这使得生产者知晓消息己经安全到达队列了,如果消息和队列是可持久化的,那么确认消息只会在队列将消息写入磁盘后才会发出,发送方确认模式的最大好处就是它们是异步的,一旦发布了一条消息,生产者应用程序就可以在等待确认的同时继续发送下一条,当确认消息最终收到的时候,生产者应用的回调方法就会被触发来处理该确认消息,如果Rabbit发生了内部错误从而导致了消息丢失,Rabbit会发送一条nack(not acknowledged,未确认)消息,就像发送方确认消息那样,只不过这次说明消息己经丢失了,同时,由于没有消息回滚的概念(同事务相比),因此发送方确认模式更加轻量级,同时,由于消息没有回滚的概念(同事务相比),因此发送方确认模式更加轻量级,同时对Rabbit代理服务器的性能影响几乎可以忽略不计。
至此,你了解了RabbitMQ的各个部分,包括消费者和生产者到持久化消息通信,那么它们是如何结合起来 的呢?一条真实的消息生命周期是怎样的呢?回答这些问题最好的方法就是,在代码层面来看消息的一生。
让我们从发布者代码开始,它需要完成以下任务
- 连接RabbitMQ
- 获取信道
- 声明交换器
- 创建消息
- 发布消息
- 关闭信道
- 关闭连接。
至此,你完成了所有的设置工作。
public class Producer2 { public static void main(String[] args) throws IOException, Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); //5 创建队列Queen /**参数介绍: * queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * autoDelete: 当没有consumer时候是否删除队列 arguments:配置的基本参数 */ channel.exchangeDeclare("hello-exchange", BuiltinExchangeType.DIRECT, true, false, false,null); Map<String, Object> headers = new HashMap<String, Object>(); headers.put("my1", "1111"); headers.put("my2", "2222"); AMQP.BasicProperties properties = new AMQP.BasicProperties().builder() .deliveryMode(2) // 传送方式 .contentEncoding("UTF-8") // 编码方式 .expiration("10000") // 过期时间 .headers(headers) //自定义属性 .build(); channel.basicPublish("hello-exchange","hola", properties,"odsidios".getBytes()); //7 释放连接资源 channel.close(); connection.close(); } }
让我们来看看上述代码做了些什么,用样板代码来连接RabbitMQ(这里没有指定的虚拟主机,使用的是默认的“/”),Rabbit默认运行在本机的5672端口上,使用默认的guest用户名和密码,然后获取信道和RabbitMQ进行通信。
下一步是声明交换器,消息将发送到这里,第一个参数是交换器名称hello-exchange,第二个参数是交换器的类型direct, durable 和autoDelete表示你想要持久化交换器并且不会自动删除。
获取消息后,你就将通过basic_publish命令发送给hello-exchange
你不需要每次都关闭连接,你可以通过一条信道/连接发送多条信息,发送完之后再关闭。
发布者己经准备就绪,现在让我们创建消费者是,它需要执行以下任务。
- 连接到RabbitMQ
- 获得信道。
- 声明交换器
- 声明队列
- 把队列和交换器绑定起来。
- 消费消息
- 关闭信道
- 关闭连接
看起来和之前的很像,但事实上却不是,在之前的代码样例中,你己经完成了前三步和后两步,这里新的内容如何声明队列,并绑定到交换器上,然后开始消费新的消息。
public class Consumer2 { public static void main(String[] args) throws IOException, Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); //3 创建连接 Connection connection = connectionFactory.newConnection(); //4 创建channel Channel channel = connection.createChannel(); //5 创建队列Queen /**参数介绍: * queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * * queue:队列名称 durable:是否持久化 exclusive是否独占,只能有一个消费者监听队列。 * autoDelete: 当没有consumer时候是否删除队列 * arguments:配置的基本参数 */ channel.exchangeDeclare("hello-exchange", BuiltinExchangeType.DIRECT, true, false, false,null); channel.queueDeclare("hello-queue",true,false,false,null); channel.queueBind("hello-queue","hello-exchange","hola"); com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel){ //这是一个回调方法,当收到消息后会自动执行该方法 /** * @param consumerTag :标识 * @param envelope: 获取交换机,路由等信息 * @param properties:配置信息 * @param body:真是的数据 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println(""+consumerTag); System.out.println("getExchange+"+envelope.getExchange()); System.out.println("getRoutingKey+"+envelope.getRoutingKey()); System.out.println("properties"+properties); System.out.println("body"+new String(body,"utf-8")); } }; channel.basicConsume("hello-queue",true,consumer); channel.close(); connection.close(); } }
在你建立了到服务器的连接,并在创建了信道,在获得连接之后,你再一次声明了交换器,由于declare命令的语义是“如果没有就创建,否则继续 ”,所以这里是什么都没有发生,如果之后你发送了queue_bind命令时,万一交换器并没有预先创建好,这样做就是避免这类错误的发生。
你使用AMQP默认参数声明了一个名为hello-queue的队列,之后你把队列绑定到hello-exchange,你使用路由键hola。
你几乎己经准备就绪开始消费信息了,不过你还需要一个用于处理消息的回调函数,在你创建该函数,它会确认消息,因此,RabbitMQ才能删除消息并给消费者发送新的消息,在回调函数末尾,你打印出来了消息体,不久你就会知道做了什么。
有了回调函数,你就可以发送basicConsume命令订阅队列了,第一个参数就是你刚编写的回调函数,接下来的参数分别是队列的名称和用于标识进程的消费者标记,RabbitMQ发送给消费者的每一条消息都会传入的回调函数。
让我们来看看整个过程做了些什么,你想发送消息并消费它们,因此你声明了交换器以保证有地方发布消息,同时你也创建了队列并绑定到hello-exchange上,然后基于你的命令行输入文本,你创建消息实例并通过RabbitMQ发送出去,基于direct交换器类型,RabbitMQ将消息路由到hello-queue上,由于另一端有消费者在等待消费,因此 ,RabbitMQ投递了消息并回调了函数进行处理,你看此处并没有提供路由键,那是因为AMQP既可以很简单也可以很复杂,这取决于你怎么使用它,对我们来说空白路由键就足够了。
你明白了如何编写基本的消费者和生产者,但是你也许在思考如何将发送方确认模式和事务结合起来,让我们看看你如何改进Hello World生产者,利用publisher comfirm来追踪消息投递,在我们动手改进Hello World 生产者使用发送方确认模式,下图形象的展示了消息ID是如何指派的。
之前我们说过,当信道设置成confirm模式时,发布的每一条消息都会获得唯一的ID,这可能会让你猜测basicPublish会返回消息ID,不过事实上消息ID不是这样工作的,由于一条信道只能被单个线程使用,因而可以确保信道上发布的消息是连续的,因此,RabbitMQ做了个简单的假设,任一信道上发布的第一条消息将获得ID,并且信道上连接的每一条消息的ID都将步进1,也就是说,信道上发布的第二条消息将会拥有ID2,第三条消息将会拥有ID3,等等,对信道来说,消息的ID是唯一的,所以一旦信道关闭后,你将无法追踪发布在该信道上任何未完成的发送方确认消息状态,这意味着RabbitMQ不必告诉你刚刚发布的消息ID,你的应用程序内部通过一个计数器自己来跟踪,每次应用程序信道发布消息时,你需要把计数器加1 ,同时,由于每条信道上的消息ID都是从1计数的,因此如果你同时拥有众多信道的话,就需要为每条信道分别维护一个内部消息ID计数器。
public class Producer { // default exchange private static String exchange = ""; // default exchange 的路由规则: routingKey(test) 将匹配同名的 queue(test) private static String routingKey = "test"; public static void main(String[] args) throws Exception { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); // 4 开启消息确认机制 channel.confirmSelect(); // 6 添加消息确认监听(异步回调) // 第三种 //异步confirm模式的编程实现最复杂,Channel对象提供的ConfirmListener()回调方法只包含deliveryTag(当前Chanel发出的消息序号), // 我们需要自己为每一个Channel维护一个unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次handleAck方法, // unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率上看,这个unconfirm集合最好 // 采用有序集合SortedSet存储结构。实际上,SDK中的waitForConfirms()方法也是通过SortedSet维护消息序号的。 //关键代码: Map<Long, Object> map = new HashMap<>(); channel.addConfirmListener(new ConfirmListener() { @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("------------ack---------deliveryTag------" + deliveryTag + " -----multiple------ " + multiple); SortedSet<Long> unconfirmedSet = getNotComfirmSeqNo(channel); if (multiple) { unconfirmedSet.headSet(deliveryTag + 1).clear(); } else { unconfirmedSet.remove(deliveryTag); } System.out.println("剩余未确认的消息序列号:" + JSON.toJSONString(unconfirmedSet)); } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { System.out.println("------------no ack---------deliveryTag------" + deliveryTag + "----multiple---" + multiple); System.out.println("服务器内部失败的消息 serNo:" +deliveryTag); } }); Random random = new Random(); // 5 发送消息 for (int i = 0; i < 10; i++) { long nextSeqNo = channel.getNextPublishSeqNo(); String msg = "消息确认模式:RabbitMQ send confirm message " + i; channel.basicPublish(exchange, routingKey, null, msg.getBytes()); System.out.println("生产者发布消息后,剩余未确认的消息序列号:" + JSON.toJSONString( getNotComfirmSeqNo(channel))); map.put(nextSeqNo, msg); } if (!channel.waitForConfirms()) { SortedSet<Long> object = getNotComfirmSeqNo(channel); System.out.println(JSON.toJSONString(object)); System.out.println("发送消息丢失的消息如下:"); System.out.println(JSON.toJSONString(getNotComfirmSeqNo(channel))); // 找到发送失败的消息,即可做我们的业务了 ... }else{ System.out.println("本次发送的所有消息都被服务器接收"); System.out.println(JSON.toJSONString(getNotComfirmSeqNo(channel))); } } public static SortedSet<Long> getNotComfirmSeqNo(Channel channel) { if(channel instanceof AutorecoveringChannel){ RecoveryAwareChannelN channelN = (RecoveryAwareChannelN) ReflectionUtils.getObjectFieldValue(channel, "delegate"); return (SortedSet<Long>) ReflectionUtils.getObjectFieldValue(channelN, "unconfirmedSet"); } return null; } }
信道在调用basicPublish方法一次以后,会将内部消息ID存储于Channel的unconfirmedSet属性中,当确认服务器收到消息后,会将内部消息ID从unconfirmedSet中移除,因此,如果消息发送失败或者说服务器没有接收到消息,即可根据未确认的消息ID从我们存储的消息中取出尝试重发了。
消费者没有什么区别,源码如下
public class Consumer { private static String queueName = "test"; public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { //1 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); // 4 创建Queue channel.queueDeclare(queueName , true , false , false , null); // 5 创建消费者 QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queueName , true , consumer); // 6 接收消息 while (true){ QueueingConsumer.Delivery delivery = consumer.nextDelivery(); String msg = new String(delivery.getBody()); System.out.println(msg); } } }
当生产者发送消息到RabbitMQ交换器,但是交换器上路由键没有匹配到相应的队列或匹配到相应的队列了,但是没有相应的消费者,此时生产者应该知道此事,并不再向交换器中发送消息,等有队列或者有消费者后,再向服务器中发送消息了,如果有这样一个业务需求怎么办呢?不用担心,rabbitMQ有解决方案。
概述
mandatory和immediate是AMQP协议中basic.publish方法中的两个标识位,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。对于刚开始接触RabbitMQ的朋友特别容易被这两个参数搞混,这里博主整理了写资料,简单讲解下这两个标识位。
-
mandatory
当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返回给生产者(Basic.Return + Content-Header + Content-Body);当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。 -
immediate
当immediate标志位设置为true时,如果exchange在将消息路由到queue(s)时发现对于的queue上么有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或者多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
概括来说,mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。
先来看 mandatory 例子吧:
public class Producer_mandatory { public static void main(String[] args) { try { ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); channel.basicQos(1);// 设置客户端最多接收未被ack的消息个数 channel.exchangeDeclare("mandatory-test", BuiltinExchangeType.DIRECT); //channel.queueDeclare("hello_world",true,false,false,null); //channel.queueBind("hello_world", "mandatory-test", "xxxxxx1"); // 当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用 basic.return 方法将消息返回给生产者 // (Basic.Return + Content-Header + Content-Body);当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。 // 当immediate标志位设置为true时,如果exchange在将消息路由到queue(s)时发现对于的queue上没有消费者,那么这条消息不会放入队列中。 // 当与消息routeKey关联的所有queue(一个或者多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。 // 概括来说,mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者, // 则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。 channel.basicPublish("mandatory-test", "xxxxxx2", true, false, MessageProperties.PERSISTENT_TEXT_PLAIN, "===mandatory===".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("Basic.return返回的结果是:"+message); } }); // channel.close(); //connection.close(); } catch (Exception e) { e.printStackTrace(); } } }
【测试结果】:
上述过程中需要注意的一点是下面两行代码
channel.queueDeclare(“hello_world”,true,false,false,null);
channel.queueBind(“hello_world”, “mandatory-test”, “xxxxxx2”);
如果我们申明了队列hello_world,并绑定了路由键xxxxxx2,运行测试,服务器申明了队列hello_world,并绑定了路由键xxxxxx2,此时发布的消息被hello_world队列接收。因此没有收到回吐消息。
当注释掉申明队列绑定路由键代码。再次测试。
发现RabbitMQ没有回吐消息,是怎么回事呢?因为在上一次,己经注册了队列hello_world并绑定了路由键xxxxxx2,因此服务器端己经存在了,因此,下次运行时,直接将消息发送到绑定路由键xxxxxx2的队列hello_world中。因此如果需要再次复现测试效果时
- 调用channel.queueUnbind(“hello_world”, “mandatory-test”, “xxxxxx2”) 解绑队列hello_world绑定的路由键
- 调用
channel.queueDelete(“hello_world”);
channel.queueDeleteNoWait(“hello_world”,false,false);
删除队列 - 更换basicPublish发布时的路由键
channel.basicPublish(“mandatory-test”, “xaxa”, true, false, MessageProperties.PERSISTENT_TEXT_PLAIN, “=mandatory=”.getBytes());
上述过程中需要注意的一点是,在删除队列时,如果当前队列中有消息时,调用channel.queueDelete(“hello_world”,true,true);会抛出异常,如下
如果要强制删除,可以这样写channel.queueDelete(“hello_world”,false,false);
下面再来看immediate参数的使用。先上代码。
public class Producer_immediate { public static void main(String[] args) { try { ConnectionFactory connectionFactory = new ConnectionFactory(); //2 设置参数 connectionFactory.setHost(RabbitConstant.HOST);//设置注解 connectionFactory.setPort(RabbitConstant.PORT);//设置端口 connectionFactory.setVirtualHost(RabbitConstant.VHOST);//设置虚拟及 connectionFactory.setUsername(RabbitConstant.USERNAME); connectionFactory.setPassword(RabbitConstant.PASSWORD); // 3 创建Channel Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); channel.basicQos(1); // // 设置客户端最多接收未被ack的消息个数 channel.exchangeDeclare("mandatory-test", BuiltinExchangeType.DIRECT); channel.queueDeclare("hello_worldx",true,false,false,null); channel.queueBind("hello_worldx", "mandatory-test", "xxxxxx3"); // 当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用 basic.return 方法将消息返回给生产者 // (Basic.Return + Content-Header + Content-Body);当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。 // 当immediate标志位设置为true时,如果exchange在将消息路由到queue(s)时发现对于的queue上没有消费者,那么这条消息不会放入队列中。 // 当与消息routeKey关联的所有queue(一个或者多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。 // 概括来说,mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者, // 则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。 channel.basicPublish("mandatory-test", "xxxxxx3", false, true, MessageProperties.PERSISTENT_TEXT_PLAIN, "===mandatory===".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("Basic.return返回的结果是:"+message); } }); // channel.close(); //connection.close(); } catch (Exception e) { e.printStackTrace(); } } }
【测试结果】
我们来看网上怎么说:
immediate
在RabbitMQ3.0以后的版本里,去掉了immediate参数的支持,发送带immediate标记的publish会返回如下错误:
“{amqp_error,not_implemented,“immediate=true”,‘basic.publish’}”
为什么移除immediate标记,参见如下版本变化描述:
Removal of “immediate” flag
What changed? We removed support for the rarely-used “immediate” flag on AMQP’s basic.publish.
Why on earth did you do that? Support for “immediate” made many parts of the codebase more complex, particularly around mirrored queues. It also stood in the way of our being able to deliver substantial performance improvements in mirrored queues.
What do I need to do? If you just want to be able to publish messages that will be dropped if they are not consumed immediately, you can publish to a queue with a TTL of 0.
If you also need your publisher to be able to determine that this has happened, you can also use the DLX feature to route such messages to another queue, from which the publisher can consume them.
这段解释的大概意思是:immediate标记会影响镜像队列性能,增加代码复杂性,并建议采用“TTL”和“DLX”等方式替代。
AMQP 命令概览
我们写了那么多的AMQP命令,而AMQP0-9-1协义中的命令远远不止代码中涉及的这些,为了让读者在遇到其他命令的时候能够迅速查阅相关信息,下面列举了AMQP 0-9-1协义的主要命令,包含名称,是否包含内容要(Content Body),对应客户端中相应的方法及知道要描述等四个维度进行说明。
名称 | 是否包含内容体 | 对应的客户端中的方法 | 简要描述 |
---|---|---|---|
Connection.Start | 否 | factory.newConnection | 建立连接相关 |
Connection.Close | 否 | connection.close | 关闭连接 |
Channel.Open | 否 | connection.openChannel | 开启信道 |
Exchange.Declare | 否 | channel.exchangeDeclare | 声明交换器 |
Exchange.Delete | 否 | channel.exchangeDelete | 删除交换器 |
Exchange.Bind | 否 | channel.exchangeBind | 交换器与交换器绑定 |
Exchange.Unbind | 否 | channel.exchangeUnBind | 交换器与交换器解绑 |
Queue.Declare | 否 | channel.queueDeclare | 声明队列 |
Queue.Purge | 否 | channel.queuePurge | 清除队列中的内容 |
Queue.Unbind | 否 | channel.queueDelete | 删除队列 |
Queue.Qos | 否 | channel.basicQos | 设置未被确认的消费个数 |
Basic.Consume | 否 | channel.basicConsume | 消费消息(推模式) |
Basic.Cancel | 否 | channel.basicCancel | 取消 |
Basic.Publish | 是 | channel.basicPublish | 发送消息 |
Basic.Get | 否 | channel.basicGet | 消费消息(拉模式) |
Basic.Ack | 否 | channel.basicAck | 确认 |
Basic.Reject | 否 | channel.basicReject | 拒绝单条 |
Basic.Recover | 否 | channel.basicRecover | 请求Broker重新发送未被确认的消息 |
Basic.Nack | 否 | channel.basicNack | 拒绝(可批量拒绝) |
Tx.Select | 否 | channel.txSelect | 开启事务 |
Tx.Commit | 否 | channel.txCommit | 事务提交 |
Tx.Rollback | 否 | channel.txRollback | 事务回滚 |
Comfirm Select | 否 | channel.comfirmSelect | 记发送端确认模式 |
总结 :
我们讲解了很多的内容,你不仅拥有了消息通信基础来构建任何你能想象得到的应用程序,同时也拥有真实存在的生产者和消费者,特别是生产者还能追踪消息投递。在后面的篇博客中,我们来深入理解RabbitMQ 和 分析Spring是如何整合RabbitMQ及源码解析。
本文的github地址是:
https://github.com/quyixiao/spring_tiny/tree/master/src/main/java/com/spring_1_100/test_71_80/test78_spring_rabbitmq
【引用其他博客地址】
https://blog.csdn.net/u013256816/article/details/54914525