RabbitMQ 是最广泛部署使用的开源的消息代理中间件。
本文讲解几个入门示例:
- Hello World!
- 工作队列
- 发布订阅
- 路由模式
- Topics 模式
- 发布确认
Hello World!
Hello World! 示例实现了:
发布者发布 “Hello World!” 到队列,消费者从队列接受到 “Hello World!”
发布者实现
- 获取 RabbitMQ 连接
- 需指 RabbitMQ Server 地址、端口号、账号密码
private static final ConnectionFactory connectionFactory = new ConnectionFactory();
static {
connectionFactory.setHost(Constants.RABBITMQ_HOST);
connectionFactory.setVirtualHost(Constants.RABBITMQ_VIRTUAL_HOST);
// 不设置用户, 默认使用 guest 用户
}
public static Connection newConnection() throws IOException, TimeoutException {
return connectionFactory.newConnection();
}
- Connection 是 TCP 长连接,AMQP 0-9-1 协议使用 Channel 复用这个长连接,所以我们要
connection.createChannel()
获取通信通道来与 RabbitMQ 通信
- 声明队列
// 队列名, 是否持久化, 是否当前连接独占, 是否不使用后自动删除, 其他参数
channel.queueDeclare(Constants.QUEUE_HELLO_WORLD, true, false, false, null);
- 往队列发布消息
String message = "Hello World!";
// exchange,队列名称,其他参数,消息
channel.basicPublish("", Constants.QUEUE_HELLO_WORLD, null, message.getBytes());
Exchange 传了空字符串,是不用指定 Exchange ?
AMQP 0-9-1 规定了默认的 Exchange 的名称是空字符串,类型是 Direct,使用队列名作为 RoutingKey
消费者实现
- 获取 RabbitMQ 连接
- 声明队列
- 监听消息
- 取消接收消息的回调在消息队列被删除时触发,basicCancel 触发的是
com.rabbitmq.client.Consumer#handleCancelOk
channel.basicConsume(Constants.QUEUE_HELLO_WORLD, false
// 接收消息回调
, (consumerTag, deliver) -> {
log.info("[{}] receive: {}", consumerTag, new String(deliver.getBody()));
// false - 只应答当前消息
channel.basicAck(deliver.getEnvelope().getDeliveryTag(), false);
}
// 取消接受消息回调
, consumerTag -> {
log.info("[{}] ConsumerA cancel receive", consumerTag);
});
发布者、消费者都需要声明队列?
若只在一方声明队列,则必须保证声明方先启动。
AMQP 0-9-1 允许重复声明队列,发布者、消费者都声明可避免队列不存在。不过声明队列时指定的属性和前一次不一致时会报错。
执行结果
- 启动发布者
- 启动消费者
消费者控制台:
工作队列
发布者发布任务,消费者处理任务。
发布者实现
- 获取 RabbitMQ 连接
- 声明队列
- 往队列发布任务(消息)
消费者实现
实现两个消费者 C1、C2:
- 获取 RabbitMQ 连接
- 声明队列
- 接收任务(消息)
channel.basicConsume(Constants.QUEUE_HELLO_WORLD, false
// 接收消息回调
, (consumerTag, deliver) -> {
String task = new String(deliver.getBody());
log.info("[{}] ConsumerA receive: {}", consumerTag, task);
try {
// C1 睡 10 ms
// Thread.sleep(10);
// C2 睡 5 ms
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("done task: {}", task);
channel.basicAck(deliver.getEnvelope().getDeliveryTag(), false);
}
// 取消接受消息回调
, consumerTag -> {
log.info("[{}] ConsumerA cancel receive", consumerTag);
});
假设消费者处理任务时宕机了,如何保证消费者确实成功执行了任务,任务没有丢失呢?
通过消息接收确认机制,消费者可以在成功处理完任务后再调用basicAck
方法应答,Broker 在收到应答再移除任务。
若处理任务时发生错误,可调用basicNack
表示拒绝消息,拒绝的消息可指定直接废弃,还是重新入队。
执行结果
- 启动消费者C1、C2
- 启动发布者
发布者发布 10 个任务
for (int i = 0; i < 10; i++) {
String message = String.format("task [%d]", i);
channel.basicPublish("", Constants.QUEUE_WORK_QUEUES, null, message.getBytes());
}
C1 控制台:
C2 控制台:
C1、C2 处理任务效率不一样,确分到相同数量的任务,为什么呢?
因为 Broker 默认不会等消费者处理完一个任务再发送下一个任务,直接持续轮询分配任务。
可以通过basicQos(n)
方法指定消费者处理完 n 个任务后再分配下 n 个任务。
channel.basicQos(1)
指定处理完一个任务再分配下一个任务,执行结果如下:
C1 控制台:
C2 控制台:
发布订阅
- Echange 将消息路由给所有队列
发布者 P 发布消息 M,消费者 C1、C2 都会收到消息 M。
发布者实现
- 获取 RabbitMQ 连接
- 声明 Exchange
// 交易所名称, 交易所类型
channel.exchangeDeclare(Constants.EXCHANGE_FANOUT, BuiltinExchangeType.FANOUT);
- 往 Exchange 发布消息
订阅者实现
- 获取 RabbitMQ 连接
- 声明队列
- 将队列绑定到 Exchange
channel.queueBind(Constants.QUEUE_FANOUT_A, Constants.EXCHANGE_FANOUT, "");
- 监听队列消息
路由模式
- Echange 根据 RoutingKey 将消息路由给目标队列
发布者 P 发布消息 M1 ,M1 的 RoutingKey = error,C1、C2 都会收到消息 M1;
发布者 P 发布消息 M2,M2 的 RoutingKey = error,只有 C2 收到消息 M2。
发布者实现
- 获取 RabbitMQ 连接
- 声明 Exchange
// 交易所名称, 交易所类型
channel.exchangeDeclare(Constants.EXCHANGE_DIRECT, BuiltinExchangeType.DIRECT);
- 往 Exchange 发布消息
消费者实现
实现两个消费者 C1、C2:
- 获取 RabbitMQ 连接
- 声明队列
- 将队列绑定到 Exchange
// C1
channel.queueBind(Constants.QUEUE_DIRECT_A, Constants.EXCHANGE_DIRECT, "info");
channel.queueBind(Constants.QUEUE_DIRECT_A, Constants.EXCHANGE_DIRECT, "warn");
channel.queueBind(Constants.QUEUE_DIRECT_A, Constants.EXCHANGE_DIRECT, "error");
// C2
channel.queueBind(Constants.QUEUE_DIRECT_B, Constants.EXCHANGE_DIRECT, "error");
- 监听队列消息
执行结果
- 启动发布者
- 启动消费者 C1、C2
发布者:
消费者 C1:
消费者 C2:
Topics 模式
- 队列绑定 Exchange 时指定通配符
- Exchange 根据消息的 RoutingKey 与 队列的通配符匹配,匹配上则将消息路由给目标队列
通配符规则:
- RoutingKey 由多个词组成,词之间通过
.
分割,格式与域名类似 *
匹配一个词#
匹配多个词
示例代码
发布确认
看了【工作队列 】示例,我们知道 AMQP 保证消费消息的可靠是通过接收消息确认机制,那如何保证发布消息的可靠性呢?
发布确认机制,在 Broker 成功接收消息后,给发布者回复确认,从而保证发布消息的可靠性。
- 同步等待确认:
// 等待 1000 ms
channel.waitForConfirms(1000);
- 异步监听确认:
channel.addConfirmListener((deliveryTag, multiple) -> {
// Broker 正常接收
log.info("[async] message {} confirmed", deliveryTag);
}, (deliveryTag, multiple) -> {
// Broker 接收不到消息
log.info("[async] message {} un confirmed", deliveryTag);
});
RabbitMQ 还支持返回发布者没消费者订阅的消息,即发送的消息 Exchange 无法路由给任何队列。
channel.addReturnListener(returnMessage -> {
log.info(“{} is returned, because {}”
, new String (returnMessage.getBody()), returnMessage.getReplyText());
});