1.Java版通信案例
(1)RabbitMQ常用发布订阅模式的运行流程
消息在producer中产生,发送到MQ的exchange上,exchange根据配置的路由方式发到相应的Queue上,Queue又将消息发送给consumer,消息从queue到consumer有push和pull两种方式。 消息队列的使用过程大概如下:
1.客户端连接到消息队列服务器,打开一个channel。
2.客户端声明一个exchange,并设置相关属性。
3.客户端声明一个queue,并设置相关属性。
4.客户端使用routing key,在exchange和queue之间建立好绑定关系。
5.客户端投递消息到exchange。
exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
RabbitMQ连接
public class ConnectionUtil {
public static Connection getConnection() {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置RabbitMQ相关信息
factory.setHost("localhost");
//factory.setUsername("lp");
//factory.setPassword("");
//factory.setPort(2088);
//创建一个新的连接
Connection connection = null;
try {
connection = factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
}
生产者:
public class Producer {
private final static String QUEUE_NAME = "test_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
/*
* 声明(创建)队列
* 参数1:队列名称
* 参数2:为true时server重启队列不会消失
* 参数3:队列是否是独占的,如果为true只能被一个connection使用,其他连接建立时会抛出异常
* 参数4:队列不再使用时是否自动删除(没有连接,并且没有未处理的消息)
* 参数5:建立队列时的其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息内容
String message = "Hello World!";
/*
* 向server发布一条消息
* 参数1:exchange名字,若为空则使用默认的exchange
* 参数2:routing key
* 参数3:其他的属性
* 参数4:消息体
* RabbitMQ默认有一个exchange,叫default exchange,它用一个空字符串表示,它是direct exchange类型,
* 任何发往这个exchange的消息都会被路由到routing key的名字对应的队列上,如果没有对应的队列,则消息会被丢弃
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
//关闭通道和连接
channel.close();
connection.close();
}
}
消费者:
public class Consumer2 {
private final static String QUEUE_NAME = "test_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列(如果你已经明确的知道有这个队列,那么下面这句代码可以注释掉,如果不注释掉的话,也可以理解为消费者必须监听一个队列,如果没有就创建一个)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" Waiting for msg....");
Consumer 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("Received is = '" + msg + "'");
}
};
/*
* 监听队列
* 参数1:队列名称
* 参数2:是否发送ack包,不发送ack消息会持续在服务端保存,直到收到ack。 可以通过channel.basicAck手动回复ack
* 参数3:消费者
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
(2)work模式:
当一个procedure中的queue被多个consumer订阅时,出现下面两个问题:
a、消费者1和消费者2获取到的消息内容是不同的,同一个消息只能被一个消费者获取。
b、消费者1和消费者2获取到的消息的数量是相同的,一个是奇数一个是偶数。
生产者:
public class Producer {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
//发送的消息间隔越来越长
Thread.sleep(i * 10);
}
channel.close();
connection.close();
}
}
消费者1与消费者2的代码相同:
public class Consumer1 {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 同一时刻服务器只会发一条消息给消费者(能者多劳模式)
//channel.basicQos(1);
Consumer 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(" [消费者1] Received '" + msg + "'");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动返回ack包确认状态
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
/*
* 监听队列,不自动返回ack包,下面手动返回
* 如果不回复,消息不会在服务器删除
*/
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
(3)订阅模式
订阅模式下有以下几个问题需要注意:
1.消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。
2.Direct 、Fanout 、Topic 三种类型,RabbitMQ默认有一个exchange,叫default exchange,它用一个空字符串表示,它是direct exchange类型。
a:Direct Exchange
任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
1.一般情况可以使用rabbitMQ自带的Exchange:”"(该Exchange的名字为空字符串,下文称其为default Exchange)。
2.这种模式下不需要将Exchange进行任何绑定(binding)操作
3.消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。
4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。
b:Fanout Exchange
任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
1.可以理解为路由表的模式
2.这种模式不需要RouteKey
3.这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
4.如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
c:Topic Exchange
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
1.这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
2.这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
3.在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
4.“#”表示0个或若干个关键字,“*”表示一个关键字。如“log.*”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
5.同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。
下面是Fanout Exchange 的演示demo
Procedure
public class Producer {
//交换机的名称
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
/*
* 声明exchange(交换机)
* 参数1:交换机名称
* 参数2:交换机类型
* 参数3:交换机持久性,如果为true则服务器重启时不会丢失
* 参数4:交换机在不被使用时是否删除
* 参数5:交换机的其他属性
*/
channel.exchangeDeclare(EXCHANGE_NAME, "fanout",true,true,null);
// 消息内容
String message = "subscribe msg";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1和消费者2代码相同,此模式下一个消息可以被两个消费者同时获取
public class Consumer1 {
//消费者1订阅的队列1,当前队列绑定到交换机上,同时消费者2改成test_queue_exchange_2
private final static String QUEUE_NAME = "test_queue_exchange_1";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/*
* 绑定队列到交换机(这个交换机的名称一定要和上面的生产者交换机名称相同)
* 参数1:队列的名称
* 参数2:交换机的名称
* 参数3:Routing Key
*
*/
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
Consumer 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(" [消费者1] Received '" + msg + "'");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
(4)1路由模式
在direct中的路由模式中,一个消息可以被多个队列通过routkey使用,在没有初始化exchange时启动消费者会报错。
生产者:
public class Producer {
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息内容
String message = "这是消息B";
//这里的路由标识为“B”
channel.basicPublish(EXCHANGE_NAME, "B", null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1
public class Consumer1 {
private final static String QUEUE_NAME = "test_queue_direct_1";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/*
* 绑定队列到交换机
* 参数1:队列的名称
* 参数2:交换机的名称
* 参数3:routingKey
*/
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "B");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
Consumer 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(" [消费者1] Received '" + msg + "'");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
消费者2:
public class Consumer2 {
private final static String QUEUE_NAME = "test_queue_direct_2";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/*
* 绑定队列到交换机
* 参数1:队列的名称
* 参数2:交换机的名称
* 参数3:routingKey
* 可以同时绑定多个路由标识,同时收到当前标识下的信息
*/
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "A");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "B");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
Consumer 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(" [消费者2] Received '" + msg + "'");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
(5)2通配符模式
将路由键和某模式进行匹配,此时队列需要绑定要一个模式上。
a:符号“#”匹配一个或多个词
b:符号“*”只能匹配一个词。
c:因此“audit.#”能够匹配到“audit.irs”和“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。
生产者:
public class Producer {
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容 模拟 有人购物下订单
String message = "新增订单:id=101";
channel.basicPublish(EXCHANGE_NAME, "order.insert", null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1:
public class Consumer1 {
private final static String QUEUE_NAME = "test_queue_topic_1";
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,#可以匹配多个词
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "order.#");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
Consumer 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(" [财务系统] Received '" + msg + "'");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
消费者2:
public class Consumer2 {
private final static String QUEUE_NAME = "test_queue_topic_2";
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,#可以匹配多个词
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "order.insert");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
Consumer 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(" [物流系统] Received '" + msg + "'");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
topic模式与direct模式相比,其routkey是可以正则匹配的。
2.RabbitMQ原理
(1)ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。
Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。
(2)Message acknowledgment
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…
(3)Prefetch count
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
(4)RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。