这里是知识名字
这个知识重要吗?有什么用?
开始:
2022年5月28日 18:47:40
一、消息队列(MQ)
(1.1)什么是MQ?
message queue 本质是一个队列,遵循先进先出的原则,只不过队列存储内容为message。
还是一种跨进程的通信机制,用于上下游传递消息。
在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。
使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。
(1.2)为什么使用MQ?
(1.2.1)流量消峰
一个系统每秒最多处理一万个请求,当超过一万的请求时,系统就会宕机。
为了解决这个问题,在客户端和系统中添加一个中间件MQ,从客户端→系统变为客户端→MQ→系统,在MQ中进行排队,防止一秒内请求超过系统处理能力导致宕机。
每个请求要进行等待,但总比不能访问好。
(1.2.2)应用解耦
当订单系统发送消息给支付系统,此时若支付系统出现故障,会导致订单系统也随着出现故障。
使用MQ解决,订单系统发送消息给MQ,MQ去发送给支付系统,若支付系统出错,当订单系统发送的消息仍然在MQ,等待支付系统恢复正常。
(1.2.3)异步处理
有些服务间调用是异步的,例如A调用B,B需要较长时间去执行,但是A需要知道B的完成情况。
以前是由A定时调用B的查询api,或者A提供一个回调函数,当B完成时调用,但是不优雅。
可以由消息总线解决这个问题,当A调用B后,只用监听B处理完成的消息,当B完成后发送一条信息给MQ,MQ转发给A服务,A服务就得知B服务完成了。
(1.3)MQ的分类
-
ActiveMQ
- 优点:单机吞吐量万级,时效性 ms 级,可用性高,基于主从架构实现高可用性,消息可靠性较低的概率丢失数据(消息丢失概率低)
- 缺点:官方社区现在对 ActiveMQ 5.x 维护越来越少,高吞吐量场景较少使用。
-
Kafka
-
RocketMQ
-
RabbitMQ
二、RabbitMQ
(2.1)RabbitMQ是什么?
是一个消息中间件,它接收存储转发消息数据。
(2.2)四大核心概念
- 生产者:产生数据发送消息的程序是生产者。
- 交换机:一方面它接收生产者产生的消息,一方面将消息推送到队列中,交换机必须知道怎么处理消息,一个交换机可以绑定多个队列。
- 队列:本质上是一个消息缓冲区,生产者可以将消息发送到一个队列,多个消费者也可以从一个队列中接收数据。
- 消费者:等待接收消息的程序,同一个出现即可以是生产者也可以是消费者。
(2.3)六大核心(六大模式)
(2.4)工作原理
Broker
就是RabbitMQ实体,用于接收存储转发消息。Virtual host
用于在一个RabbitMQ实体中根据不同的用户,划分多个虚拟端口号,每个用户在自己的vhost创建交换机和队列等。Connection
生产者、消费者和Broker建立的TCP
连接。Channel
建立TCP连接需要极大耗费资源的,所以在Connection
中创建多个信道,用于传输消息,使用Channel id
来进行识别。Exchange
交换机,会根据分发规则,匹配查询表中的键分发消息到queue中。Queue
队列,消息被送到这里,等待消费者取走。Binding
交换机和队列的虚拟连接,用于分发消息。
(2.5)安装RabbitMQ
- rabbitMQ需要erlang环境。
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
yum 报错:Another app is currently holding the yum lock; waiting for it to exit…
是因为yum只能支持一个例程运行,如果有一个例程已经在运行,其他的必须等待该进程退出释放lock。出现这种情况时,可以用以下命令来恢复:
rm -f /var/run/yum.pid
(2.6)常用命令
如果使用systemctl 操作
systemctl options rabbitmq-server.service
- 添加开机启动 RabbitMQ 服务
- chkconfig rabbitmq-server on
- 启动服务
- /sbin/service rabbitmq-server start
- 查看服务状态
- /sbin/service rabbitmq-server status
- 停止服务(选择执行)
- /sbin/service rabbitmq-server stop
- 开启 web 管理插件
- rabbitmq-plugins enable - rabbitmq_management
开启web管理插件以后,访问服务器IP地址,就进入了rabbitMQ后台管理,注意关闭防火墙。
- 关闭应用的命令为
- rabbitmqctl stop_app
- 清除的命令为
- rabbitmqctl reset
- 重新启动命令为
- rabbitmqctl start_app
(2.7)用户管理
创建账号
rabbitmqctl add_user 账号 密码
设置用户角色
rabbitmqctl set_user_tags 账号 角色
设置用户权限
set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
-p <vhostpath>
表示虚拟vhost<conf>
是否能修改配置文件<write> <read>
可读可写".*"
代表有当前权限rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
三、HelloWord(简单队列模式)
使用Java代码实现发送单个消息的生产者并打印消息的消费者。
(3.1)创建Maven工程并导入依赖
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
(3.1)编写生产者代码
public class Producer {
public static final String QUEUE_NAME = "hello";
public static final String HOST = "192.168.19.200";
public static final String ADMIN = "admin";
public static final String PASSWORD = "123";
public static void main(String[] args) throws IOException, TimeoutException {
// 要发送的消息
String message = "hello word!";
// 创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置参数
factory.setHost(HOST);
factory.setUsername(ADMIN);
factory.setPassword(PASSWORD);
// 创建连接
Connection connection = factory.newConnection();
// 连接创建信道
Channel channel = connection.createChannel();
// 信道创建队列
// arg0 : 队列名称
// arg1 : 是否持久化 true: 持久化 false(默认): 存储内存中
// arg2 : 是否共享 true: 多个消费者可以使用 false: 只能一个消费者使用
// arg3 : 是否自动删除,最后一个消费者断开连接后,当前队列是否自动删除。
// arg4 : 其他进阶设置。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 发送消息
// arg0 : 发送的目标交换机,此时没有添加交换机
// arg1 : 路由的 key
// arg2 : 其他的参数信息
// arg3 : 发送的消息体
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("生产者生产了一条消息");
}
}
(3.2)编写消费者代码
public class Consumer {
// 队列名称要与生产者一致。
public static final String QUEUE_NAME = "hello";
public static final String HOST = "192.168.19.200";
public static final String USERNAME = "admin";
public static final String PASSWORD = "123";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST);
factory.setUsername(USERNAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 信道创建完成等待消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody()));
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消息消费被中断");
};
// arg0: 队列名
// arg1: 消费成功后是否自动应答
// arg2: 消费者收到消息后处理消息的回调
// arg3: 消费者取消消费的回调
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
四、Work Queues (工作队列)
工作队列主要思想是避免立即执行资源密集型任务。
多个工作线程轮询分发消息(轮流工作)
实例:由一个生产者发送大量的消息,创建两个工作线程,观察工作模式。
(4.1)编写生产者代码
public class Producer {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
String message = in.next();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("消息发送完成:" + message);
}
}
}
(4.2)编写消费者(工作线程)代码
public class Worker {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("接收到的消息: " + new String(message.getBody()));
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消息消费被中断了");
};
System.out.println("worker02 准备就绪");
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
表示允许开启多个线程,此时每启动一次主程序就创建了一个线程。
(4.3)消息应答
(4.3.1)消息应答的概念
RabbitMQ将消息发送给了消费者,就会将该消息标记为删除,那么如果消费者在处理完成之前宕机了,那么将将丢失该消息。
为了解决这个问题,rabbitMQ引入消息应答机制。
消息应答
:消费者在接收并处理完成该消息后会通知rabbitMQ处理完成,rabbitMQ可以将该消息进行删除。
(4.3.2)自动应答(少使用)
消息发送以后,就认为已经完成任务,如果在代码中发生问题,会导致消息丢失,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
(4.3.3)手动应答
Channel.basicAck()
用于肯定确认,RabbitMQ将得知当前消息成功处理,可以将其丢弃。Channel.basicNack()
用于否定确认,当前消息没有成功处理。Channel.basicReject()
用于否定确认,当前消息不再处理,直接丢弃。
(4.3.4)Multiple(不要使用)
手动应答可以使用批量应答来减少网络拥堵。
true
可以批量应答channel
上未应答的消息。false
不能批量应答。
当开启批量应答,如果在处理其中一个消息时发生了错误,那么它之前的消息也可能会丢失,所以通常不使用批量应答。
(4.3.5)消息自动重新入队
如果消费者因为某种原因导致失去连接,消息未发送ack
确认,RabbitMQ将了解到消息未完全处理,会将其重新加入队伍进行排队,如果有其他消费者可以处理,那么会分发给那个消费者,这样,即使某个消费者丢失连接,也可以确保不会丢失消息。
(4.3.6)实现手动应答
是在工作队列模式下的。
(4.4)RabbitMQ持久化
是为了解决RabbitMQ服务停止以后,消息生产者发送的消息不丢失,默认情况下,当RabbitMQ服务停止时,它会忽略队列和消息。
确保消息不会丢失需要将队列和消息都标记为持久化。
(4.4.1)队列的持久化
在使用信道创建队列时,传入true
声明当前队列为持久化队列。
如果之前队列不是持久化,需要删除,再进行创建。
此时RabbitMQ重启,当前durableTest
队列仍然存在。
(4.4.2)消息的持久化
将消息标记为持久化并不能完全保证不会丢失消息。
尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。
此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。
如果需要更强有力的持久化策略,需要在发布确认
模式中。
(4.5)不公平分发
RabbitMQ分发消息采用轮询分发,在消费者之间有性能差距时,这种方式不是最优的。
在消费者设置参数channel.basicQos(1);
轮询分发(默认):0
不公平分发:1
(4.5.1)预取值
指定分发消息的数量。
在消费者设置参数channel.basicQos(预取值);
五、confirm(发布确认)
是解决持久化的问题。
- 队列必须持久化
- 队列中的消息必须持久化
- 发布确认:在RabbitMQ将消息保存到磁盘上以后,通知生产者。
(5.1)发布确认原理
生产者将信道设置为confirm
模式,一旦进入这个模式,该信道上的信息会有唯一ID(从1开始),一旦消息被投递到所有匹配的队列之后,Broker(RabbitMQ实体)就会发送一个确认给生产者(包括消息的唯一ID),使得生产者得知消息已经正确的到达队列。
confirm
模式最大的好处在于他是异步的,一旦发布一条消息,生产者在等待返回确认时可以发送下一条消息,当消息得到确认以后,可以通过回调方法来处理该确认信息,如果rabbitMQ内部错误导致消息丢失,会发送一条nack
消息,生产者同样可以在回调函数中处理该消息。
(5.2)开启发布确认
在生产者的代码中开启。
(5.3)单个确认发布
发送一条,等待确认,才会发布下一条。
如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
发布速度慢。
public class 单步确认 {
public static int message_count = 1000;
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
channel.confirmSelect();
long begin = System.currentTimeMillis();
for (int i = 0; i < message_count; i++) {
String message = i + " ";
channel.basicPublish("", queueName, null, message.getBytes());
channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println("发布" + message_count + "个单独确认消息,耗时" + (end - begin) + "ms");
}
}
(5.4)批量确认发布
发布一批消息,然后一起确认,可以提高吞吐量。
当发生故障时,无法得知是哪个消息发生错误。
public class 批量确认 {
public static int message_count = 1000;
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
channel.confirmSelect();
int batchSize = 100;
long begin = System.currentTimeMillis();
for (int i = 1; i <= message_count; i++) {
String message = i + " ";
channel.basicPublish("", queueName, null, message.getBytes());
if (i % batchSize == 0) {
channel.waitForConfirms();
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + message_count + "个批量确认消息,耗时" + (end - begin) + "ms");
}
}
(5.5)异步确认发布
会在信道中将每一个消息都对应上一个消息序号,当收到一个消息时broker
会调用确认收到回调函数ackCallbak
,如果没有收到会调用未确认收到回调函数nackCallback
,所以生产者只用批量地发送消息,会由broker
来通知消息是否发送成功。
public class 异步确认 {
public static int message_count = 1000;
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
channel.confirmSelect();
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
// map.headMap(key, boolean) 返回map中键小于给定key的子哈希表,true 代表可以取边界
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(sequenceNumber, true);
confirmed.clear();
} else {
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
String message = outstandingConfirms.get(sequenceNumber);
System.out.println("发布的消息" + message + "未被确认,序列号" + sequenceNumber);
};
channel.addConfirmListener(ackCallback, nackCallback);
long begin = System.currentTimeMillis();
for (int i = 0; i < message_count; i++) {
String message = i + " ";
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
System.out.println(channel.getChannelNumber());
channel.basicPublish("", queueName, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布" + message_count + "个异步确认消息,耗时" + (end - begin) + "ms");
}
}
六、交换机(Exchanges)
当传入空字符串时,使用默认交换机。
(6.1)交换机的概念
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。
实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。
交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
(6.2)交换机的类型
直接(direct), 主题(topic) ,标题(headers) ,扇出(fanout),无名
(6.2.1)无名
第一个参数是交换机的名称。空字符串表示默认或无名称交换机,即使用默认交换机。
(6.3)临时队列
不具有持久化的队列就是临时队列。
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。
其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
(6.4)绑定
binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队
列进行了绑定关系。
Routing key 的目的就是一个交换机可以绑定多个队列,交换机根据它来区分队列。
七、发布订阅模式(fanout交换机)
它是将接收到的所有消息广播到它知道的
所有队列中。
(7.1)发布订阅模式实例
实例:生产者发送一个消息给交换机,交换机实现发布订阅模式,让两个消费者都接收到生产者的消息。
public class ReceiveLog01 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("ReceiveLog01 开始等待接收消息");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "UTF-8"));
};
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息被取消了");
};
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
public class ReceiveLog02 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("ReceiveLog02 开始等待接收消息");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "UTF-8"));
};
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息被取消了");
};
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
String message = in.nextLine();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("生产者生成消息:" + message);
}
}
}
八、路由模式(Routing)
路由模式和发布订阅模式差距仅仅是routingKey。
(8.1)路由模式实例
发送三条信息到两个消费者,两条消息发送给消费者1,是根据不同的routingKey,一条消息发送给消费者2.
public class EmitLog {
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);
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("info","普通 info 信息");
bindingKeyMap.put("warning","警告 warning 信息");
bindingKeyMap.put("error","错误 error 信息");
bindingKeyMap.put("debug","调试 debug 信息");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKey, null,
message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
public class ReceiveLog01 {
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 = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "info");
channel.queueBind(queueName, EXCHANGE_NAME, "warning");
System.out.println("ReceiveLog01 开始等待接收消息");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "UTF-8"));
};
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息被取消了");
};
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
public class ReceiveLog02 {
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 = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println("ReceiveLog02 开始等待接收消息");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody(), "UTF-8"));
};
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息被取消了");
};
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
九、主题模式(topics)
在路由模式中,只能根据routingKey选择一个队列,而不能同时路由。
例如:需要接收的类型有 A 和 B 两种消息,但是某个队列只想接收A类型的消息,此时direct就无法完成,只能使用topic
(9.1)主题模式的规则
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。
单词列表最多不能超过255个字节。
两个替换符
*
可以代替一个单词#
可以代替零个或多个单词
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
十、死信队列
(10.1)死信的概念
是无法被消费的消息,由于某种原因,导致队列中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有死信队列。
应用场景:保证订单业务的消息数据不丢失,需要使用到死信队列机制,当消息消费异常时,将消息投入到死信队列中。
死信可以作为延迟消息处理。
(10.2)死信的来源
- 消息TTL(存活时间 )过期
- 队列达到最大长度,无法再添加数据到
broker
中 - 消息被拒绝且requeue=false(不重新加入队列)
(10.3)死信队列的实现
一个生产者产生消息。
两个消费者,一个正常消费者,一个死信消费者。
两个队列,一个正常队列,一个死信队列。
生产者发送消息,正常消息正常消费者消费,死信消息死信消费者消费。
在正常消费者中绑定死信队列以及死信交换机由死信消费者进行消费。
- 生产者
public class Producer {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
// 消息TTL过期导致加入死信队列
// AMQP.BasicProperties properties = new
// AMQP.BasicProperties().builder().expiration("10000").build();
// channel.basicPublish(NORMAL_EXCHANGE, "zs", properties, message.getBytes());
for (int i = 1; i < 11; i++) {
String message = "info:" + i;
channel.basicPublish(NORMAL_EXCHANGE, "zs", null, message.getBytes());
System.out.println("生产者发送消息:" + message);
}
}
}
- 正常消费者
public class NormalConsumer {
// 普通交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
// 死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
// 普通队列
public static final String NORMAL_QUEUE = "normal_queue";
// 死信队列
public static final String DEAD_QUEUE = "dead_queue";
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);
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "ls");
// 正常队列绑定死信队列
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange
// x-dead-letter-routing-key
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
params.put("x-dead-letter-routing-key", "ls");
// 限制队列长度导致加入死信队列
// params.put("x-max-length", 5);
channel.queueDeclare(NORMAL_QUEUE, false, false, false, params);
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zs");
DeliverCallback deliverCallback = (consumerTag, message) -> {
String messageStr = new String(message.getBody(), "UTF-8");
// 丢弃消息放入死信队列,注意应答方式从自动变成了手动
if ("info:5".equals(messageStr) || "info:6".equals(messageStr)) {
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
System.out.println("消息" + messageStr + "被拒绝了,加入死信队列由死信消费者消费");
}
else {
System.out.println("消息" + messageStr + "被接收了,正常消费");
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
System.out.println("正常消费者准备消费");
channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, consumerTag -> {});
}
}
- 死信消费者
public class DeadConsumer {
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "ls");
System.out.println("死信消费者准备消费");
DeliverCallback deliverCallback = (consumerTag, message) ->{
System.out.println("死信消费者开始消费:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(DEAD_QUEUE, true, deliverCallback, consumerTag -> {});
}
}
十一、延迟队列
延迟队列:存放的是需要在指定时间内处理的元素。
(11.1)使用场景
- 订单未支付
- 预定事务,在时间点前通知
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务。
(11.2)rabbitMQ中的TTL
-
TTL
是消息、队列的属性,声明一条消息或该队列中的所有消息的最大存活时间。 -
单位为毫秒,当设置了TTL,在设置的时间内没有被消费,那么就会变成死信。
-
如果同时配置了队列TTL和消息的TTL,那么取较小值。
(11.3)队列和消息设置TTL
public class 队列 {
public static final String QUEUE_NAME = "name";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 100);
channel.queueDeclare(QUEUE_NAME, false, false, false, arguments);
}
}
public class 消息 {
public static final String QUEUE_NAME = "name";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.expiration("1000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish("", "", properties, "message".getBytes());
}
}
十二、SpringBoot整合RabbitMQ
创建springboot工程
(12.1)添加依赖
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
(12.2)修改配置文件
(12.3)实例一:实现简单延迟队列
使用SpringBoot整合rabbitmq实现:在浏览器发送一段消息,由两个交换机进行接收,一个交换机TTL为10s,一个交换机TTL为40s,消息过期以后消息由死信队列进行消费。
- 声明队列和消息
- 生产消息
- 消费消息
缺点:每增加一个时间需求就要新增加一个队列,耦合度太大。
@Configuration
public class TTLQueueConfig {
public static final String EXCHANGE_X = "exchange_X";
public static final String EXCHANGE_DEAD_Y = "exchange_Y";
public static final String QUEUE_A = "queue_A";
public static final String QUEUE_B = "queue_B";
public static final String QUEUE_D = "queue_D";
public static final String Routing_XA = "XA";
public static final String Routing_XB = "XB";
public static final String Routing_YD = "YD";
// 创建交换机 X 并加入容器
@Bean("exchangeX")
public DirectExchange getExchangeX() {
return new DirectExchange(EXCHANGE_X);
}
// 创建交换机 Y 并加入容器
@Bean("exchangeY")
public DirectExchange getExchangeY() {
return new DirectExchange(EXCHANGE_DEAD_Y);
}
// 创建队列QA, 并且设置过期时间为 10 s
@Bean("queueQA")
public Queue getQueueA() {
Map<String, Object> args = new HashMap<>(3);
args.put("x-dead-letter-exchange", EXCHANGE_DEAD_Y);
args.put("x-dead-letter-routing-key", Routing_XA);
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 创建队列QB, 并且设置过期时间为 40 s
@Bean("queueQB")
public Queue getQueueQB() {
Map<String, Object> args = new HashMap<>(3);
args.put("x-dead-letter-exchange", EXCHANGE_DEAD_Y);
args.put("x-dead-letter-routing-key", Routing_XB);
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
// 将队列 QA和交换机 X 进行绑定,路由键在 XA
@Bean
public Binding setBindingQAtoX(@Qualifier("queueQA") Queue queue,
@Qualifier("exchangeX") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(Routing_XA);
}
/**
* 将队列QB和交换机X进行绑定,路由键为 XB
*/
@Bean
public Binding setBindingQBtoX(@Qualifier("queueQA") Queue queue,
@Qualifier("exchangeX") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(Routing_XB);
}
@Bean("queueQD")
public Queue getQueueQD() {
return new Queue(QUEUE_D);
}
/**
*
* @param queue QD队列
* @param exchange Y交换机
* @return 将它们进行绑定
*/
@Bean
public Binding setBindingQDtoY(@Qualifier("queueQD") Queue queue,
@Qualifier("exchangeY") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(Routing_YD);
}
}
@RequestMapping("ttl")
@RestController
@Slf4j
public class Producer {
@Autowired
RabbitTemplate rabbitTemplate;
@RequestMapping("sendMsg/{message}")
public void sendMsg(@PathVariable("message") String message) {
log.info("生产者在{}发送了一条消息:{}", new Date(), message);
rabbitTemplate.convertAndSend("X", "XA", "发送了一条{TTL:10}的消息:" + message);
rabbitTemplate.convertAndSend("X", "XB", "发送了一条{TTL:40}的消息:" + message);
}
}
@Component
@Slf4j
public class DeadLetterConsumer {
@RabbitListener(queues = "QD")
public void receive(Message message) {
log.info("当前时间:{}, 死信消费者接收到消息:{}", new Date().toString(), new String(message.getBody()));
}
}
(12.4)实例二:通用的延迟队列(死信延迟的缺陷)
在消费者发送消息时告知TTL,来达到想延迟多久就延迟多久。
在发送消息时,配置当前消息的TTL
缺点:RabbitMQ只会检查第一个消息是否过期,如果过期就加入到死信队列,如果第一个消息延迟很长,第二个消息延迟很短,此时不能实现后发送延迟短的消息先消费。
因为队列是先进先出的,消息可能会不按时过期。
(12.5)RabbitMQ插件实现延迟队列
可以添加延迟消息
交换机
(12.5.1)下载插件并安装
https://www.rabbitmq.com/community-plugins.html
rabbitmq_delayed_message_exchange
解压放置到 RabbitMQ 的插件目录。
进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
@RequestMapping("sendDelayedMsg/{message}/{ttlTime}")
public void sendDelayedMsg(@PathVariable String message,
@PathVariable Integer ttlTime) {
log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}", new
Date(),ttlTime, message);
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE, DELAYED_ROUTINGKEY, message, msg ->{
msg.getMessageProperties().setDelay(ttlTime);
return msg;
});
}
@RabbitListener(queues = "delayed.queue")
public void receiveDelayedQueue(Message message) {
log.info("当前时间:{}, 延迟队列消费者接收到消息:{}", new Date().toString(), new String(message.getBody()));
}
@Configuration
public class 插件延迟队列配置文件 {
public static final String DELAYED_EXCHANGE = "delayed.exchange";
public static final String DELAYED_QUEUE = "delayed.queue";
public static final String DELAYED_ROUTINGKEY = "delayed.routingKey";
@Bean("delayed.exchange")
public CustomExchange setDelayedExchange() {
// CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments)
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
}
@Bean("delayed.queue")
public Queue setDelayedQueue() {
return new Queue(DELAYED_QUEUE);
}
@Bean
public Binding DQueueBindingDExchange(@Qualifier("delayed.queue") Queue queue,
@Qualifier("delayed.exchange") CustomExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTINGKEY).noargs();
}
}
十三、发布确认(高级)
应 用 [xxx] 在 [08-1516:36:04] 发 生 [ 错误日志异常 ] , alertId=[xxx] 。 由
[org.springframework.amqp.rabbit.listener.BlockingQueueConsumer:start:620] 触发。
应用 xxx 可能原因如下
服务名为:
异常为: org.springframework.amqp.rabbit.listener.BlockingQueueConsumer:start:620,
产 生 原 因 如 下 :1.org.springframework.amqp.rabbit.listener.QueuesNotAvailableException:
Cannot prepare queue for listener. Either the queue doesn't exist or the broker will not
allow us to use it.||Consumer received fatal=false exception on startup:
只要交换机或者队列不存在都会丢失消息。
即生产者发送消息,如果交换机或者队列接收不到消息,就将消息加入到缓冲中,定时重新发送。
所以要解决两个问题
- 交换机接收不到消息怎么处理
- 队列接收不到消息怎么处理
使用回调接口解决,生产者发送消息给交换机,交换机如果接收成功会返回消息给生产者
(13.1)当交换机无法接收消息(回调函数)
(13.1.1)配置文件
none
默认值,禁用发布确认模式CORRELATED
发布消息成功以后交换机会触发回调方法
(13.1.2)实现接口,编写初始化方法
当发送失败就会调用这个接口。
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
/**
* 自动注入 rabbitTemplate
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 将当前回调行数设置进注入的 rabbitTemplate
* 执行顺序:
* 先将 MyCallBack 加入容器中,
* 自动注入 rabbitTemplate
* 调用 init() 在 rabbitTemplate 中设置MyCallBack
*/
/**
* 被 @PostConstruct 修饰的方法会在服务器加载 Servlet 的时候运行
*/
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
}
/**
*
* @param correlationData 保存回调消息的信息
* @param ack true: 交换机收到消息 false: 交换机没有收到消息
* @param cause 失败原因,当ack = true 时为null
*/
@Override
public void confirm(CorrelationData correlationData,
boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId() : "";
if (ack) {
log.info("交换机收到ID为{}的消息", id);
}
else {
log.info("交换机未收到ID为{}的消息, 原因:{}", id, cause);
}
}
}
(13.1.3)在发送消息时设置参数
成功发送的提示
修改交换机名称,发送失败的提示
交换机未收到ID为1的消息, 原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange123' in vhost '/', class-id=60, method-id=40)
得到发送到交换机失败的通知就可以对消息进行保存,定时再次发送。
(13.2)当队列无法接收消息(回退消息)
当交换机无法给队列发送消息时,消息会被直接丢弃。
- routingKey 不存在
- 队列不存在
通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
(13.2.1)配置文件
代表消息可回退
(13.2.2)实现接口
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(this);
/**
* 当消息不可达时,将消息返回给生产者
* @param returned
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
log.info("消息:{} 被退回, 退回交换机为:{}, 路由键为{}, 退回原因:{}",
returned.getMessage(), returned.getExchange(),
returned.getRoutingKey(), returned.getReplyText());
}
(13.3)备份交换机
如果消息无法发送给交换机,那么将消息发送给备份交换机,由备份交换机发送。
在注入交换机时,声明备用交换机
@Bean("confirm.exchange")
public DirectExchange confirmExchange() {
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(EXCHANGE_CONFIRM_NAME)
.durable(true)
.withArgument("alternate-exchange", EXCHANGE_BACKUP_EXCHANGE);
return exchangeBuilder.build();
}
此时当主交换机发送失败时,会使用备份交换机再次发送
mandatory 参数与备份交换机可以一起使用的时候,备份交换机优先级高。
十四、幂等性问题
(14.1)概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
(14.2)消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
(14.3)解决思路
一般使用全局ID来标识消息,当消费消息时,通过ID判断是否消费过。
- 唯一ID + 指纹码
- redis 原子性(最好的)
十五、RabbitMQ 集群
(15.1)配置环境
- 修改主机名词
- 修改IP
- 配置
/etc/hosts
文件 - 在 node1 查看 node2 && node3 是否使用同一 cookie
在 node1 上执行远程操作命令
-
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
-
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
- 启动RabbitMQ,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务
-
在节点二执行
-
rabbitmqctl stop_app
- (rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
-
rabbitmqctl reset
-
rabbitmqctl join_cluster rabbit@node1
-
rabbitmqctl start_app(只启动应用服务)
-
-
在节点三执行
- rabbitmqctl stop_app
- rabbitmqctl reset
- rabbitmqctl join_cluster rabbit@node2
- rabbitmqctl start_app
- 创建用户
- 创建账号
rabbitmqctl add_user admin 123
- 设置用户角色
rabbitmqctl set_user_tags admin administrator
- 设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
- 解除集群节点(在从节点执行)
- rabbitmqctl stop_app
- rabbitmqctl reset
- rabbitmqctl start_app
- rabbitmqctl cluster_status
- rabbitmqctl forget_cluster_node - rabbit@node2(node1 机器上执行)
指导复习的问题
犯的错误
-
Failed to start bean ‘documentationPluginsBootstrapper‘; nested exception is java.lang.NullPointerEx
:Springboot 版本太高导致 -
在使用springboot整合时
@RabbitListener(queues = "QD")
是指监听当前队列,如果当前队列收消息就消费。 -
加入容器的
controller
文件名称不能一样 -
注意关闭防火墙