RabbitMQ 介绍
一、RabbitMQ 概念
- RabbitMQ 是一个消息中间件,它接受并转发消息。
- 你可以把它当做一个 快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里。
- RabbitMQ 与快递站的主要区别在于,它不处理快件而是 接收,存储 和 转发 消息数据。
二、RabbitMQ 工作原理
名词 | 描述 |
---|---|
Broker | 接收 和 分发 消息的应用,RabbitMQ Server 就是 Message Broker |
Virtual Host | 出于 多租户 和 安全因素 设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 Namespace 概念。当多个不同的用户使用同一个 RabbitMQ Server 提供的服务时,可以划分出多个 Virtual Host,每个用户在自己的 Virtual Host 创建 Exchange 和 Queue 等 |
Connection | Publisher、Consumer 和 Broker 之间的 TCP 连接 |
Channel | 如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 Connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 Thread 创建单独的 Channel 进行通讯,AMQP Method 包含了 Channel Id 帮助客户端 和 Message Broker 识别 Channel,所以 Channel 之间是完全隔离的。Channel 作为轻量级的 Connection,极大减少了操作系统建立 TCP Connection 的开销 |
Exchange | Message 到达 Broker 的第一站,根据分发规则,匹配查询表中的 Routing Key,分发消息到 Queue 中去。常用的类型有:扇出Fanout(multicast)、直接Direct(point-to-point),主题Topic(publish-subscribe)、标题Headers |
Queue | 消息最终被送到这里等待 Consumer 取走 |
Binding | Exchange 和 Queue 之间的虚拟连接,Binding 中可以包含 routing_key ,Binding 信息被保存到 Exchange 中的查询表中,用于 Message 的分发依据 |
三、RabbitMQ 四大核心
1. 生产者(Producer
)
- 产生数据发送消息的程序是生产者。
2. 交换机(Exchange
)
- 交换机 是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。
- 交换机 必须确切的知道如何处理它接收到的消息,是将这些消息推送到特定队列 还是推送到多个队列,亦或者是把消息丢弃,这个得由 交换机类型 决定。
- RabbitMQ 消息传递模型的核心思想是:生产者生产的消息 不会 直接发送到队列。
- 实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
- 相反,生产者只能将消息发送到交换机(Exchange)。
2.1 交换机类型
2.1.1 扇出(
Fanout
)
2.1.2 直接(
Direct
)
2.1.3 主题(
Topic
)
2.1.4 标题(
Headers
)
2.2 无名交换机
- 默认交换,通过 空字符串(
""
) 进行标识。
/**
* 生产者发送一个消息。
* 1. 发送到那个交换机(`""`代表默认交换机 或 无名称交换机)。
* 2. 路由的`key`是那个(消息能路由发送到队列中,其实是由`routingKey(bindingkey)`绑定`key`指定的)。
* 3. 其他的参数信息。
* 4. 发送的消息体。
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
2.3 延迟交换机
- RabbitMQ 插件。
2.4 备份交换机
- 发布确认高级用到。
3. 队列(Queue
)
- 队列 是 RabbitMQ 内部使用的一种 数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。
- 队列 仅受主机的内存 和 磁盘 限制的约束,本质上是一个大的 消息缓冲区。
- 许多 生产者 可以将消息发送到一个队列,许多 消费者 可以尝试从一个队列接收数据。
- 这就是我们使用队列的方式。
3.1 临时队列
- 随机名称的队列。
3.2 死信队列
- 消息 TTL(Time To Live)生存时间值过期。
- 队列达到最大长度。
- 消息被拒绝。
3.3 延迟队列
4. 消费者(Consumer
)
- 消费 与 接收 具有相似的含义,消费者大多时候是一个等待接收消息的程序。
- 请注意 生产者、消费者 和 消息中间件 很多时候并不在同一机器上。
- 同一个应用程序既可以是 生产者,又是可以是 消费者。
四、交换机类型
1. 扇出(Fanout
)
- Fanout 是将接收到的消息 广播 到他绑定的所有队列中(默认创建的 Exchange 类型)。
1.1 架构图
- Logs 和 临时队列 的绑定关系如下图。
1.2 消费者
/**
* @author: wy
* describe: 接收日志
*/
public class ReceiveLogsFanout1 {
// 交换机名称
private static final String EXCHANGE_NAME = "fanout_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/*
声明一个队列(临时队列)
1. 队列名称是随机的
2. 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机 与 队列
channel.queueBind(queueName, EXCHANGE_NAME, "");
// 接收消息
channel.basicConsume(queueName, true,
// 接收消息
(consumerTag, message) -> {
System.out.println(message);
System.out.printf("ReceiveLogsFanout1,接收到消息: %s\r\n", new String(message.getBody()));
},
// 取消消息
consumerTag -> {
System.out.printf("ReceiveLogsFanout1,中断消费消息: %s\r\n", consumerTag);
});
System.out.println("ReceiveLogsFanout1,等待接收消费...");
}
}
1.3 生产者
/**
* @author: wy
* describe: 发送日志
* 发布订阅模式
*/
public class EmitLogFanout {
// 交换机名称
private static final String EXCHANGE_NAME = "fanout_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 从控制台接收消息
Scanner scanner = new Scanner(System.in);
System.out.println("输入消息: ");
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.printf("发送消息: %s\r\n", message);
}
}
}
2. 直接(Direct
)
- 上面我们构建了一个简单的日志记录系统,能够向许多接收者广播日志消息。
- 现在我们将向其中添加一些特别的功能,比如说我们 只让某个消费者订阅部分消息。
- 例如:我们希望将日志消息写入磁盘的程序,仅接收 严重错误(Error)。
- 而不存储哪些 警告(Warning)或 信息(Info)日志,避免浪费磁盘空间。
- Fanout 这种交换机类型并不能给我们很大的灵活性,它只能进行无意识的广播。
- 在这里我们将使用
Direct类型交换机
来进行替换,这种类型的工作方式是,消息只去绑定的RoutingKey
队列中去。
- 在上面图中,我们可以看到 X 交换机 绑定了两个队列,交换机类型是 Direct。
- 队列 Q1 绑定键为 orange,队列 Q2 绑定键有两个,一个绑定键为 black,另一个绑定键为 green。
- 在上面这种绑定情况下,生产者发布消息到 Exchange 上,绑定键为 orange 的消息会被发布到 队列 Q1。
- 绑定键为 black 或 green 的消息会被发布到 队列 Q2,其他消息类型的消息将被丢弃。
2.1 架构图
2.2 消费者一
/**
* @author: wy
* describe: 接收日志
*/
public class ReceiveLogsDirect1 {
// 交换机名称
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 声明一个队列
String queueName = "console";
channel.queueDeclare(queueName, false, false, false, null);
// 绑定交换机 与 队列
channel.queueBind(queueName, EXCHANGE_NAME, "info");
channel.queueBind(queueName, EXCHANGE_NAME, "warning");
// 接收消息
channel.basicConsume(queueName, true,
// 接收消息
(consumerTag, message) -> {
System.out.println(message);
System.out.printf("ReceiveLogsDirect1,接收到消息: %s\r\n", new String(message.getBody()));
},
// 取消消息
consumerTag -> {
System.out.printf("ReceiveLogsDirect1,中断消费消息: %s\r\n", consumerTag);
});
System.out.println("ReceiveLogsDirect1,等待接收消费...");
}
}
2.3 消费者二
/**
* @author: wy
* describe: 接收日志
*/
public class ReceiveLogsDirect2 {
// 交换机名称
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 声明一个队列
String queueName = "disk";
channel.queueDeclare(queueName, false, false, false, null);
// 绑定交换机 与 队列
channel.queueBind(queueName, EXCHANGE_NAME, "error");
// 接收消息
channel.basicConsume(queueName, true,
// 接收消息
(consumerTag, message) -> {
System.out.println(message);
System.out.printf("ReceiveLogsDirect2,接收到消息: %s\r\n", new String(message.getBody()));
},
// 取消消息
consumerTag -> {
System.out.printf("ReceiveLogsDirect2,中断消费消息: %s\r\n", consumerTag);
});
System.out.println("ReceiveLogsDirect2,等待接收消费...");
}
}
2.4 生产者
/**
* @author: wy
* describe: 发送日志
* 路由模式
* 一、direct_logs交换机
* 1. console队列
* 1.1 routing_key_info
* 1.2 routing_key_warning
* 2. disk队列
* 2.1 routing_key_error
*/
public class EmitLogDirect {
// 交换机名称
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 从控制台接收消息
Scanner scanner = new Scanner(System.in);
System.out.println("输入消息: ");
while (scanner.hasNext()) {
String message = scanner.next();
// info、warning、error
String routingKey = "error";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.printf("发送消息: %s\r\n", message);
}
}
}
2.5 多重绑定
- 上图所示,如果 Exchange 的类型是 Direct,但是它绑定的多个队列的 RoutingKey 都相同。
- 在这种情况下虽然绑定类型是 Direct,但是它和 Fanout 有点类似,跟广播差不多。
3. 主题(Topic
)
- 上面改进了日志记录系统,不再使用只能进行随意广播的 Fanout 交换机,而是使用了 Direct 交换机。
- 从而实现有选择性地接收日志。
- 尽管使用 Direct 交换机改进了我们的系统,但是它仍然存在局限性。
- 比如说:我们想接收的日志类型有
info.base
和info.advantage
,某个队列只想接收info.base
的消息,那这个时候 Direct 就办不到了。- 这个时候就只能使用 Topic 类型。
3.1 规则要求
- 发送到 Topic 类型交换机的消息的 RoutingKey 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。
- 这些单词可以是任意单词,比如说:
stock.usd.nyse
、nyse.vmw
、quick.orange.rabbit
,这些类型的。- 当然这个单词列表最多不能超过 255 个字节。
- 在这个规则列表中,其中有两个替换符是需要注意的。
*
(星号):可以代替一个单词。#
(井号):可以替代 零个 或 多个 单词。
- 当一个队列绑定键是
#
,那么这个队列将接收所有数据,就有点像 Fanout 了。- 如果 队列 绑定键当中没有
#
和*
出现,那么该队列绑定类型就是 Direct 了。
3.2 架构图
- Q1 绑定的是中间带 orange 的三个单词的字符串
*.orange.*
- Q2 绑定的是最后一个是 rabbit 的三个单词的字符串
*.*.rabbit
,和第一个单词是 lazy 的多个单词的字符串lazy.#
RoutingKey | 结果 |
---|---|
quick.orange.rabbit | 被队列 Q1、Q2 接收到 |
lazy.orange.elephant | 被队列 Q1、Q2 接收到 |
quick.orange.fox | 被队列 Q1 接收到 |
lazy.brown.fox | 被队列 Q2 接收到 |
lazy.pink.rabbit | 虽然同时满足两个绑定,但只被队列 Q2 接收一次 |
quick.brown.fox | 不匹配任何绑定,不会被任何队列接收到,会被丢弃 |
quick.orange.male.rabbit | 四个单词不匹配任何绑定,会被丢弃 |
lazy.orange.male.rabbit | 四个单词,但匹配 Q2 |
3.3 消费者一
/**
* @author: wy
* describe: 消费者
*/
public class ReceiveLogsTopic1 {
// 交换机名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 声明队列
String queueName = "Q1";
channel.queueDeclare(queueName, false, false, false, null);
// 队列绑定交换机
String routingKey = "*.orange.*";
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
// 接收消息
channel.basicConsume(queueName, true,
(consumerTag, message) -> {
System.out.printf("ReceiveLogsTopic1,接收到消息: %s, queueName: %s, routingKey: %s",
new String(message.getBody(), "UTF-8"),
queueName,
message.getEnvelope().getRoutingKey()).println();
},
consumerTag -> {
});
System.out.println("ReceiveLogsTopic1, 等待接收消费...");
}
}
3.4 消费者二
/**
* @author: wy
* describe: 消费者
*/
public class ReceiveLogsTopic2 {
// 交换机名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 声明队列
String queueName = "Q2";
channel.queueDeclare(queueName, false, false, false, null);
// 队列绑定交换机
String routingKey1 = "*.*.rabbit";
String routingKey2 = "lazy.*.*";
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
// 接收消息
channel.basicConsume(queueName, true,
(consumerTag, message) -> {
System.out.printf("ReceiveLogsTopic2,接收到消息: %s, queueName: %s, routingKey: %s",
new String(message.getBody(), "UTF-8"),
queueName,
message.getEnvelope().getRoutingKey()).println();
},
consumerTag -> {
});
System.out.println("ReceiveLogsTopic2, 等待接收消费...");
}
}
3.5 生产者
/**
* @author: wy
* describe: 生产者
* quick.orange.rabbit 被队列 Q1Q2 接收到
* lazy.orange.elephant 被队列 Q1Q2 接收到
* quick.orange.fox 被队列 Q1 接收到
* lazy.brown.fox 被队列 Q2 接收到
* lazy.pink.rabbit 虽然满足两个绑定但只被队列 Q2 接收一次
* quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃
* quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃
* lazy.orange.male.rabbit 是四个单词但匹配 Q2
*/
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
private static final Map<String, String> bindingKeyMap = new HashMap<>();
static {
bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");
}
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 发送消息
for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
String routingKey = entry.getKey();
String message = entry.getValue();
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.printf("EmitLogTopic, 发送消息: %s", message).println();
}
}
}
五、队列
1. 临时队列
- 我们连接到 RabbitMQ 时,都需要一个全新的空队列。
- 为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。
- 其次一旦我们断开了消费者的连接,队列将被自动删除。
/*
声明一个队列(临时队列)
1. 队列名称是随机的
2. 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
2. 死信队列
- 死信,顾名思义就是无法被消费的消息,字面意思可以这样理解。
- 一般来说,Producer 将消息投递到 Broker 或者直接到 Queue 里了,Consumer 从 Queue 取出消息进行消费,但某些时候由于特定的原因
导致 Queue 中的某些消息无法被消费
,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
- 应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的 死信队列 机制。
- 当消息消费发生异常时,将消息投入到死信队列中。
- 还有比如说:用户在商城下单成功并点击去支付后,在指定时间未支付将自动失效。
2.1 死信来源
2.1.1 消息 TTL
过期
- 消费者。
// 1. 消息`TTL`过期,设置过期时间`100`秒(也可以在生产方设置)
arguments.put("x-message-ttl", 100 * 1000);
- 生产者。
// 1. 消息`TTL`过期,设置`TTL`过期时间`10`秒(ttl: Time to live)
AMQP.BasicProperties props = new AMQP.BasicProperties().builder().expiration("10000").build();
channel.basicPublish(NORMAL_EXCHANGE, NORMAL_ROUTING_KEY, props, message.getBytes("UTF-8"));
2.1.2 队列达到最大长度
- 队列满了,无法再添加数据到
MQ
中。
// 2. 队列达到最大长度,设置正常队列长度限制
arguments.put("x-max-length", 6);
2.1.3 消息被拒绝
basicReject(用于拒绝消息)
或basicNack(用于否定确认)
,并且requeue=false
。
// 是否开启自动应答
boolean autoAck = false;
// 正常 接收消息
channel.basicConsume(NORMAL_QUEUE, autoAck,
(consumerTag, message) -> {
String msg = new String(message.getBody(), "UTF-8");
if (msg.endsWith("6")) {
/**
* 3. 消息被拒绝(拒绝info: 6)
* requeue 是否放回队列(false不放回)
*/
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
System.out.printf("拒绝到消息: %s, queueName: %s, routingKey: %s",
msg,
NORMAL_QUEUE,
message.getEnvelope().getRoutingKey()).println();
} else {
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.printf("接收到消息: %s, queueName: %s, routingKey: %s",
msg,
NORMAL_QUEUE,
message.getEnvelope().getRoutingKey()).println();
}
},
consumerTag -> {
});
2.2 架构图
2.3 消费者一
/**
* @author: wy
* describe: 消费者
* 死信队列
*/
public class Consumer1 {
// 交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
private static final String DEAD_EXCHANGE = "dead_exchange";
// 队列名称
private static final String NORMAL_QUEUE = "normal-queue";
private static final String DEAD_QUEUE = "dead-queue";
// routingKey
private static final String NORMAL_ROUTING_KEY = "zhangsan";
private static final String DEAD_ROUTING_KEY = "lisi";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 声明正常交换机、死信交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
// 正常队列参数
Map<String, Object> arguments = new HashMap<>();
// 指定死信交换机
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// 指定死信RoutingKey
arguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
// 1. 消息TTL过期,设置过期时间100秒(也可以在生产方设置)
// arguments.put("x-message-ttl", 100 * 1000);
// 2. 队列达到最大长度,设置正常队列长度限制
// arguments.put("x-max-length", 6);
// 声明正常队列
channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
// 声明死信队列
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
// 队列 绑定 交换机
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_ROUTING_KEY);
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_ROUTING_KEY);
// 是否开启自动应答
boolean autoAck = false;
// 正常 接收消息
channel.basicConsume(NORMAL_QUEUE, autoAck,
(consumerTag, message) -> {
String msg = new String(message.getBody(), "UTF-8");
if (msg.endsWith("6")) {
/**
* 3. 消息被拒绝(拒绝info: 6)
* requeue 是否放回队列(false不放回)
*/
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
System.out.printf("拒绝到消息: %s, queueName: %s, routingKey: %s",
msg,
NORMAL_QUEUE,
message.getEnvelope().getRoutingKey()).println();
} else {
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.printf("接收到消息: %s, queueName: %s, routingKey: %s",
msg,
NORMAL_QUEUE,
message.getEnvelope().getRoutingKey()).println();
}
},
consumerTag -> {
});
System.out.println("Consumer1, 等待接收消息...");
}
}
2.4 消费者二
/**
* @author: wy
* describe: 消费者
* 死信队列
*/
public class Consumer2 {
// 队列名称
private static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
// 接收消息
channel.basicConsume(DEAD_QUEUE, true,
(consumerTag, message) -> {
System.out.printf("接收到消息: %s, queueName: %s, routingKey: %s",
new String(message.getBody(), "UTF-8"),
DEAD_QUEUE,
message.getEnvelope().getRoutingKey()).println();
},
consumerTag -> {
});
System.out.println("Consumer2, 等待接收消息...");
}
}
2.5 生产者
/**
* @author: wy
* describe: 生产者
* 死信队列
* 1. 消息TTL过期(延迟队列)
* 2. 队列达到最大长度
* 3. 消息被拒绝
*/
public class Producer {
// 交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
// routingKey
private static final String NORMAL_ROUTING_KEY = "zhangsan";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
// 1. 消息TTL过期,设置TTL过期时间10秒(ttl: time to live)
// AMQP.BasicProperties props = new AMQP.BasicProperties().builder()
// .expiration("10000").build();
for (int i = 0; i < 10; i++) {
String message = String.format("info: %s", i);
// channel.basicPublish(NORMAL_EXCHANGE, NORMAL_ROUTING_KEY, props, message.getBytes("UTF-8"));
// TimeUnit.SECONDS.sleep(2);
channel.basicPublish(NORMAL_EXCHANGE, NORMAL_ROUTING_KEY, null, message.getBytes("UTF-8"));
System.out.printf("发送消息: %s", message).println();
}
}
}
3. 延迟队列
- 队列内部是有序的,最重要的特性就体现在它的延时属性上。
- 延时队列中的元素是希望在指定时间到了以后,或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
3.1 延迟使用场景
- 订单在十分钟之内未支付则自动取消。
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。
- 这些场景都有一个特点,需要在某个事件发生之后 或者 之前的指定时间点完成某一项任务。
- 如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭。
- 看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做。
- 如:对于账单一周内未支付则进行自动结算这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。
- 但对于数据量比较大,并且时效性较强的场景。
- 如:订单十分钟内未支付则关闭,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
3.2 RabbitMQ
中的 TTL
- TTL 是 RabbitMQ 中一个消息或者队列的属性。
- 表明一条消息 或者 该队列中的所有消息的最大存活时间, 单位是毫秒。
- 换句话说,如果一条消息设置了
TTL
属性或者进入了设置TTL
属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为 死信。- 如果同时配置了队列的 TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
3.2.1 队列设置 TTL
/**
* 设置队列TTL为10秒
*/
@Bean("queueA")
public Queue queueA() {
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 队列设置TTL
arguments.put("x-message-ttl", 10 * 1000);
return QueueBuilder
.durable(QUEUE_A)
.withArguments(arguments)
.build();
}
3.2.2 消息设置 TTL
// 发送消息的时候,设置延迟时长
message.getMessageProperties().setExpiration(ttlTime);
3.2.3 两者的区别
- 如果设置了 队列 的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列会被丢到死信队列中)。
- 消息 设置了 TTL 属性,消息即使过期,也不一定会被马上丢弃,因为 消息是否过期是在即将投递到消费者之前判定的。如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间。
- 如果不设置 TTL,表示消息永远不会过期。
- 如果将 TTL 设置为 0,则表示此时可以直接投递给该消息到消费者,否则该消息将会被丢弃。
- 延时队列,不就是想要消息延迟多久被处理吗?TTL 则刚好能让消息在延迟多久之后成为死信。
- 另一方面,成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就完事了,因为里面的消息都是希望被立即处理的消息。
3.3 队列设置 TTL
3.3.1 架构图
- 创建两个队列 QA 和 QB,两个队列 TTL 分别设置为 10S 和 40S。
- 然后在创建一个 交换机 X 和 死信交换机 Y,它们的类型都是 Direct。
- 创建一个 死信队列 QD。
- 它们的绑定关系如下。
- 第一条消息在 10S 后变成了死信消息,然后被消费者消费掉。
- 第二条消息在 40S 后变成了死信消息,然后被消费者消费掉。
- 这样一个延时队列就打造完成了。
3.3.2 Config
/**
* @author: wy
* describe: 死信队列配置
* 一、X交换机
* 1. QA队列,XA,TTL10秒
* 2. QB队列,XB,TTL40秒
* 3. QC队列,XC,不设置TTL
* 二、Y交换机
* 1. QD死信队列,YD
*/
@Configuration
public class TTLQueueConfig {
// 普通交换机
public static final String X_EXCHANGE = "X";
// 普通队列,TTL为10秒
public static final String QUEUE_A = "QA";
// TTL为40秒
public static final String QUEUE_B = "QB";
// 死信交换机
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
// 死信队列
public static final String DEAD_LETTER_QUEUE_D = "QD";
/**
* DirectExchange 直接交换机
*/
@Bean("xExchange")
public DirectExchange xExchange() {
return new DirectExchange(X_EXCHANGE);
}
@Bean("yExchange")
public DirectExchange yExchange() {
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
/**
* 设置队列TTL为10秒
*/
@Bean("queueA")
public Queue queueA() {
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 设置TTL
arguments.put("x-message-ttl", 10 * 1000);
return QueueBuilder
.durable(QUEUE_A)
.withArguments(arguments)
.build();
}
/**
* 设置队列TTL为40秒
*/
@Bean("queueB")
public Queue queueB() {
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 设置TTL
arguments.put("x-message-ttl", 40 * 1000);
return QueueBuilder
.durable(QUEUE_B)
.withArguments(arguments)
.build();
}
/**
* 死信队列
*/
@Bean("queueD")
public Queue queueD() {
return QueueBuilder
.durable(DEAD_LETTER_QUEUE_D)
.build();
}
/**
* queueA 绑定 xExchange
*/
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueA)
.to(xExchange)
.with("XA");
}
/**
* queueB 绑定 xExchange
*/
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueB)
.to(xExchange)
.with("XB");
}
/**
* queueD 绑定 yExchange
*/
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange) {
return BindingBuilder
.bind(queueD)
.to(yExchange)
.with("YD");
}
}
3.3.3 消费者
- 死信队列。
/**
* @author: wy
* describe: 消费者消费消息
* 死信队列
*/
@Slf4j
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = TTLQueueConfig.DEAD_LETTER_QUEUE_D)
public void receiveQueueD(Message message, Channel channel) {
String msg = new String(message.getBody());
log.info("当前时间: {}, 收到死信队列的消息: {}", new Date().toString(), msg);
}
}
3.3.4 生产者
/**
* @author: wy
* describe: 生产者发消息
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 1. 发送固定时间的延迟消息
* http://127.0.0.1:8080/ttl/sendMsg/%E4%BD%A0%E5%A5%BD%E5%91%80
*/
@GetMapping("/sendMsg/{msg}")
public String sendMsg(@PathVariable String msg) {
log.info("当前时间: {}, 发送一条消息给两个TTL队列: {}", new Date().toString(), msg);
rabbitTemplate.convertAndSend(TTLQueueConfig.X_EXCHANGE, "XA", String.format("消息来自TTL为10秒的队列: %s", msg));
rabbitTemplate.convertAndSend(TTLQueueConfig.X_EXCHANGE, "XB", String.format("消息来自TTL为40秒的队列: %s", msg));
return "success";
}
}
3.4 消息设置 TTL
- 上面如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列。
- 这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列。
- 如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
3.4.1 架构图
- 在这里新增了一个队列 QC,绑定关系如下,该队列不设置
TTL
时间,由生产者设置消息 TTL。
- 看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并 不会按时死亡。
- 因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时很长,而第二个消息的延时很短,第二个消息并不会优先得到执行。
3.4.2 Config
/**
* @author: wy
* describe: 死信队列配置
* 一、X交换机
* 1. QA队列,XA,TTL10秒
* 2. QB队列,XB,TTL40秒
* 3. QC队列,XC,不设置TTL
* 二、Y交换机
* 1. QD死信队列,YD
*/
@Configuration
public class TTLQueueConfig {
// 普通交换机
public static final String X_EXCHANGE = "X";
// 普通队列,TTL为10秒
public static final String QUEUE_A = "QA";
// TTL为40秒
public static final String QUEUE_B = "QB";
// 不设置TTL
public static final String QUEUE_C = "QC";
// 死信交换机
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
// 死信队列
public static final String DEAD_LETTER_QUEUE_D = "QD";
/**
* DirectExchange 直接交换机
*/
@Bean("xExchange")
public DirectExchange xExchange() {
return new DirectExchange(X_EXCHANGE);
}
@Bean("yExchange")
public DirectExchange yExchange() {
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
/**
* 设置队列TTL为10秒
*/
@Bean("queueA")
public Queue queueA() {
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 设置TTL
arguments.put("x-message-ttl", 10 * 1000);
return QueueBuilder
.durable(QUEUE_A)
.withArguments(arguments)
.build();
}
/**
* 设置队列TTL为40秒
*/
@Bean("queueB")
public Queue queueB() {
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key", "YD");
// 设置TTL
arguments.put("x-message-ttl", 40 * 1000);
return QueueBuilder
.durable(QUEUE_B)
.withArguments(arguments)
.build();
}
/**
* 不设置TTL
*/
@Bean("queueC")
public Queue queueC() {
Map<String, Object> arguments = new HashMap<>(2);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信RoutingKey
arguments.put("x-dead-letter-routing-key", "YD");
return QueueBuilder
.durable(QUEUE_C)
.withArguments(arguments)
.build();
}
/**
* 死信队列
*/
@Bean("queueD")
public Queue queueD() {
return QueueBuilder
.durable(DEAD_LETTER_QUEUE_D)
.build();
}
/**
* queueA 绑定 xExchange
*/
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueA)
.to(xExchange)
.with("XA");
}
/**
* queueB 绑定 xExchange
*/
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueB)
.to(xExchange)
.with("XB");
}
/**
* queueC 绑定 xExchange
*/
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder
.bind(queueC)
.to(xExchange)
.with("XC");
}
/**
* queueD 绑定 yExchange
*/
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange) {
return BindingBuilder
.bind(queueD)
.to(yExchange)
.with("YD");
}
}
3.4.3 消费者
/**
* @author: wy
* describe: 消费者消费消息
* 死信队列
*/
@Slf4j
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = TTLQueueConfig.DEAD_LETTER_QUEUE_D)
public void receiveQueueD(Message message, Channel channel) {
String msg = new String(message.getBody());
log.info("当前时间: {}, 收到死信队列的消息: {}", new Date().toString(), msg);
}
}
3.4.4 生产者
/**
* @author: wy
* describe: 生产者发消息
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 1.1 发送指定过期时间的延迟消息
* http://127.0.0.1:8080/ttl/sendMsgToQC/%E4%BD%A0%E5%A5%BD1/20000 延迟20秒
* http://127.0.0.1:8080/ttl/sendMsgToQC/%E4%BD%A0%E5%A5%BD2/2000 延迟2秒
*/
@GetMapping("/sendMsgToQC/{msg}/{ttlTime}")
public String sendMsgToQC(@PathVariable String msg, @PathVariable String ttlTime) {
log.info("当前时间: {}, 发送一条TTL为{}毫秒的消息给QC队列: {}", new Date().toString(), ttlTime, msg);
rabbitTemplate.convertAndSend(TTLQueueConfig.X_EXCHANGE, "XC",
String.format("消息来自TTL为%s毫秒的QC队列: %s", ttlTime, msg),
message -> {
// 发送消息的时候,延迟时长
message.getMessageProperties().setExpiration(ttlTime);
return message;
});
return "success";
}
}
- 问题:消息一延时 20 秒,消息二延时 2 秒,而消息二必须要等待消息一延时超时。
- 所以消息二也延时 20 秒。
3.5 RabbitMQ
插件——实现延迟队列
- 上文中提到的问题,如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,就无法设计成一个通用的延时队列。那如何解决呢?
x-delayed-message
交换机。
3.5.1 架构图
- 在这里新增了一个队列
delayed.queue
,一个自定义交换机delayed.exchange
,绑定关系如下。
- 在我们自定义的交换机中,
x-delayed-message
是一种新的交换类型,该类型消息 支持延迟投递机制。- 消息传递后并不会立即投递到目标队列中,而是存储在
mnesia
(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。
3.5.2 Config
/**
* @author: wy
* describe: 延迟交换机配置
* 基于`x-delayed-message`插件延迟消息
*/
@Configuration
public class DelayExchangeConfig {
// 延迟交换机
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
// 队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
// RoutingKey
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public CustomExchange delayExchange() {
Map<String, Object> arguments = new HashMap<>();
// 延迟类型"直接"
arguments.put("x-delayed-type", "direct");
/**
* 1. 交换机的名称
* 2. 交换机的类型
* 3. 是否需要持久化
* 4. 是否需要自动删除
* 5. 其他的参数
*/
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", false, false, arguments);
}
@Bean
public Queue delayQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
/**
* delayedQueue 绑定 DelayedExchange
*/
@Bean
public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayQueue") Queue delayQueue,
@Qualifier("delayExchange") CustomExchange delayExchange) {
return BindingBuilder
.bind(delayQueue)
.to(delayExchange)
.with(DELAYED_ROUTING_KEY)
.noargs();
}
}
3.5.3 消费者
/**
* @author: wy
* describe: 延迟交换机
* 基于插件延迟消息
*/
@Slf4j
@Component
public class DelayExchangeConsumer {
@RabbitListener(queues = DelayExchangeConfig.DELAYED_QUEUE_NAME)
public void receiveQueueD(Message message, Channel channel) {
String msg = new String(message.getBody());
log.info("当前时间: {}, 收到延迟队列的消息: {}", new Date().toString(), msg);
}
}
3.5.4 生产者
/**
* @author: wy
* describe: 生产者发消息
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 2. 基于插件,发送指定过期时间的延迟消息
* http://127.0.0.1:8080/ttl/sendDelayMsg/%E4%BD%A0%E5%A5%BD1/20000
* http://127.0.0.1:8080/ttl/sendDelayMsg/%E4%BD%A0%E5%A5%BD2/2000
*/
@GetMapping("/sendDelayMsg/{msg}/{delayTime}")
public String sendDelayMsg(@PathVariable String msg, @PathVariable Integer delayTime) {
log.info("当前时间: {}, 发送一条时长{}的延迟消息给delayed.queue队列: {}", new Date().toString(), delayTime, msg);
rabbitTemplate.convertAndSend(
DelayExchangeConfig.DELAYED_EXCHANGE_NAME,
DelayExchangeConfig.DELAYED_ROUTING_KEY,
String.format("消息来自时长为%s毫秒的delayed.queue队列: %s", delayTime, msg),
message -> {
// 发送消息的时候,延迟时长
message.getMessageProperties().setDelay(delayTime);
return message;
});
return "success";
}
}
3.6 延迟队列总结
- 延时队列 在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性。
- 如:消息可靠发送、消息可靠投递、死信队列 来保障消息至少被消费一次,以及未被正确处理的消息不会被丢弃。
- 另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
- 当然,延时队列还有很多其它选择。
- 比如:利用 Java 的 DelayQueue,利用 Redis 的 Zset,利用 Quartz 或者 利用 Kafka 的时间轮,这些方式各有特点,看需要适用的场景。
六、绑定(Binding
)
- Binding 是 Exchange 和 Queue 之间的桥梁,告诉我们 交换机 和 那个 队列 进行了绑定关系。
- 比如说下面这张图告诉我们 X交换机 与 Q1 和 Q2 进行了绑定。
- Binding 是绑定 交换机 和 队列 之间的桥梁关系,也可以理解 队列只对它绑定的 交换机 的消息感兴趣。
- 绑定用参数
routingKey
来表示,也可称该参数为bindingKey
;- 创建绑定我们用代码:
channel.queueBind(queueName, EXCHANGE_NAME, "routingKey")
绑定之后的意义由其交换类型决定。