什么是消息中间件
消息:
消息队列中间件:
传递模式:
点对点:基于队列
发布/订阅:基于内容节点
消息中间件的作用
解耦
冗余
扩展性
削峰
可恢复性
顺序保证
缓冲
异步通信
RabbitMQ的起源
RabbitMQ的安装及简单使用
安装Erlang
sudo yum -y install erlang
安装RabbitMQ
运行RabbitMQ
rabbitmq-server -detached
rabbitmqctl status
rabbitmqctl cluster_status
生产和消费消息
com.rabbitmq.amqp-client
默认用户密码(只能通过本地网络访问):guest/guest
添加用户
[root@Lime-CentOS ~]# rabbitmqctl add_user root root
Creating user "root" ...
...done.
设置权限
[root@Lime-CentOS ~]# rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
Setting permissions for user "root" in vhost "/" ...
...done.
设置管理员角色
[root@Lime-CentOS ~]# rabbitmqctl set_user_tags root administrator
Setting tags for user "root" to [administrator] ...
...done.
生产者客户端代码
package club.limeyu.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Author: L
* @Description:
* @Date: create in 2020-12-24 21:17
*/
public class RabbitProducer {
private static final String EXCHANGE_NAME = "exchange_demo";
private static final String ROUTING_KEY = "routingkey_demo";
private static final String QUEUE_NAME = "queue_demo";
private static final String IP_ADDRESS = "127.0.0.1";
private static final int PORT = 5672; // RabbitMQ 服务端默认端口号为5672
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(IP_ADDRESS);
connectionFactory.setPort(PORT);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();// 创建连接
Channel channel = connection.createChannel(); // 创建信道
// 创建一个type="direct"、持久化的、非自动删除的交换器
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
// 创建一个持久化、非排他的、非自动删除的队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 将交换器与队列通过路由键绑定
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 发送一条持久化的消息
String message = "Hello World";
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
channel.close();
connection.close();
System.out.println("Producer Over");
}
}
消费者客户端代码
package club.limeyu.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author: L
* @Description:
* @Date: create in 2020-12-24 21:33
*/
public class RabbitConsumer {
private static final String QUEUE_NAME = "queue_demo";
public static final String IP_ADDRESS = "127.0.0.1";
private static final int PORT = 5672;
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Address[] addresses = {
new Address(IP_ADDRESS, PORT)
};
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
// 这里的连接方式与生产者的demo略有不同,注意辨别区别
Connection connection = connectionFactory.newConnection(addresses); // 创建连接
Channel channel = connection.createChannel(); // 创建信道
channel.basicQos(64); // 设置客户端最多接收未被ack的消息个数
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("recv message : " + new String(body));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(QUEUE_NAME, defaultConsumer);
// 等待回调函数执行完毕之后,关闭资源
TimeUnit.SECONDS.sleep(5);
channel.close();
connection.close();
System.out.println("Consumer Over");
}
}
啦啦啦
RabbitMQ入门
相关概念介绍
RabbitMQ架构模型生产者和消费者
队列
Queue:队列,是RabbitMQ的内部对象,用于存储消息。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
RabbitMQ不支持队列层面的广播消费。
交换器、路由键、绑定
交换器类型
直连交换机(direct)
扇形交换机(fanout)
不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到扇形交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。
任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
可以理解为路由表的模式
这种模式不需要RouteKey
这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
DEMO
package club.limeyu.rabbitmq.tt.fanout;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 上午11:44
*/
@Slf4j
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT, true);
channel.queueDeclare("queue_fanout_1", true, false, false, null);
channel.queueDeclare("queue_fanout_2", true, false, false, null);
channel.queueBind("queue_fanout_1", "exchange_fanout", "rk_fanout_1");
channel.queueBind("queue_fanout_2", "exchange_fanout", "rk_fanout_2");
log.info("扇形交换器不需要RouteKey");
channel.basicPublish("exchange_fanout", "", MessageProperties.TEXT_PLAIN, "this message is sent to exchange_fanout".getBytes());
channel.close();
connection.close();
}
}
package club.limeyu.rabbitmq.tt.fanout;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 上午11:53
*/
@Slf4j
public class RabbitConsumer1 {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("Msg : {}", new String(body));
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("queue_fanout_1", false, defaultConsumer);
TimeUnit.SECONDS.sleep(3);
channel.close();
connection.close();
}
}
package club.limeyu.rabbitmq.tt.fanout;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 上午11:53
*/
@Slf4j
public class RabbitConsumer2 {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("Msg : {}", new String(body));
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("queue_fanout_2", false, defaultConsumer);
TimeUnit.SECONDS.sleep(3);
channel.close();
connection.close();
}
}
主题交换机(topic)
头交换机(headers)
路由键
绑定键
RabbitMQ运转流程
AMQP协议介绍
Module Layer : 位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。例如,客户端可以使用Queue.Declare命令声明一个队列或使用Basic.Consume订阅消费一个队列中的消息。
Session Layer : 位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。
Transport Layer : 位于最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。
AMQP生产者流转过程
AMQP消费者流转过程
AMQP命令概览
客户端开发向导
连接RabbitMQ
ShutdownSignalException
使用交换器和队列
exchangeDeclare方法详解
Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map
autoDelete:设置是否自动删除。autoDelete设置为true则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:“当与此交换器连接的客户端都断开时,RabbitMQ会自动删除本交换器”。
internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
argument:其他一些结构化参数。
void exchangeDeclareNoWait(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map
Exchange.DeclareOk exchangeDeclarePassive(String name) throws IOException;
queueDeclare方法详解
queueBind方法详解
exchangeBind方法详解
何时创建
发送消息
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body) throws IOException;
props:消息的基本属性集(14个属性成员)。
contentType
contentEncoding
headers(Map
deliveryMode
priority
correlationId:用来关联请求(request)和其调用RPC之后的回复(response)
replyTo:通常用来设置一个回调队列
expiration
messageId
timestamp
type
userId
appId
clusterId
mandatory
immediate
消费信息
消费模式
推模式(push):Basic.Consume
拉模式(pull):Basic.Get
消费端的确认与拒绝
消息确认机制(message acknowledgement)
等待投递给消费者的消息;
已经投递给消费者,但是还没有收到消费者确认信号的消息。
如果RabbitMQ一直没有收到消费者的确认信息,并且消费此信息的消费者已经断开链接,则RabbitMQ会安排该消息重新进入队列。
rabbitmqctl list_queues name messages_ready messages_unacknowledged
void basicReject(long deliveryTag, boolean requeue) throws IOException
deliveryTag : 可以看作消息的编号,一个64位的长整型值。
requeue:设置RabbitMQ是否需要重新将这条消息存入队列。
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException
multiple:false则表示拒绝编号为deliveryTag的这一条消息。true则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息。
Basic.Recover
Basic.RecoverOk basicRecover() throws IOException
Basic.RecoverOk basicRecover(boolean requeue) throws IOException
是否恢复消息到队列,参数是是否requeue,true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费,而不是自己再次消费。false则消息会重新被投递给自己。
关闭连接
public void addShutdownListener(ShutdownListener listener);
public void removeShutdownListener(ShutdownListener listener);
ShutdownSignalException
public boolean isHardError()
RabbitMQ进阶
消息何去何从
mandatory参数
当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设置为false时,出现上述情形,则消息直接被丢弃。
那么生产者如何获取到没有被正确路由到合适队列的消息呢?这时候可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。
Demo
package club.limeyu.rabbitmq.tt.mandatory;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/22 下午2:39
*/
@Slf4j
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setPassword("root");
connectionFactory.setUsername("root");
connectionFactory.setPort(5672);
connectionFactory.setHost("www.limeyu.club");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 添加 ReturnListener 监听器, 如果消息发送时,mandatory = true,且没有被正确路由,RabbitMQ Server 调用 Basic.Return 返回此消息给发送者。此监听器负责接收 RabbitMQ Server 返回的消息并处理。
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("replyCode : {}, replyText : {}, exchange : {}, routingKey : {}, BasicProperties : {}, body : {}", replyCode, replyText, exchange, routingKey, properties, new String(body));
}
});
channel.exchangeDeclare("exchange_mandatory", BuiltinExchangeType.DIRECT, true, false, null);
channel.queueDeclare("queue_mandatory", true, false, false, null);
channel.queueBind("queue_mandatory", "exchange_mandatory", "rk_mandatory");
String mandatory_state = new String[]{"rk_mandatory_correct", "rk_mandatory_incorrect_has_rk", "rk_mandatory_incorrect_no_rk"}[2];
switch (mandatory_state) {
case "rk_mandatory_correct":
channel.basicPublish("exchange_mandatory", "rk_mandatory", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "this message mandatory is true, if cannot correct routing, will return back sender.".getBytes());
break;
case "rk_mandatory_incorrect_has_rk":
channel.basicPublish("exchange_mandatory", "rk_mandatory_incorrect_has_rk", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "this message mandatory is true, if cannot correct routing, will return back sender.".getBytes());
break;
default:
// 不指定路由键
channel.basicPublish("exchange_mandatory", "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "this message mandatory is true, if cannot correct routing, will return back sender.".getBytes());
}
TimeUnit.SECONDS.sleep(1);
channel.close();
connection.close();
}
}
immediate参数
当immediate参数设为true时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回至生产者。
备份交换器(Alternate Exchange简称AE)
生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失;如果设置了mandatory参数,那么需要添加ReturnListener的编辑逻辑,生产者的代码将变得复杂。如果既不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,可以将未被路由的消息存储在RabbitMQ中,再在需要的时候去处理这些消息。
可以通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加alternate-exchange参数来实现,也可以通过策略的方式实现。
package club.limeyu.rabbitmq.tt.ae;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/22 下午1:30
*/
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_ae", BuiltinExchangeType.DIRECT, true, false, null);
channel.queueDeclare("queue_ae_no_rk", true, false, false, null);
// 没有绑定路由键
channel.queueBind("queue_ae_no_rk", "exchange_ae", "");
channel.queueDeclare("queue_ae_rk_1", true, false, false, null);
// 绑定路由键 rk_queue_ae_rk_1
channel.queueBind("queue_ae_rk_1", "exchange_ae", "rk_queue_ae_rk_1");
channel.queueDeclare("queue_ae_rk_2", true, false, false, null);
// 绑定路由键 rk_queue_ae_rk_2
channel.queueBind("queue_ae_rk_2", "exchange_ae", "rk_queue_ae_rk_2");
Map arguments_exchange_normal = new HashMap<>();
arguments_exchange_normal.put("alternate-exchange", "exchange_ae");
// 通过 alternate-exchange = exchange_ae(备份交换器名称)为 exchange_normal 交换器 绑定 exchange_ae 备份交换器
channel.exchangeDeclare("exchange_normal", BuiltinExchangeType.DIRECT, true, false, arguments_exchange_normal);
channel.queueDeclare("queue_normal", true, false, false, null);
channel.queueBind("queue_normal", "exchange_normal", "rk_normal");
String rk_state = new String[]{"normal", "no_rk", "rk_1", "rk_2", "have_a_nice_day"}[4];
switch (rk_state) {
case "normal":
// 正常情况。发送消息到 exchange_normal 交换器 通过 rk_normal 绑定的 queue_normal 消息队列中。
channel.basicPublish("exchange_normal", "rk_normal", MessageProperties.PERSISTENT_TEXT_PLAIN, "this message on rk_normal".getBytes());
break;
case "no_rk":
// 没有指定路由键。发送消息到 exchange_normal 交换器 通过 alternate-exchange = exchange_ae 绑定的 exchange_ae 备份交换器上。
// 由于没有指定路由键,消息被 exchange_ae 交换器 路由到 没有绑定路由键的 queue_ae_no_rk 消息队列中。
channel.basicPublish("exchange_normal", "", MessageProperties.PERSISTENT_TEXT_PLAIN, "this message no rk".getBytes());
break;
case "rk_1":
// 指定一个正常交换器不存在,备份交换器存在的路由键。
channel.basicPublish("exchange_normal", "rk_queue_ae_rk_1", MessageProperties.PERSISTENT_TEXT_PLAIN, "this message on rk_queue_ae_rk_1".getBytes());
break;
case "rk_2":
// 指定一个正常交换器不存在,备份交换器存在的路由键。
channel.basicPublish("exchange_normal", "rk_queue_ae_rk_2", MessageProperties.PERSISTENT_TEXT_PLAIN, "this message on rk_queue_ae_rk_2".getBytes());
break;
default:
// 指定一个正常交换器不存在,备份交换器也不存在的路由键。
channel.basicPublish("exchange_normal", "have_a_nice_day", MessageProperties.PERSISTENT_TEXT_PLAIN, "this message on have_a_nice_day".getBytes());
break;
}
TimeUnit.SECONDS.sleep(1);
channel.close();
connection.close();
}
}
啦啦啦
过期时间(TTL)
设置消息的TTL
通过队列声明设置
x-message-ttl
毫秒
如果不设置TTL,则表示此消息不会过期;如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃。
Map arguments = new HashMap<>();
arguments.put("x-message-ttl", 6000);
channel.queueDeclare("messageTtlQueue", true, false, false, arguments);
通过消息单独指定
expiration
毫秒
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(2); // 持久化消息
builder.expiration("6000"); // 设置TTL=6000ms
AMQP.BasicProperties basicProperties = builder.build();
channel.basicPublish("messageTtlExchange", "messageTtlRoutingKey", basicProperties, "messageTtlBody".getBytes());
设置队列的TTL
x-expires
毫秒
通过channel.queueDeclare方法中的x-expires参数可以控制队列被自动删除前处于未使用状态的时间。未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未被调用过Basic.Get命令。
RabbitMQ会确保在过期时间到达后将队列删除,但是不保障删除的动作有多及时。在RabbitMQ重启后,持久化的队列的过期时间会被重新计算。
Map arguments = new HashMap<>();
arguments.put("x-expires", 1000 * 10); // 创建一个过期时间10秒的队列(rabbitmqctl list_queues)
channel.queueDeclare("queueQueueTtl", false, false, false, arguments);
死信队列(Dead-Letter-Exchange)
当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列。
消息变成死信一般是由于以下几种情况:
消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;
消息过期;
队列达到最大长度;
通过在channel.queueDeclare方法中设置x-dead-letter-exchange参数来为这个队列添加DLX
// 生产者
package club.limeyu.rabbitmq.tt.dlx.reject;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/19 下午5:25
*/
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setPassword("root");
connectionFactory.setUsername("root");
connectionFactory.setPort(5672);
connectionFactory.setHost("www.limeyu.club");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 声明死信交换器
channel.exchangeDeclare("exchange_dlx", BuiltinExchangeType.DIRECT, true);
// 声明死信队列
channel.queueDeclare("queue_dlx", true, false, false, null);
// 通过路由键routingKey_dlx绑定交换器与队列
channel.queueBind("queue_dlx", "exchange_dlx", "routingKey_dlx");
// 声明正常交换器
channel.exchangeDeclare("exchange_normal", BuiltinExchangeType.FANOUT, true);
// 配置队列属性
Map arguments_normal = new HashMap<>();
// 设置死信后发送的交换器
arguments_normal.put("x-dead-letter-exchange", "exchange_dlx");
// 设置死信后发送的路由键。如果不指定,则使用原队列的路由键
arguments_normal.put("x-dead-letter-routing-key", "routingKey_dlx");
// 声明正常队列。设置队列属性
channel.queueDeclare("queue_normal", true, false, false, arguments_normal);
// 通过路由键routingKey_normal绑定交换器与队列
channel.queueBind("queue_normal", "exchange_normal", "routingKey_normal");
// 发送消息到MQ
channel.basicPublish("exchange_normal", "routingKey_normal", MessageProperties.TEXT_PLAIN, "就把今夜留给今夜吧".getBytes());
channel.close();
connection.close();
}
}
// 消费者
package club.limeyu.rabbitmq.tt.dlx.reject;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.GetResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/19 下午5:31
*/
public class RabbitConsumer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 从queue_normal队列中拉去消息
GetResponse queue_normal = channel.basicGet("queue_normal", false);
if (null != queue_normal) {
// 拒绝消息。触发MQ将消息转发到死信交换器中
channel.basicReject(queue_normal.getEnvelope().getDeliveryTag(), false);
}
TimeUnit.SECONDS.sleep(1);
channel.close();
connection.close();
}
}
延迟队列
ttl + dlx
优先级队列
队列级别设置优先级
x-max-priority
消息级别设置优先级
priority()
RPC实现
replyTo:通常用来设置一个回调队列;
correlationId:用来关联请求(request)和其调用RPC之后的回复(response);
一般在RabbitMQ中进行RPC时很简单的。客户端发送请求消息,服务端回复响应消息。为了接收响应的消息,我们需要在请求消息中发送一个回调队列(replyTo)。可以使用默认的队列。
String callbackQueueName = channel.queueDeclare() .getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties().builder().replyTo(callbackQueueName).build();
channel.basicPublish("","rpc_queue",props,message.getBytes ());
// then code to read a response message from the callback_queue...
如果像上面的代码中一样,为每个RPC请求创建一个回调队列,则是非常低效的。但是幸运的是这里有一个通用的解决方案——可以为每个客户端创建一个单一的回调队列。
这样就产生了一个新的问题,对于回调队列而言,在其接收到一条回复的消息之后,它并不知道这条消息应该和哪一个请求匹配。这里就用到correlationId这个属性了,我们应该为每一个请求设置一个唯一的correlationId。之后在回调队列接收到回复的消息时,可以根据这个属性匹配到相应的请求。如果回调队列接收到一条未知correlationId的回复消息,可以简单地将其丢弃。
你有可能会问,为什么要将回调队列中的未知信息丢弃而不是仅仅将其看作失败?这样可以针对这个失败做一些弥补措施。参考图4-7,考虑这样一种情况,RPC服务器可能在发送给回调队列(amq.gen-LhQzlgv3GhDOv8PIDabOXA)并且在确认接收到请求的消息(rpc_queue中的消息)之后挂掉了,那么只需重启下RPC服务器即可,RPC服务会重新消费rpc_queue队列中的请求,这样就不会出现RPC服务端未处理请求的情况。这里的回调队列可能会收到重复消息的情况,这需要客户端能够优雅地处理这种情况,并且RPC请求也需要保证其本身是幂等的。
根据图4-7所示,RPC的处理流程如下:
(1)当客户端启动时,创建一个匿名的回调队列(名称由RabbitMQ自动创建,如图4-7中的回调队列为amq.gen-LhQzlgv3GhDOv8PIDabOXA)。
(2)客户端为RPC请求设置2个属性:replyTo用来告知RPC服务端回复请求时的目的队列,即回调队列;correlationId用来标记一个请求。
(3)请求被发送到rpc_queue队列中。
(4)RPC服务端监听rpc_queue队列中的请求,当请求到来时,服务端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
(5)客户端监听回调队列,当有消息时,检查correlationId属性,如果与请求匹配,那就是结果了。
啦啦啦
package club.limeyu.rabbitmq.tt.rpc;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Authorliangmy
* @Description:
* @Create:2021/2/19下午6:16
*/
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
public static void main(String args[]) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
System.out.println("[x] A waiting RPC requests");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(properties.getCorrelationId())
.build();
String response = "";
try {
String message = new String(body, "UTF-8");
int n = Integer.parseInt(message);
System.out.println("[.]fib(" + message + ")");
response += fib(n);
} catch (Exception e) {
System.out.println("[.]" + e.toString());
} finally {
channel.basicPublish("", properties.getReplyTo(),
replyProps, response.getBytes("UTF-8"));
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
}
private static int fib(int n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
}
package club.limeyu.rabbitmq.tt.rpc;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/19 下午6:27
*/
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
private QueueingConsumer consumer;
private ConnectionFactory connectionFactory = new ConnectionFactory();
{
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
try {
connection = connectionFactory.newConnection();
channel = connection.createChannel();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
public RPCClient() throws IOException, TimeoutException {
replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
}
public String call(String message) throws
IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException {
String response = null;
String corrId = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", requestQueueName, props, message.getBytes());
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response = new String(delivery.getBody());
break;
}
}
return response;
}
public void close() throws Exception {
connection.close();
}
public static void main(String args[]) throws Exception {
RPCClient fibRpc = new RPCClient();
System.out.println("[x] Requesting fib (30)");
String response = fibRpc.call("2");
System.out.println("[.] Got '" + response + "'");
fibRpc.close();
}
}
持久化
交换器持久化
队列持久化
消息持久化
生产者确认(通过事务机制实现、通过发送方确认(publisher confirm)机制实现)
事务机制(降低消息吞吐量)
channel.txSelect:将当前的信道设置成事务模式
channel.txCommit:提交事务
channel.txRolback:事务回滚
package club.limeyu.rabbitmq.tt.tx;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 上午10:57
*/
@Slf4j
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_tx", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("queue_tx", true, false, false, null);
channel.queueBind("queue_tx", "exchange_tx", "routingKey_tx");
log.info("channel.txSelect() :: 设置信道为事务模式");
channel.txSelect();
try {
channel.basicPublish("exchange_tx", "routingKey_tx", MessageProperties.TEXT_PLAIN, "this message on tx mode".getBytes());
if (true) {
throw new RuntimeException("模拟发送消息发生异常");
}
channel.txCommit();
log.info("channel.txCommit() :: 消息发送成功,提交事务");
} catch (Exception e) {
channel.txRollback();
log.info("channel.txRollback() :: 消息发送异常,回滚事务");
}
channel.close();
connection.close();
}
}
发送方确认机制
生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达目的地了。如果消息和队列是可持久化掉的,那么确认消息会在消息写入磁盘之后发出。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示到这个序号之前的所有消息都已经得到了处理。
channel.confirmSelect
channel.waitForConfirms
channel.waitForConfirmsOrDie
批量confirm方法:每发送一批消息后,调用channel.waitForConfirms方法,等待服务器的确认返回。
定期或定量,亦或者两者结合起来调用channel.waitForConfirms来等待RabbitMQ的确认返回。
出现返回Basic.Nack或者超时情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量confirm的性能应该是不升反降的。
DEMO
package club.limeyu.rabbitmq.tt.confirm;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 下午1:33
*/
@Slf4j
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_confirm", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("queue_confirm", true, false, false, null);
channel.queueBind("queue_confirm", "exchange_confirm", "rk_confirm");
// 设置channel为发送方确认模式
channel.confirmSelect();
int msgCount = 0;
final int BATCH_COUNT = 100;
try {
while (true) {
channel.basicPublish("exchange_confirm", "rk_confirm", MessageProperties.TEXT_PLAIN, "this message on confirm mode".getBytes());
log.info("将发送出去的消息存入缓存中,缓存可以是一个ArrayList或者BlockingQueue之类的");
if (msgCount >= BATCH_COUNT) {
msgCount = 0;
try {
if (!channel.waitForConfirms()) {
log.info("将缓存中的消息清空");
}
log.info("将缓存中的消息重新发送");
} catch (InterruptedException e) {
log.info("将缓存中的消息重新发送");
}
}
}
} finally {
channel.close();
connection.close();
}
}
}
异步confirm方法:提供一个回调方法,服务端确认了一条或多条消息后客户端会回调这个方法进行处理。
在客户端Channel接口中提供的addConfirmListener方法可以添加ConfirmListener这个回调接口,这个ConfirmListener接口包含两个方法:handleAck和handleNack,分别用来处理RabbitMQ回传的Basic.Ack和Basic.Nack。在这两个方法中都包含有一个参数deliveryTag(在publisher confirm模式下用来标记消息的唯一有序序号)。我们需要为每一个信道维护一个“unconfirm”的消息序号集合,每发送一条消息,集合中的元素加1。每当调用ConfirmListener中的handleAck方法时,“unconfirm”集合中删掉相应的一条(multiple设置为false)或者多条(multiple设置为true)记录。从程序运行效率上来看,这个“unconfirm”集合最好采用有序集合SortedSet的存储结构。事实上,Java客户端SDK中的waitForConfirms方法也是通过SortedSet维护消息序号的。
DEMO
package club.limeyu.rabbitmq.tt.confirm.sync;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 下午2:42
*/
@Slf4j
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_confirm_sync", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("queue_confirm_sync", true, false, false, null);
channel.queueBind("queue_confirm_sync", "exchange_confirm_sync", "rk_confirm_sync");
TreeSet confirmSet = new TreeSet<>();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("Ack, SeqNO : {}, multiple : {}", deliveryTag, multiple);
if (multiple) {
confirmSet.headSet(deliveryTag, true).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.info("dddddNack, SeqNO : {}, multiple : {}", deliveryTag, multiple);
if (multiple) {
confirmSet.headSet(deliveryTag).clear();
} else {
confirmSet.remove(deliveryTag);
}
log.info("注意这里需要添加处理消息重发的场景");
}
});
channel.confirmSelect();
for (int i = 0; i < 100; i++) {
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("exchange_confirm_sync", "rk_confirm_sync", MessageProperties.PERSISTENT_TEXT_PLAIN, ("this message constom id is : " + i).getBytes());
confirmSet.add(nextPublishSeqNo);
}
TimeUnit.SECONDS.sleep(5);
for (int i = 0; i < 100; i++) {
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("exchange_confirm_sync", "rk_confirm_sync", MessageProperties.PERSISTENT_TEXT_PLAIN, ("this message constom id is : " + i).getBytes());
confirmSet.add(nextPublishSeqNo);
}
TimeUnit.SECONDS.sleep(5);
channel.close();
connection.close();
}
}
奇怪:队列自动删除之后,依旧回复的是Ack。如何模拟Nack场景???
消费端要点介绍
channel.basicAck
channel.basicNack
channel.basicReject
消息分发;
消息顺序性;
弃用QueueingConsumer
消息分发
Channel.void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException
prefetchSize:
prefetchCount:设置客户端最多接收未被ack的消息个数。设置为0则表示没有上限
global:消费者所能接收未确认消息的总体大小的上限,单位为B,设置为0则表示没有上限。
对于一个信道来说,它可以同时消费多个队列,当设置了prefetchCount大于0时,这个信道需要和各个队列协调以确保发送的消息都没有超过所限定的prefetchCount的值,这样会使RabbitMQ的性能降低,尤其是这些队列分散在集群中的多个Broker节点之中。
RabbitMQ为了提升相关性能,在AMQP 0-9-1协议之上重新定义了global这个参数
global参数 |
AMQP 0-9-1 | RabbitMQ |
---|---|---|
false |
信道上所有的消费者都需要遵从prefetchCount的限定值 | 信道上新的消费者需要遵从prefetchCount的限定值 |
true |
当前通信链路(Connection)上所有的消费者都需要遵从prefetchCount的限定值 | 信道上所有的消费者都需要遵从prefetchCount的限定值 |
RabbitMQ会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,如果达到了所设置的上限,那么RabbitMQ就不会向这个消费者再发送任何消息。直到消费者确认了某条消息之后,RabbitMQ将相应的计数减1,之后消费者可以继续接收消息,直到再次到达计数上限。
DEMO(prefetchCount)
package club.limeyu.rabbitmq.tt.qos;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 下午5:35
*/
public class RabbitProducer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_qos", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("queue_qos", true, false, false, null);
channel.queueBind("queue_qos", "exchange_qos", "rk_qos");
// 发送100条消息到队列
for (int i = 0; i < 100; i++) {
channel.basicPublish("exchange_qos", "rk_qos", MessageProperties.PERSISTENT_TEXT_PLAIN, ("this message no : " + i).getBytes());
}
TimeUnit.SECONDS.sleep(1);
channel.close();
connection.close();
}
}
package club.limeyu.rabbitmq.tt.qos;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.TreeSet;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 下午6:21
*/
@Slf4j
public class RabbitConsumer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 设置客户端最多接收未被ack的消息个数
channel.basicQos(5);
Queue ackQueue = new PriorityQueue<>();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("msg rev : {}", new String(body));
// 保存已接收消息的唯一标识
ackQueue.add(envelope.getDeliveryTag());
}
};
channel.basicConsume("queue_qos", false, defaultConsumer);
TimeUnit.SECONDS.sleep(2);
while (!ackQueue.isEmpty()) {
// 消费端确认消息已处理
channel.basicAck(ackQueue.remove(), false);
TimeUnit.SECONDS.sleep(1);
}
TimeUnit.SECONDS.sleep(2);
channel.close();
connection.close();
}
}
如果在订阅消息之前,即设置了global为true的限制,又设置了global为false的限制,那么那个会生效呢?RabbitMQ会确保两者都会生效。举例说明,当前有两个队列queue1和queue2:queue1有10条消息,分别为1到10;queue2也有10条消息,分别是11到20。有两个消费者分别消费这两个队列。那么这里每个消费者最多只能收到3个未确认的消息,两个消费者能收到的未确认的消息个数之和的上限为5。在未确认消息的情况下,如果consumer1收到了消息1、2和3,那么consumer2至多只能收到11和12.
DEMO(global)
package club.limeyu.rabbitmq.tt.qos.global;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 下午7:23
*/
public class RabbitProducer {
public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_qos_global", BuiltinExchangeType.FANOUT, true);
channel.queueDeclare("queue_qos_global_1", true, false, false, null);
channel.queueDeclare("queue_qos_global_2", true, false, false, null);
channel.queueBind("queue_qos_global_1", "exchange_qos_global", "rk_qos_global_1");
channel.queueBind("queue_qos_global_2", "exchange_qos_global", "rk_qos_global_2");
// 发送10条消息到扇形交换器
for (int i = 0; i < 10; i++) {
channel.basicPublish("exchange_qos_global", "", MessageProperties.PERSISTENT_TEXT_PLAIN, ("this message no : " + i).getBytes());
}
TimeUnit.SECONDS.sleep(1);
channel.close();
connection.close();
}
}
package club.limeyu.rabbitmq.tt.qos.global;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/20 下午7:27
*/
@Slf4j
public class RabbitConsumer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("www.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 每个消费者最多只能收到3个未确认的消息
channel.basicQos(3, false);
// 两个消费者能收到的未确认的消息个数之和的上限为5
channel.basicQos(5, true);
Queue ackQueue = new PriorityQueue<>();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("msg rev : {}, consumerTag : {}", new String(body), consumerTag);
ackQueue.add(envelope.getDeliveryTag());
}
};
channel.basicConsume("queue_qos_global_1", false, defaultConsumer);
channel.basicConsume("queue_qos_global_2", false, defaultConsumer);
TimeUnit.SECONDS.sleep(2);
while (!ackQueue.isEmpty()) {
// 消费端确认消息已处理
channel.basicAck(ackQueue.remove(), false);
TimeUnit.SECONDS.sleep(1);
}
TimeUnit.SECONDS.sleep(2);
channel.close();
connection.close();
}
}
消息顺序性
消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。
弃用QueueingConsumer
默认使用LinkedBlockingQueue缓存消息,造成内存溢出。
QueueingConsumer会拖累一个Connection下的所有信道,使其性能降低
同步递归调用QueueingConsumer会产生死锁
RabbitMQ的自动连接恢复机制(automatic connection recovery)不支持QueueingConsumer的这种形式
QueueingConsumer不是事件驱动的。
消息传输保障
At most once :最多一次。消息可能会丢失,但绝对不会重复传输。
At least once:最少一次。消息绝不会丢失,但可能会重复传输。
Exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。
“最少一次”投递实现需要考虑:
消息生产者需要开始事务机制或publisher confirm机制,以确保消息可以可靠地传输到RabbitMQ中。
消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。
消息和队列都需要进行持久化处理,以确保RabbitMQ服务器在遇到异常情况时不会造成消息丢失。
消费者在消费信息的同时需要将autoAck设置为false,然后通过手动确认的方式去确认已经正确消费的消息,以避免在消费端引起不必要的消息丢失。
RabbitMQ管理
多租户与权限
虚拟主机(virtual host):每一个RabbitMQ服务器都能创建虚拟的消息服务器,简称为vhost。每一个vhost本质上是一个独立的小型RabbitMQ服务器,拥有自己独立的队列、交换器及绑定关系等,并且它拥有自己独立的权限。
vhost时AMQP概念的基础,客户端在连接的时候必须制定一个vhost。RabbitMQ默认创建的vhost为“/”,使用默认的用户名guest和密码guest就可以访问。
创建vhost:rabbitmqctl add_vhost {vhost}
[root@Lime-CentOS ~]# rabbitmqctl add_vhost vhost1
Creating vhost "vhost1" ...
...done.
罗列当前vhost的相关信息:rabbitmqctl list_vhosts [vhostinfoitem...]
name:表示vhost的名称。
tracing:表示是否使用了RabbitMQ的trace功能。
[root@Lime-CentOS ~]# rabbitmqctl list_vhosts name tracing
Listing vhosts ...
/ false
vhost1 false
...done.
删除vhost:rabbitmqctl delete_vhost {vhost}
[root@Lime-CentOS ~]# rabbitmqctl delete_vhost vhost1
Deleting vhost "vhost1" ...
...done.
在RabbitMQ中,权限控制则是以vhost为单位的。当创建一个用户时,用户通常会被指派给至少一个vhost,并且只能访问被指派的vhost内的队列、交换器和绑定关系等。
授予权限:rabbitmqctl set_permissions [-p vhost] {user} {conf} {write} {read}
vhost:授予用户访问权限的vhost名称,可以设置为默认值,即vhost为“/”。
user:可以访问制定vhost的用户名
conf:一个用于匹配用户在哪些资源上拥有可配置(队列和交换器的创建及删除之类的操作)权限的正则表达式。
write:一个用于匹配用户在哪些资源上拥有可写(发布消息)权限的正则表达式。
read:一个用于匹配用户在哪些资源上拥有可读(与消息有关的操作,包括读取消息及清空整个队列等)权限的正则表达式。
清除权限:rabbitmqctl clear_permissions [-p vhost] {username}
列举权限信息:
显示虚拟主机上的权限:rabbitmqctl list_permissions [-p vhost]
显示用户的权限:rabbitmqctl list_user_permissions {username}
标准语法:rabbitmqctl [-n node] [-t timeout] [-q] {command} [command options...]
[-n node] : 默认节点是“rabbit@hostname”
[-q] :使用-q标志来启用quiet模式,这样可以屏蔽一下消息的输出。默认不开启quiet模式。
[-t timeout]:操作超时时间(秒为单位),只适用于“list_xxx”类型的命令,默认是无穷大。
用户管理
创建用户:rabbitmqctl add_user {username} {password}
修改密码:rabbitmqctl change_password {username} {newpassword}
清除密码:rabbitmqctl clear_password {username}
通过密码验证用户:rabbitmqctl authenticate_user {username} {password}
删除用户:rabbitmqctl delete_user {username}
罗列当前的所有用户(每个结果行都包含用户名称,,其后紧跟用户的角色(tags)):rabbitmqctl list_users
用户的角色分为5种类型
none:
management:
policymaker:
monitoring:
administartor:
设置用户的角色:rabbitmqctl set_user_tags {username} {tag ...}
Web端管理
rabbitmq-plugins [-n node] {command} [command options...]
启用插件:rabbitmq-plugins enable [plugin-name]
关闭插件:rabbitmq-plugins disable [plugin-name]
查看当前插件的使用情况:rabbitmq-plugins list
[E*]为显式启动,[e*]为隐式启动
应用与集群管理
应用管理
rabbitmqctl stop [pid_file]
rabbitmqctl shutdown
rabbitmqctl stop_app
停止RabbitMQ服务应用,但是Erlang虚拟机还是处于运行状态。此命令的执行优先于其他管理操作(这些管理操作需要先停止RabbitMQ应用),比如rabbitmqctl reset。
rabbitmqctl start_app
rabbitmqctl wait [pid_file]
rabbitmqctl reset
将RabbitMQ节点重置还原到最初状态。包括从原来所在的集群中删除此节点,从管理数据库中删除所有的配置数据,如已配置的用户、vhost等,以及删除所有的持久化消息。执行rabbitmqctl reset命令前必须停止RabbitMQ应用(比如先执行rabbitmqctl stop_app)。
rabbitmqctl force_reset
强制将RabbitMQ节点重置还原到最初状态。不同于rabbitmqctl reset命令,rabbitmqctl force_reset命令不论当前管理数据库的状态和集群配置是什么,都会无条件地重置节点。它只能在数据库或集群配置已损坏的情况下使用。执行命令前必须先停止RabbitMQ应用。
rabbitmqctl rotate_logs {suffix}:表示将日志文件的内容追加到新的日志文件中去,这个新的日志文件的文件名是原有的日志文件名加上命令中的 suffix,并且恢复日志到原来位置的新文件中。注意:如果新文件原先不存在,那么会新建一个;如果suffix为空,那么不会发生日志转移,只是重新打开了一次日志文件而已。
rabbitmqctl hipe_compile {directory}
集群管理
rabbitmqctl join_cluster {cluster_node} [--ram]
将节点加入制定集群中。在这个命令执行前需要停止RabbitMQ应用并重置节点。
rabbitmqctl cluster_status
rabbitmqctl change_cluster_node_type {disc|ram}
rabbitmqctl forget_cluster_node [--offline]
将节点从集群中删除,运行离线执行。
rabbitmqctl update_cluster_nodes {clusternode}
rabbitmqctl force_boot
rabbitmqctl sync_queue [-p vhost] {queue}
rabbitmqctl cancel_sync_queue [-p vhost] {queue}
rabbitmqctl set_cluster_name {name} :设置集群名称
服务端状态
rabbitmqctl list_queues [-p vhost] [queueinfoitem ...]
rabbitmqctl list_exchanges [-p vhost] [exchangeinfoitem ...]
rabbitmqctl list_bindings [-p vhost] [bindinginfoitem ...]
rabbitmqctl list_connections [connectioninfoitem ...]
rabbitmqctl list_channels [channelinfoitem ...]
rabbitmqctl list_consumers [-p vhost]
rabbitmqctl status
rabbitmqctl node_health_check
rabbitmqctl environment
rabbitmqctl report
rabbitmqctl eval {expr}
eval的扩展
HTTP API 接口管理
啦啦啦
RabbitMQ配置(环境变量、配置文件、运行时参数和策略)
环境变量
RabbitMQ默认的安装目录:/usr/lib/rabbitmq/lib/rabbitmq_server-3.3.5/sbin
RabbitMQ默认配置文件路径:/usr/lib/rabbitmq/lib/rabbitmq_server-3.3.5/sbin/rabbitmq-defaults
RabbitMQ环境变量文件路径(默认不存在,需要手动创建):CONF_ENV_FILE=${SYS_PREFIX}/etc/rabbitmq/rabbitmq-env.conf
RABBITMQ_NODE_IP_ADDRESS
RABBITMQ_NODE_PORT:供客户端建立连接端口
RABBITMQ_DIST_PORT
RABBITMQ_NODENAME:配置RabbitMQ节点名称
RABBITMQ_CONF_ENV_FILE:RabbitMQ环境变量的配置文件(rabbitmq-env.conf)的地址,默认值为$RABBITMQ_HOME/etc/rabbitmq/rabbitmq-env.conf
RABBITMQ_USE_LONGNAME
RABBITMQ_CONFIG_FILE:RabbitMQ配置文件(rabbitmq.confg)的路径,注意没有“.config”的后缀。默认值为$RABBITMQ_HOME/etc/rabbitmq/rabbitmq
RABBITMQ_MNESIA_BASE:RABBITMQ_MNESIA_DIR的父目录。除非明确设置了RABBITMQ_MNESIA_DIR目录,否则每个节点都应该配置这个环境变量。默认值为$RABBITMQ_HOME/var/lib/rabbitmq/mnesia。注意对于RabbitMQ的操作用户来说,需要有对当前目录可读、可写、可创建文件及子目录的权限。
RABBITMQ_MNESIA_DIR:包含RabbitMQ服务节点的数据库、数据存储及集群状态等目录,默认值为$RABBITMQ_MNESIA_BASE/$RABBITMQ_NODENAME
RABBITMQ_LOG_BASE
RABBITMQ_LOGS
RABBITMQ_SASL_LOGS
PLUGINS_DIR
RABBITMQ_SERVER_ERL_ARGS:在调用RabbitMQ服务器时使用的erl命令的标准参数, 应该仅用于调试目的
RABBITMQ_SERVER_START_ARGS:在调用RabbitMQ服务器时使用的erl命令的额外参数。这不会覆盖RABBITMQ_SERVER_ERL_ARGS.
配置文件
RabbitMQ服务启动时,服务日志打印“config files(s)”为目前的配置文件所在的路径。
[root@Lime-CentOS ~]# rabbitmq-server
RabbitMQ 3.3.5. Copyright (C) 2007-2014 GoPivotal, Inc.
## ## Licensed under the MPL. See http://www.rabbitmq.com/
## ##
########## Logs: /var/log/rabbitmq/[email protected]
###### ## /var/log/rabbitmq/[email protected]
##########
Starting broker... completed with 6 plugins.
[root@Lime-CentOS ~]# tail -f -n 100 /var/log/rabbitmq/[email protected]
=INFO REPORT==== 23-Feb-2021::10:25:30 ===
Starting RabbitMQ 3.3.5 on Erlang R16B03-1
Copyright (C) 2007-2014 GoPivotal, Inc.
Licensed under the MPL. See http://www.rabbitmq.com/
=INFO REPORT==== 23-Feb-2021::10:25:30 ===
node : rabbit@Lime-CentOS
home dir : /var/lib/rabbitmq
config file(s) : /etc/rabbitmq/rabbitmq.config
cookie hash : WvUjrKLL8ksbutFi7KlPCA==
log : /var/log/rabbitmq/[email protected]
sasl log : /var/log/rabbitmq/[email protected]
database dir : /var/lib/rabbitmq/mnesia/rabbit@Lime-CentOS
配置项
tcp_listeners
num_tcp_acceptors
配置加密
优化网络配置
参数及策略
参数
vhost级别的Parameter
rabbitmqctl set_parameter [-p vhost] {component_name} {name} {value}
rabbitmqctl list_parameters [-p vhost]
rabbitmqctl clear_parameter [-p vhost] {component_name} {key}
global级别的Parameter
rabbitmqctl set_global_parameter name value
rabbitmqctl list_global_parameters
rabbitmqctl clear_global_parameter name
策略(特殊的Parameter用法,vhost级别)
一个Policy可以匹配一个或多个队列(或者交换器,或者两者兼有)
Policy也可以支持动态地修改一些属性参数。
rabbitmqctl set_policy [-p vhost] [--priority priority] [--apply-to apply-to] {name} {pattern} {definition}
Virtual host
Name
Pattern
Apply to
Priority:定义优先级。如果有多个Policy作用于同一个交换器或者队列,那么Priority最大的那个Policy才会有用。
Definition
rabbitmqctl list_policies [-p vhost]
rabbitmqctl clear_policy [-p vhost] {name}
如果多个Policy都作用到同一个交换器或者队列上,且这些Policy的优先级都是一样的,则参数项最多的Policy具有决定权。如果参数一样多,则最后添加的Policy具有决定权。
RabbitMQ运维
集群搭建
元数据信息
队列元数据:队列的名称及属性
交换器:交换器的名称及属性
绑定关系元数据:交换器与队列或者交换器与交换器之间的绑定关系
vhost元数据:为vhost内的队列、交换器和绑定提供命名空间及安全属性
在RabbitMQ集群中创建队列,集群只会在单个节点而不是在所有节点上创建队列的进程并包含完整的队列信息(元数据、状态、内容)。这样只有队列的宿主节点,即所有者节点知道队列的所有信息,所有其他非所有者节点只知道队列的元数据和指向该队列存在的那个节点的指针。因此当集群节点崩溃时,该节点的队列进程和关联的绑定都会消失。附加在那些队列上的消费者也会丢失其所订阅的信息,并且任何匹配该队列绑定信息的新消息也都会消失。
不同于队列那样拥有自己的进程,交换器其实只是一个名称和绑定列表。当消息发布到交换器时,实际上是由所连接的信道将消息上的路由键同交换器的绑定列表进行比较,然后再路由消息。当创建一个新的交换器时,RabbitMQ所要做的就是将绑定列表添加到集群中的左右节点上。这样,每个节点上的每条信道都可以访问新的交换器了。
多机多节点配置
第一步,配置各个节点的hosts文件
vim /etc/hosts
192.168.0.2 node1
192.168.0.3 node2
192.168.0.4 node3
第二步,编辑RabbitMQ的cookie文件,以确保各个节点的cookie文件使用的是同一个值。cookie相当于密钥令牌,集群中的RabbitMQ节点需要通过交换密钥令牌以获取相互认证。
vim /var/lib/rabbitmq/.erlang.cookie
第三步,配置集群。配置集群有三种方式:通过rabbitmqctl工具配置;通过rabbitmq.config配置文件配置;通过rabbitmq-autocluster插件配置
# 启动RabbitMQ服务
rabbitmq-server -detached
# 查看各节点RabbitMQ服务集群信息
rabbitmqctl cluster_status
# 停止node2、node3节点RabbitMQ服务
rabbitmqctl stop_app
# 重置node2、node3节点RabbitMQ服务
rabbitmqctl reset
# node2、node3节点加入node1节点的RabbitMQ服务
rabbitmqctl join_cluster rabbit@node1
# 开启node2、node3节点RabbitMQ服务
rabbitmqctl start_app
# 如果最后一个关闭的节点最终由于某些异常而无法启动,将当前节点剔出当前集群
rabbitmqctl forget_cluster_node
# 如果集群中的所有节点由于某些非正常因素,比如断电而关闭,那么集群中的节点都会认为还有其他节点在它后面关闭,此时需要强制启动一个节点,之后集群才能正常启动。
rabbitmqctl force_boot
集群节点类型
Disc:磁盘类型(默认)
ram:内存类型(--ram)
将node2节点加入node1节点的时候指定node2节点的类型为内存节点
rabbitmqctl join_cluster rabbit@node1 --ram
切换节点类型:rabbitmqctl change_cluster_node_type {disc,ram}
rabbitmqctl stop_app
rabbitmqctl change_cluster_node_type disc
rabbitmqctl start_app
rabbitmqctl cluster_status
剔除单个节点
node2节点不再运行RabbitMQ的情况。node1或node3节点运行状态
# perform operation on rabbit@node2
# 关闭node2应用或节点
rabbitmqctl stop_app
rabbitmqctl stop
# perform operation on rabbit@node1
# 在node1或node3节点剔除node2节点
rabbitmqctl forget_cluster_node rabbit@node2
node1或node3节点未启动状态下,剔除node2节点
rabbitmqctl forget_cluster_node rabbit@node1 -offline
在node2上执行退出集群
# perform operation on rabbit@node2
# rabbit@node2节点恢复为独立节点
# node1或node3节点查看集群已没有node2节点信息
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
集群节点的升级
确保元节点的Mnesia中的数据不被变更。(Mnesia数据库是Erlang内置的一个DBMS,可以直接存储Erlang的各种数据结构。)
(1)关闭所有节点的服务,注意采用rabbitmqctl stop命令关闭
(2)保存各个节点的Mnesia数据
(3)解压新版本的RabbitMQ到指定的目录
(4)指定新版本的Mnesia路径为步骤2中保存的Mnesia数据路径。(RABBITMQ_MNESIA_BASE=/opt/mnesia)
(5)启动新版本的服务,注意先重启原版本中最后关闭的那个节点。
配置和数据不可丢弃
(1)保存元数据
(2)关闭所有生产者
(3)等待消费者消费完队列中的所有数据
(4)关闭所有消费者
(5)重新安装RabbitMQ
(6)重建元数据
集群迁移
单机多节点配置(在一台机器上部署多个RabbitMQ服务节点,需要确保每个节点都有独立的名称、数据存储位置、端口号(包括插件的端口号)等。)
安装Erlang
curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
yum -y -install erlang
安装RabbitMQ
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash
yum -y install rabbit-server
为每个RabbitMQ服务节点设置不同的端口号和节点名称来启动相应的服务。
RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit1 rabbitmq-server -detached
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2 rabbitmq-server -detached
RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3 rabbitmq-server -detached
在启动rabbit1@node1节点的服务之后,继续启动rabbit2@node1 和 rabbit@node1 服务节点会遇到失败的情况。这种情况大多数是由于配置发生了冲突而造成后面的节点启动失败,需要进一步确认是否开启了某些功能,比如RabbitMQ Management插件。如果开启了RabbitMQ Management插件,就需要为每个服务节点配置一个对应的插件端口号
RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit1 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15672}]" rabbitmq-server -detached
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" rabbitmq-server -detached
RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" rabbitmq-server -detached
将rabbit2@node1节点加入rabbit1@node1的集群之中
rabbitmqctl -n rabbit2@node1 stop_app
rabbitmqctl -n rabbit2@node1 reset
rabbitmqctl -n rabbit2@node1 join_cluster rabbit1@node1
rabbitmqctl -n rabbit2@node1 start_app
rabbitmqctl -n rabbit2@node1 cluster_status
将rabbit2@node1节点加入rabbit1@node1的集群之中
rabbitmqctl -n rabbit3@node1 stop_app
rabbitmqctl -n rabbit3@node1 reset
rabbitmqctl -n rabbit3@node1 join_cluster rabbit1@node1
rabbitmqctl -n rabbit3@node1 start_app
rabbitmqctl -n rabbit3@node1 cluster_status
查看服务日志
默认地址$RABBITMQ_HOME/var/log/rabbitmq
RABBITMQ_NODENAME.log
如果想查看RabbitMQ应用服务的日志,则需要查询RABBITMQ_NODENAME.log这个文件,所谓的RabbitMQ服务日志指的就是这个文件。
RABBITMQ_NODENAME-sasl.log
SASL(System Application Support Libraries,系统应用程序支持库)是库的集合,作为Erlang-OTP发行版的一部分。它们帮助开发者在开发Erlang应用程序时提供一系列标准,其中之一是日志记录格式。所以当RabbitMQ记录Erlang相关信息时,它会将日志写入文件RABBITMQ_NODENAME-sasl.log中。举例来说,可以在这个文件中找到Erlang的崩溃报告,有助于调试无法启动的RabbitMQ节点。
日志级别node、error、warning、info、debug
日志级别可以通过rabbitmq.config配置文件中的log_levels参数来进行配置,默认为[{connection,info}]
常见日志摘要
启动RabbitMQ服务 - 日志
RabbitMQ版本号、Erlang版本号、RabbitMQ服务节点名称、cookie的hash值、RabbitMQ配置文件地址、内存限制、磁盘限制、默认账户guest的创建及权限配置等。
如果开启了RabbitMQ Management插件,则在启动RabbitMQ的时候会打印插件启动日志
统计值信息的初始化日志
aggr_queue_stats_fine_stats
aggr_queue_stats_deliver_get
aggr_queue_stats_queue_msg_counts
关闭RabbitMQ服务 - 日志
rabbitmqctl stop_app(只关闭RabbitMQ服务)
rabbitmqctl stop(关闭RabbitMQ服务 和 Erlang虚拟机)
建立集群 - 日志
客户端与RabbitMQ建立连接 - 日志
客户端强制中断连接 - 日志
amq.rabbitmq.log(类型为topic的默认交换器amq.rabbitmq.log就是用来手机RabbitMQ日志的)
通过amq.rabbitmq.log收集日志
创建4个日志队列queue.debug、queue.info、queue.warning和queue.error,分别采用debug、info、warning和error这4个路由键来绑定amq.rabbitmq.log。如果要使用一个队列来收集所有级别的日志,可以使用“#”这个路由键。
DEMO(没有找到amq.rabbitmq.log呀!!!版本:RabbitMQ 3.8.12 on Erlang 23.2.5)
package club.limeyu.rabbitmq.tt.log;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/25 下午4:07
*/
public class RabbitConsumer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 创建4个日志队列queue.debug、queue.info、queue.warning和queue.error,
// 分别采用debug、info、warning和error这4个路由键来绑定amq.rabbitmq.log。
// 如果要使用一个队列来收集所有级别的日志,可以使用“#”这个路由键。
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("node1.limeyu.club");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queue.debug", true, false, false, null);
channel.queueDeclare("queue.info", true, false, false, null);
channel.queueDeclare("queue.warning", true, false, false, null);
channel.queueDeclare("queue.error", true, false, false, null);
channel.queueBind("queue.debug", "amq.rabbitmq.trace", "debug");
channel.queueBind("queue.info", "amq.rabbitmq.trace", "info");
channel.queueBind("queue.warning", "amq.rabbitmq.trace", "warning");
channel.queueBind("queue.error", "amq.rabbitmq.trace", "error");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("queue.debug", false, defaultConsumer);
channel.basicConsume("queue.info", false, defaultConsumer);
channel.basicConsume("queue.warning", false, defaultConsumer);
channel.basicConsume("queue.error", false, defaultConsumer);
TimeUnit.SECONDS.sleep(30);
channel.close();
connection.close();
}
}
单节点故障恢复(机器硬件故障、机器掉电、网络异常、服务进程异常)
啦啦啦
集群迁移
元数据重建
数据迁移和客户端连接的切换
自动化迁移
集群监控
通过HTTP API接口提供监控数据
通过客户端提供监控数据
检测RabbitMQ服务是否健康
元数据管理与监控
跨越集群的界限
Federation
联邦交换器
联邦队列
Federation的使用
(2)需要定义匹配交换器或队列的一种/多种策略(Policy)。
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
在Federation中存在3种级别的配置
(1)Upstreams:每个upstream用于定义与其他Broker建立连接的信息。
(2)Upstream sets:每个upstream set用于对一系列使用Federation功能的upstream进行分组。
(3)Policies:每个Policy会选定出一组交换器,或者队列,亦或者两者皆有而进行限定,进而作用于一个单独的upstream或upstream set 之上。
Shovel
Shovel 的原理
add_forward_headers
reconnect_delay
Shovel的使用
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
静态方式部署
动态方式部署
案例:消息堆积的治理
通过Shovel将队列中的消息移交给另一个集群
RabbitMQ高阶
存储机制
队列索引(rabbit_queue_index):维护队列中落盘消息的信息,包括消息的存储地点、是否已被交付给消费者、是否已被消费者ack等。
消息存储(rabbit_msg_store):以键值对的形式存储消息,它被所有队列共享,在每个节点中有且只有一个。
msg_store_persistent:负责持久化消息的持久化,重启后消息不会丢失
msg_store_transient:负责非持久化消息的持久化,重启后消息会丢失。
消息(包括消息体、属性和headers)可以直接存储在rabbit_queue_index中,也可以被保存在rabbit_msg_store中。(queue_index_embed_msgs_below)
默认$RABBITMQ_HOME/var/lib/mnesia/rabbit@$HOSTNAME/路径下包含queues、msg_store_peristent、msg_store_transient这3个文件夹
rabbit_queue_index中以顺序(文件名从0开始累加)的段文件来进行存储,后缀为“.idx”,每个段文件中包含固定的SEGMENT_ENTRY_COUNT条记录,SEGMENT_ENTRY_COUNT默认值为16384。
经过rabbit_msg_store处理的所有消息都会以追加的方式写入到文件中,当一个文件的大小超过指定的限制(file_size_limit)后,关闭这个文件再创建一个新的文件以供新的消息写入。文件名(文件后缀是“.rdq”)从0开始进行累加,因此文件名最小的文件也是最老的文件。在进行消息的存储时,RabbitMQ会在ETS(Erlang Term Storage)表中记录消息在文件中的位置映射(Index)和文件的相关信息(FileSummary)。
在读取消息的时候,先根据消息的ID(msg_id)找到对应存储的文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息的内容。如果文件不存在或者被锁住了,则发送请求由rabbit_msg_store进行处理。
消息的删除只是从ETS表删除指定消息的相关信息,同时更新消息对应的存储文件的相关信息。执行消息删除操作时,并不立即对在文件中的消息进行删除,也就是说消息依然在文件中,仅仅是标记为垃圾数据而已。
rabbit_amqqueue_process:协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的confirm和消费端的ack)等。
backing_queue:消息存储的具体形式和引擎,并向rabbit_amqqueue_process提供相关的接口以供调用。
消息的4中状态:
alpha:消息内容(包括消息体、属性和headers)和消息索引都存储在内存中。
beta:消息内容保存在磁盘中,消息索引保存在内存中。
gamma:消息内容保存在磁盘中,消息索引在磁盘和内存中都有。
delta:消息内容和索引都在磁盘中。
对于普通的没有设置优先级和镜像的队列来说,backing_queue的默认实现是rabbit_variable_queue,其内部通过5个子队列Q1(alpha)、Q2(beta)、Delta(delta)、Q3(gamma)和Q4(alpha)来实现消息的各个状态。
整个队列包括rabbit_amqqueue_process和backing_queue的各个子队列。
消费者获取消息也会引起消息的状态转换。当消费者获取消息时,首先会从Q4中获取消息,如果获取成功则返回。如果Q4为空,则尝试从Q3中获取消息,系统首先会判断Q3是否为空,如果为空则返回队列为空,即此时队列中无消息。如果Q3不为空,则取出Q3中的消息,进而再判断此时Q3和Delta中的长度,如果都为空,则可以认为Q2、Delta、Q3、Q4全部为空,此时将Q1中的消息直接转移至Q4,下次直接从Q4中获取消息。如果Q3为空,Delta不为空,则将Delta的消息转移至Q3中,下次可以直接从Q3中获取消息。在将消息从Delta转移到Q3的过程中,是按照索引分段读取的,首先读取某一段,然后判断读取的消息的个数与Delta中消息的个数是否相等,如果相等,则可以判定此时Delta中已无消息,则直接将Q2和刚读到的消息一并放入到Q3中;如果不相等,仅将此次读取到的消息转移到Q3。
通常在负载正常时,如果消息被消费的速度不小于接收新消息的数据,对于不需要保证可靠不丢失的消息来说,极有可能只会处于alpha状态。对于durable属性设置为true的消息,它一定会进入gamma状态,并且在开启publisher confirm机制时,只有到了gamma状态时才会确认该消息已被接收,若消息消费数据足够快、内存也充足,这些消息也不会继续走到下一个状态。
惰性队列会将接收到的消息直接存入文件系统中,而不管是持久化的或者是非持久化的,这样可以减少了内存的消耗,但是会增加I/O的使用,如果消息是持久化的,那么这样的I/O操作不可避免,惰性队列和持久化的消息可谓是“最佳拍档”。注意如果惰性队列中存储的是非持久化的消息,内存的使用率会一直很稳定,但是重启之后消息一样会丢失。
channel.queueDeclare x-queue-mode:{lazy|default}
BlockedListener监听相应连接的阻塞信息。
rabbitmqctl set_vm_memory_high_watermark {fraction}(重启失效)
rabbitmqctl set_vm_memory_high_watermark absolute {memory_limit}(重启失效)
配置文件(重启生效)
网络分区
RabbitMQ扩展
附录A 集群元数据信息示例
SpringBoot 整合
Maven
yml
spring:
rabbitmq:
host: rabbit.limeyu.club
port: 5672
username: rabbit
password: rabbit
直连交换器
配置交换器、队列、绑定(DirectExchangeConfig)
package club.limeyu.sprbtrbtmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午10:58
*/
@Configuration
public class DirectExchangeConfig {
// 声明接收接车单ID的队列
@Bean
public Queue declareQueue() {
Queue queue = new Queue("queue.receiveBaseId", true, false, false, null);
return queue;
}
// 声明直连交换器
@Bean
public DirectExchange declareDirectExchange() {
DirectExchange directExchange = new DirectExchange("exchange.direct.receiveBaseId", true, false, null);
return directExchange;
}
// 绑定交换器与队列
@Bean
public Binding BindQueue() {
Binding binding = BindingBuilder.bind(declareQueue()).to(declareDirectExchange()).with("rk.receiveBaseId");
return binding;
}
}
发送消息到RabbitMQ Server(SaveReceiveBaseListener)
package club.limeyu.sprbtrbtmq.event.listener;
import club.limeyu.sprbtrbtmq.event.SaveReceiveBaseEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:19
*/
@Slf4j
@Component
public class SaveReceiveBaseListener implements ApplicationListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void onApplicationEvent(SaveReceiveBaseEvent saveReceiveBaseEvent) {
rabbitTemplate.convertAndSend("exchange.direct.receiveBaseId", "rk.receiveBaseId", saveReceiveBaseEvent.getReceiveBaseId());
}
}
消费端监听消费队列(多个消费端轮询消费)
消费端负载 1 (ConsumerReceiveBaseId1)
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:43
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.receiveBaseId")
public class ConsumerReceiveBaseId1 {
@RabbitHandler
public void consumerReceiveBaseId(Long receiveBaseId) {
log.info("负载消费1::接收到接车单:{}", receiveBaseId);
}
}
消费端负载 2 (ConsumerReceiveBaseId2)
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:43
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.receiveBaseId")
public class ConsumerReceiveBaseId2 {
@RabbitHandler
public void consumerReceiveBaseId(Long receiveBaseId) {
log.info("负载消费2 :: 接收到接车单:{}", receiveBaseId);
}
}
扇形交换器
配置交换器、队列、绑定
package club.limeyu.sprbtrbtmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午1:16
*/
@Configuration
public class FanoutExchangeConfig {
@Bean
public FanoutExchange declareFanoutExchange() {
FanoutExchange fanoutExchange = new FanoutExchange("exchange.fanout.receiveBaseId", true, false, null);
return fanoutExchange;
}
@Bean
public Queue declareQueueHave() {
Queue queue = new Queue("queue.have", true, false, false, null);
return queue;
}
@Bean
public Queue declareQueueNice() {
Queue queue = new Queue("queue.nice", true, false, false, null);
return queue;
}
@Bean
public Queue declareQueueDay() {
Queue queue = new Queue("queue.day", true, false, false, null);
return queue;
}
@Bean
public Binding bindingQueueHave() {
Binding binding = BindingBuilder.bind(declareQueueHave()).to(declareFanoutExchange());
return binding;
}
@Bean
public Binding bindingQueueNice() {
Binding binding = BindingBuilder.bind(declareQueueNice()).to(declareFanoutExchange());
return binding;
}
@Bean
public Binding bindingQueueDay() {
Binding binding = BindingBuilder.bind(declareQueueDay()).to(declareFanoutExchange());
return binding;
}
}
发送消息到RabbitMQ Server
package club.limeyu.sprbtrbtmq.event.listener;
import club.limeyu.sprbtrbtmq.event.SaveReceiveBaseEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:19
*/
@Slf4j
@Component
public class SaveReceiveBaseListener implements ApplicationListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void onApplicationEvent(SaveReceiveBaseEvent saveReceiveBaseEvent) {
rabbitTemplate.convertAndSend("exchange.fanout.receiveBaseId", "", saveReceiveBaseEvent.getReceiveBaseId());
}
}
消费端监听消费队列
监听queue.have队列
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午1:27
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.have")
public class ConsumerFanoutHave {
@RabbitHandler
public void consumerQueueHave(Long receiveBaseId) {
log.info("queue.have 消费 :{}", receiveBaseId);
}
}
监听queue.nice队列
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午1:27
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.nice")
public class ConsumerFanoutNice {
@RabbitHandler
public void consumerQueueNice(Long receiveBaseId) {
log.info("queue.nice 消费 :{}", receiveBaseId);
}
}
监听queue.day队列
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午1:27
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.day")
public class ConsumerFanoutDay {
@RabbitHandler
public void consumerQueueDay(Long receiveBaseId) {
log.info("queue.day 消费 :{}", receiveBaseId);
}
}
主题交换器
设置交换器、队列、绑定关系
package club.limeyu.sprbtrbtmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:50
*/
@Slf4j
@Configuration
public class TopicExchangeConfig {
@Bean
public TopicExchange declareTopicExchange() {
TopicExchange topicExchange = new TopicExchange("exchange.topic.receiveBaseId", true, false, null);
return topicExchange;
}
@Bean
public Queue declareQueueNatural() {
Queue queue = new Queue("queue.natural.receiveBaseId", true, false, false, null);
return queue;
}
@Bean
public Queue declareQueueOrange() {
Queue queue = new Queue("queue.orange.receiveBaseId", true, false, false, null);
return queue;
}
@Bean
public Queue declareQueueFresh() {
Queue queue = new Queue("queue.fresh.receiveBaseId", true, false, false, null);
return queue;
}
@Bean
public Binding bindingQueueNatural() {
Binding binding = BindingBuilder.bind(declareQueueNatural()).to(declareTopicExchange()).with("rk.natural.receiveBaseId");
return binding;
}
@Bean
public Binding bindingQueueOrange() {
Binding binding = BindingBuilder.bind(declareQueueOrange()).to(declareTopicExchange()).with("rk.#");
return binding;
}
@Bean
public Binding bindingQueueFresh() {
Binding binding = BindingBuilder.bind(declareQueueFresh()).to(declareTopicExchange()).with("rk.fresh.*");
return binding;
}
}
发送消息到RabbitMQ Server
package club.limeyu.sprbtrbtmq.event.listener;
import club.limeyu.sprbtrbtmq.event.SaveReceiveBaseEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:19
*/
@Slf4j
@Component
public class SaveReceiveBaseListener implements ApplicationListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void onApplicationEvent(SaveReceiveBaseEvent saveReceiveBaseEvent) {
// 匹配 rk.natural.receiveBaseId
// 匹配 rk.#
// rabbitTemplate.convertAndSend("exchange.topic.receiveBaseId", "rk.natural.receiveBaseId", saveReceiveBaseEvent.getReceiveBaseId());
// 匹配 rk.#
// rabbitTemplate.convertAndSend("exchange.topic.receiveBaseId", "rk.topic", saveReceiveBaseEvent.getReceiveBaseId());
// 匹配 rk.#
// rabbitTemplate.convertAndSend("exchange.topic.receiveBaseId", "rk.fresh", saveReceiveBaseEvent.getReceiveBaseId());
// 匹配 rk.#
// 匹配 rk.fresh.receiveBaseId
rabbitTemplate.convertAndSend("exchange.topic.receiveBaseId", "rk.fresh.morning", saveReceiveBaseEvent.getReceiveBaseId());
}
}
消费端监听消费队列
监听queue.natural.receiveBaseId队列
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午12:04
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.natural.receiveBaseId")
public class ConsumerTopicNatural {
@RabbitHandler
public void consumerQueueNatural(Long receiveBaseId) {
log.info("queue.natural.receiveBaseId 接收到消息 : {}", receiveBaseId);
}
}
监听queue.orange.receiveBaseId队列
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午12:04
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.orange.receiveBaseId")
public class ConsumerTopicOrange {
@RabbitHandler
public void consumerQueueOrange(Long receiveBaseId) {
log.info("queue.orange.receiveBaseId 接收到消息 : {}", receiveBaseId);
}
}
监听queue.fresh.receiveBaseId队列
package club.limeyu.sprbtrbtmq.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午12:04
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.fresh.receiveBaseId")
public class ConsumerTopicFresh {
@RabbitHandler
public void consumerQueueFresh(Long receiveBaseId) {
log.info("queue.fresh.receiveBaseId 接收到消息 : {}", receiveBaseId);
}
}
头交换器
生产方确认机制(事务机制、发送方确认机制)
yml
spring:
rabbitmq:
# 确认消息已发送到交换机(Exchange)
publisher-confirms: true
# 确认消息已发送到队列(Queue)
publisher-returns: true
设置RabbitTemplate,配置ConfirmCallback、ReturnCallback监听器
package club.limeyu.sprbtrbtmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午1:46
*/
@Slf4j
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate initRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("ConfirmCallback :: 请求唯一标志 : {}", null != correlationData ? correlationData.getId() : null);
log.info("ConfirmCallback :: 确认情况 : {}", ack);
log.info("ConfirmCallback :: 原因 : {}", cause);
}
});
// 当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,
// 那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。
// 当mandatory参数设置为false时,出现上述情形,则消息直接被丢弃。
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("ReturnCallback :: message.getBody() : {}", message.getBody());
MessageProperties messageProperties = message.getMessageProperties();
if (null != messageProperties) {
log.info("ReturnCallback :: messageProperties.getAppId() : {}", messageProperties.getAppId());
log.info("ReturnCallback :: messageProperties.getCorrelationIdString() : {}", messageProperties.getCorrelationIdString());
log.info("ReturnCallback :: messageProperties.getContentType() : {}", messageProperties.getContentType());
log.info("ReturnCallback :: messageProperties.getContentLength() : {}", messageProperties.getContentLength());
log.info("ReturnCallback :: messageProperties.getContentEncoding() : {}", messageProperties.getContentEncoding());
log.info("ReturnCallback :: messageProperties.getConsumerTag() : {}", messageProperties.getConsumerTag());
log.info("ReturnCallback :: messageProperties.getConsumerQueue() : {}", messageProperties.getConsumerQueue());
log.info("ReturnCallback :: messageProperties.getClusterId() : {}", messageProperties.getClusterId());
log.info("ReturnCallback :: messageProperties.getDeliveryTag() : {}", messageProperties.getDeliveryTag());
log.info("ReturnCallback :: messageProperties.getDeliveryMode() : {}", messageProperties.getDeliveryMode());
log.info("ReturnCallback :: messageProperties.getDelay() : {}", messageProperties.getDelay());
log.info("ReturnCallback :: messageProperties.getExpiration() : {}", messageProperties.getExpiration());
Map headers = messageProperties.getHeaders();
for (Map.Entry entry : headers.entrySet()) {
log.info("ReturnCallback :: headers : key : {}, value : {}", entry.getKey(), entry.getValue());
}
log.info("ReturnCallback :: messageProperties.getInferredArgumentType() : {}", messageProperties.getInferredArgumentType());
log.info("ReturnCallback :: messageProperties.getMessageId() : {}", messageProperties.getMessageId());
log.info("ReturnCallback :: messageProperties.getMessageCount() : {}", messageProperties.getMessageCount());
log.info("ReturnCallback :: messageProperties.getPriority() : {}", messageProperties.getPriority());
log.info("ReturnCallback :: messageProperties.getReceivedDelay() : {}", messageProperties.getReceivedDelay());
log.info("ReturnCallback :: messageProperties.getReceivedDeliveryMode() : {}", messageProperties.getReceivedDeliveryMode());
log.info("ReturnCallback :: messageProperties.getReplyToAddress() : {}", messageProperties.getReplyToAddress());
log.info("ReturnCallback :: messageProperties.getReplyTo() : {}", messageProperties.getReplyTo());
log.info("ReturnCallback :: messageProperties.getRedelivered() : {}", messageProperties.getRedelivered());
log.info("ReturnCallback :: messageProperties.getReceivedUserId() : {}", messageProperties.getReceivedUserId());
log.info("ReturnCallback :: messageProperties.getReceivedRoutingKey() : {}", messageProperties.getReceivedRoutingKey());
log.info("ReturnCallback :: messageProperties.getReceivedExchange() : {}", messageProperties.getReceivedExchange());
log.info("ReturnCallback :: messageProperties.getTargetBean() : {}", messageProperties.getTargetBean());
log.info("ReturnCallback :: messageProperties.getTargetMethod() : {}", messageProperties.getTargetMethod());
log.info("ReturnCallback :: messageProperties.getTimestamp() : {}", messageProperties.getTimestamp());
log.info("ReturnCallback :: messageProperties.getType() : {}", messageProperties.getType());
log.info("ReturnCallback :: messageProperties.getUserId() : {}", messageProperties.getUserId());
}
log.info("ReturnCallback :: replyCode : {}", replyCode);
log.info("ReturnCallback :: replyText : {}", replyText);
log.info("ReturnCallback :: exchange : {}", exchange);
log.info("ReturnCallback :: routingKey : {}", routingKey);
}
});
return rabbitTemplate;
}
}
配置交换器、队列、绑定关系
package club.limeyu.sprbtrbtmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午3:43
*/
@Component
@Configuration
public class DirectExchangePublisherConfirmConfig {
// 声明交换器
@Bean
public DirectExchange declareExchangeDirectConfirm() {
DirectExchange directExchange = new DirectExchange("exchange.direct.publisher.confirm", true, false, null);
return directExchange;
}
// 声明队列
@Bean
public Queue declareQueueConfirm() {
Queue queue = new Queue("queue.publisher.confirm");
return queue;
}
// 绑定关系
@Bean
public Binding bindingQueue() {
Binding binding = BindingBuilder.bind(declareQueueConfirm()).to(declareExchangeDirectConfirm()).with("rk.publisher.confirm");
return binding;
}
}
发送消息到RabbitMQ Server
package club.limeyu.sprbtrbtmq.event.listener;
import club.limeyu.sprbtrbtmq.event.SaveReceiveBaseEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 上午11:19
*/
@Slf4j
@Component
public class SaveReceiveBaseListener implements ApplicationListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void onApplicationEvent(SaveReceiveBaseEvent saveReceiveBaseEvent) {
// 交换器不存在(ConfirmCallback 服务端确认一条或多条消息后,发送方会回调这个方法进行处理)
// rabbitTemplate.convertAndSend("exchange.direct.publisher.confirm.not.exist", "rk.publisher.confirm", saveReceiveBaseEvent.getReceiveBaseId(), new CorrelationData(UUID.randomUUID().toString()));
// 交换器存在,路由键不匹配或队列不存在(ConfirmCallback 服务端确认一条或多条消息后,发送方会回调这个方法进行处理,ReturnCallback 监听获取到没有被正确路由到合适队列的消息)
// rabbitTemplate.convertAndSend("exchange.direct.publisher.confirm", "rk.publisher.confirm.not.exist", saveReceiveBaseEvent.getReceiveBaseId(), new CorrelationData(UUID.randomUUID().toString()));
// 交换器存在,队列存在,正常发送消息(ConfirmCallback 服务端确认一条或多条消息后,发送方会回调这个方法进行处理)
rabbitTemplate.convertAndSend("exchange.direct.publisher.confirm", "rk.publisher.confirm", saveReceiveBaseEvent.getReceiveBaseId(), new CorrelationData(UUID.randomUUID().toString()));
}
}
消费方确认机制
配置简单消息监听容器(SimpleMessageListenerContainer)
package club.limeyu.sprbtrbtmq.config;
import club.limeyu.sprbtrbtmq.rabbitmq.ConsumerConfirmManual;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午3:18
*/
@Component
@Configuration
public class ConsumerConfirmConfig {
@Autowired
private ConsumerConfirmManual consumerConfirmManual;
@Bean
public SimpleMessageListenerContainer initSimpleMessageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(connectionFactory);
// 设置多个并发消费者一起消费
simpleMessageListenerContainer.setConcurrentConsumers(1);
// 设置最多的并发消费者
simpleMessageListenerContainer.setMaxConcurrentConsumers(1);
// AcknowledgeMode.NONE : 默认推送的所有消息都已经消费成功
// AcknowledgeMode.MANUAL : 需要人为地获取到channel之后调用方法向server发送ack(或消费失败时的nack)信息
// AcknowledgeMode.AUTO : 由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到server端
simpleMessageListenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 同时监听多个队列
simpleMessageListenerContainer.addQueueNames("queue.consumer.confirm.manual");
// 设置消息监听
simpleMessageListenerContainer.setMessageListener(consumerConfirmManual);
return simpleMessageListenerContainer;
}
}
获取消息,处理消息。(实现ChannelAwareMessageListener接口)
package club.limeyu.sprbtrbtmq.rabbitmq;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @Author liangmy
* @Description:
* @Create: 2021/2/26 下午3:31
*/
@Slf4j
@Component
public class ConsumerConfirmManual implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
log.info("message.getBody() : {}", new String(message.getBody()));
MessageProperties messageProperties = message.getMessageProperties();
if (null != messageProperties) {
log.info("messageProperties.getAppId() : {}", messageProperties.getAppId());
log.info("messageProperties.getClusterId() : {}", messageProperties.getClusterId());
log.info("messageProperties.getConsumerQueue() : {}", messageProperties.getConsumerQueue());
log.info("messageProperties.getConsumerTag() : {}", messageProperties.getConsumerTag());
log.info("messageProperties.getContentEncoding() : {}", messageProperties.getContentEncoding());
log.info("messageProperties.getContentLength() : {}", messageProperties.getContentLength());
log.info("messageProperties.getContentType() : {}", messageProperties.getContentType());
log.info("messageProperties.getCorrelationIdString() : {}", messageProperties.getCorrelationIdString());
log.info("messageProperties.getDelay() : {}", messageProperties.getDelay());
log.info("messageProperties.getDeliveryMode() : {}", messageProperties.getDeliveryMode());
log.info("messageProperties.getDeliveryTag() : {}", messageProperties.getDeliveryTag());
log.info("messageProperties.getExpiration() : {}", messageProperties.getExpiration());
Map headers = messageProperties.getHeaders();
for (Map.Entry entry : headers.entrySet()) {
log.info("messageProperties.getHeaders() :: key : {}, value : {}", entry.getKey(), entry.getValue());
}
log.info("messageProperties.getInferredArgumentType() : {}", messageProperties.getInferredArgumentType());
log.info("messageProperties.getMessageCount() : {}", messageProperties.getMessageCount());
log.info("messageProperties.getMessageId() : {}", messageProperties.getMessageId());
log.info("messageProperties.getPriority() : {}", messageProperties.getPriority());
log.info("messageProperties.getReceivedDelay() : {}", messageProperties.getReceivedDelay());
log.info("messageProperties.getReceivedDeliveryMode() : {}", messageProperties.getReceivedDeliveryMode());
log.info("messageProperties.getReceivedExchange() : {}", messageProperties.getReceivedExchange());
log.info("messageProperties.getReceivedRoutingKey() : {}", messageProperties.getReceivedRoutingKey());
log.info("messageProperties.getReceivedUserId() : {}", messageProperties.getReceivedUserId());
log.info("messageProperties.getRedelivered() : {}", messageProperties.getRedelivered());
log.info("messageProperties.getReplyTo() : {}", messageProperties.getReplyTo());
log.info("messageProperties.getReplyToAddress() : {}", messageProperties.getReplyToAddress());
log.info("messageProperties.getTargetBean() : {}", messageProperties.getTargetBean());
log.info("messageProperties.getTargetMethod() : {}", messageProperties.getTargetMethod());
log.info("messageProperties.getTimestamp() : {}", messageProperties.getTimestamp());
log.info("messageProperties.getType() : {}", messageProperties.getType());
log.info("messageProperties.getUserId() : {}", messageProperties.getUserId());
}
// 确认单条消息已接收
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// 确认单条消息不接收,并重新加入队列
// channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
// 确认单条消息不接收,并不重新加入队列
// channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
// 拒绝消息,重新加入队列
// channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
// 是否恢复消息到队列,参数是requeue。
// true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费,而不是自己再次消费。
// false则消息会重新被投递给自己。
channel.basicRecover(true);
}
}
RPC实现
延迟队列
备份交换器
啦啦啦
业务架构