RabbitMQ 中常用的工作模式有简单模式、工作队列模式、发布订阅模式、路由模式和通配符模式,我们将使用 RabbitMQ 的 Java API 来实现这些工作模式。本文关于 RabbtiMQ 的 API 都依赖于 amqp-client。
<!--rabbitmq java客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.12.0</version>
</dependency>
</dependencies>
一、简单模式
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)
P:生产者,也就是要发送消息的程序
C:消费者:消息的接收者,会一直等待消息到来
queue:消息队列,图中红色部分。生产者向其中投递消息,消费者从其中取出消息
1. 生产者
public class HelloProducer {
private static final String QUEUE_NAME = "hello-world";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/**
* 参数说明
* queue: 队列名称
* durable: 是否持久化消息内容(即消息是否落盘)
* exclusive: 是否独占,只有一个消费者监听这个队列
* autoDelete: 是否自动删除,当没有消费者存在时,自动删除队列
* arguments: 其他配置参数
*/
//该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/**
* 参数说明
* exchange: 交换机名称,简单模式下使用默认的
* routingKey: 路由名称,简单模式下就是队列名称
*/
String body = "hello rabbitmq ...";
channel.basicPublish("", QUEUE_NAME, null, body.getBytes(StandardCharsets.UTF_8));
// 2.释放资源
channel.close();
connection.close();
System.out.println("producer done");
}
}
2. 消费者
public class HelloConsumer {
private static final String QUEUE_NAME = "hello-world";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("消费端等待接收消息......");
Consumer consumer = new DefaultConsumer(channel) {
/**
* 回调方法,当接收到消息后会自动执行该方法
* @param consumerTag 消费者标识
* @param envelope 消息的包装数据,比如交换机、路由key等
* @param properties 配置信息
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag: " + consumerTag + " Exchange: " + envelope.getExchange() + " RoutingKey: " + envelope.getRoutingKey());
System.out.println("消费端接收到消息,body: " + new String(body));
}
};
// autoAck: true 表示自动应答;false 表示手动应答
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
先运行消费者,在运行生产者,执行结果如下:
二、工作队列(Work queues)模式
与简单模式相比,工作队列模式有一个或多个消费端,并且多个消费者共同消费同一个队列中的消息,这些消费者是竞争关系,即同一时刻只有一个消费者能去队列中拉取消息,该种模式也使用默认的交换机。
1. 生产者
public class WorkQueueProducer {
private static final String QUEUE_NAME = "work-queue";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 1; i < 11; i++) {
String body = "第 " + i + " 次, hello rabbitmq ...";
channel.basicPublish("", QUEUE_NAME, null, body.getBytes(StandardCharsets.UTF_8));
}
// 2.释放资源
channel.close();
connection.close();
System.out.println("producer done");
}
2. 消费者A
public class AWorkQueueConsumer {
private static final String QUEUE_NAME = "work-queue";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("消费端等待接收消息......");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费端接收到消息,body: " + new String(body));
}
};
// autoAck: true 表示自动应答;false 表示手动应答
channel.basicConsume(QUEUE_NAME, true, consumer);
}
3. 消费者B
public class BWorkQueueConsumer {
private static final String QUEUE_NAME = "work-queue";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//该方法功能:如果没有名称为 queue 的队列,则会创建一个,存在 queue 时则不创建
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("消费端等待接收消息......");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费端接收到消息,body: " + new String(body));
}
};
// autoAck: true 表示自动应答;false 表示手动应答
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
三、发布订阅模式
订阅模式中,多了一个 Exchange 角色。Exchange 只负责消息转发,不具有消息存储能力,如果 Exchange 没有和任何消息队列绑定,那么消息就会丢失
- 生产者发送消息,消息先发到交换机 Exchange 中,然后由交换机转发到绑定的消息队列中
- 每个消息队列有自己对应的消费者,然后进行消息消费
- 该种模式适用于生产者消息被多个客户端应用消费的场景
1. 生产者
public class PubSubProducer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/**
* 2.创建交换机,方法 exchangeDeclare,方法参数说明:
* (1) exchange: 交换机机名称
* (2) type: 交换机类型(参考枚举类BuiltinExchangeType,
* direct:定向交换机,把消息转发指定的routing key绑定的queue中
* fanout:广播交换机,把消息转发给所有绑定的queue
* topic:通配符交换机,把消息转发给匹配到的routing key绑定的queue中)
* (3) durable: 交换机是否持久化,会将 exchange 的信息保存到磁盘中,rabbitmq 重启时不会丢失
* (4) autoDelete: 是否自动删除,true 表示当 exchange 不再被使用的时候会被自动删除
* (5) internal: 是否内置,true 表示当前 exchange 是一个内置交换机,此时不能直接通过客户端程序向这个交换机中发送消息的
* (6) arguments:设置交换机的其他参数
*/
String exchangeName = "pub_sub_fanout";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false, true, null);
String queueA = "fanout_queue_a";
String queueB = "fanout_queue_b";
channel.queueDeclare(queueA, false, false, true, null);
channel.queueDeclare(queueB, false, false, true, null);
// 当交换机类型是 BuiltinExchangeType.FANOUT 时,routingKey 为空串,不能为 null
channel.queueBind(queueA, exchangeName, "");
channel.queueBind(queueB, exchangeName, "");
String body = "pub/sub fanout 模式测试 ...";
channel.basicPublish(exchangeName, "", null, body.getBytes(StandardCharsets.UTF_8));
channel.close();
connection.close();
System.out.println("PubSubProducer done");
}
}
2.客户端A消费者
public class PubSubConsumerA {
public static String queueA = "fanout_queue_a";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("消费端等待接收消息......");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg = new String(message.getBody());
System.out.println("队列A接受到消息: " + msg);
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("队列A消息中断: " + consumerTag);
}
};
// autoAck: true 表示自动应答;false 表示手动应答
channel.basicConsume(queueA, true, deliverCallback, cancelCallback);
}
}
3.客户端B消费者
public class PubSubConsumerB {
public static String queueA = "fanout_queue_b";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("消费端等待接收消息......");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg = new String(message.getBody());
System.out.println("队列B接受到消息: " + msg);
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("队列B消息中断: " + consumerTag);
}
};
channel.basicConsume(queueA, true, deliverCallback, cancelCallback);
}
}
四、路由模式
该种模式就是在 Exchange 和 队列 绑定时需要指定一个 routingKey (路由key)。
- 生产者向 Exchange 发送消息时必须指定消息的 routingKey
- Exchange 不直接把消息交给绑定的队列,而是根据 routingKey 进行判断,只有队列中绑定的 routingKey 和 消息中的 routingKey 完全一直时,消息才会被转发到该队列中
1. 生产者
public class RouteProducer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String exchangeName = "exchange.direct.test";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, false, true, null);
String queueError = "direct_error";
String queueInfo = "direct_info";
channel.queueDeclare(queueError, false, false, true, null);
channel.queueDeclare(queueInfo, false, false, true, null);
channel.queueBind(queueError, exchangeName, "error");
// 队列B绑定routingKey : info error warning
channel.queueBind(queueInfo, exchangeName, "info");
channel.queueBind(queueInfo, exchangeName, "error");
channel.queueBind(queueInfo, exchangeName, "warning");
String body = "日志级别:info";
channel.basicPublish(exchangeName, "info", null, body.getBytes(StandardCharsets.UTF_8));
channel.close();
connection.close();
System.out.println("RouteProducer done");
}
}
启动生产者以后,我们可以在管理员后台看到队列 direct_info 中有一条未消费的消息,它绑定在 exchange.direct.test 这个 Exchange 中,对应的 routingKey 有 error 、info 和 warning
2. 错误信息消费者
public class RouteErrorConsumer {
public static String queueA = "direct_error";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("RouteError等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("RouteError 接受到消息 body : " + new String(body));
}
};
channel.basicConsume(queueA, true, consumer);
}
}
3. 正常信息消费者
public class RouteInfoConsumer {
private static String queueInfo = "direct_info";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("113.133.166.59");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("RouteInfo等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("RouteInfo 接受到消息 body : " + new String(body));
}
};
channel.basicConsume(queueInfo, true, consumer);
}
}
五、Topic 主题模式
该中模式可以实现 Pub/Sub 发布与订阅模式 和 Routing路由模式的 功能,只是 Topic 模式在配置 routingKey 时可以使用通配符,使得配置方式更加灵活。
- *(星号):可以代替一个单词
- #(井号):可以代替零个或者多个单词
- 当一个队列绑定的 routingKey 是 #(井号),那么这个队列将接收所有数据,类似 fanout 类型 Exchange
- 当一个队列绑定的 routingKey 中没有 #(井号)和 *(星号),类似 direct 类型 Exchange
根据上图routingKey的绑定关系,请看如下例子:
-
消息被 Q1 接受到
quick.orange.dog -
消息被 Q2 接受到
lazy.brown.dog
lazy.pink.rabbit (虽然满足两个routingKey绑定,但只被队列 Q2 接收一次) -
消息被 Q1、Q2 接受到
quick.orange.rabbit
lazy.orange.test -
消息被丢弃
quick.brown.dog (没有匹配上任何routingKey)
1. 生产者
public class TopicProducer {
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
String exchangeName = "exchange.topic.test";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);
/**
* Q1 :绑定routingKey 是 *.orange.*
* Q2 :绑定routingKey 是 *.*.rabbit 和 lazy.#
*/
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit", "Q1Q2:quick.orange.rabbit");
bindingKeyMap.put("lazy.orange.test", "Q1Q2:lazy.orange.test");
bindingKeyMap.put("quick.orange.dog", "Q1:quick.orange.dog");
bindingKeyMap.put("lazy.brown.dog", "Q2:lazy.brown.dog");
for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
String bindingKey = entry.getKey();
String body = entry.getValue();
// 发送消息
channel.basicPublish(exchangeName, bindingKey, null, body.getBytes(StandardCharsets.UTF_8));
}
System.out.println("TopicProducer done");
}
}
2. *.orange.* 的消费者
public class OrangeTopicConsumer {
public static String queueName = "Q1";
private static String exchangeName = "exchange.topic.test";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, exchangeName, "*.orange.*");
System.out.println("OrangeTopicConsumer等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("OrangeTopicConsumer 接受到消息 body : " + new String(body));
}
};
channel.basicConsume(queueName, true, consumer);
}
}
3. *.*.rabbit和 lazy.# 的消费者
public class LazyTopicConsumer {
public static String queueName = "Q2";
private static String exchangeName = "exchange.topic.test";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, exchangeName, "*.*.rabbit");
channel.queueBind(queueName, exchangeName, "lazy.#");
System.out.println("LazyTopicConsumer等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("LazyTopicConsumer 接受到消息 body : " + new String(body));
}
};
channel.basicConsume(queueName, true, consumer);
}
}
六、工作模式总结
- 简单模式
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。
- 工作队列模式(Work Queue)
一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
- 发布订阅模式(Publish / Subscribe)
需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
- 路由模式(Routing)
需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
- 通配符模式(Topic)
需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。