RabbitMq
RabbitMq 是一个实现AMQP(高级消息队列协议)的消息队列中间件,采用Erlang语言开发。最初起源于金融系统,用于在分布式系统中存储转发消息。
一、Rabbitmq 架构以及基础概念
组成部分说明:
Producer:生产者,消息生产者,通过channel 与server端连接,负责发送消息至server端。
Channel:信道,负责连接客户端和server端,是基于TCP连接之上的虚拟连接。
Broker:消息服务,包括两个部分:Exchange和Queue。
Exchange:交换机,负责将消息转发至各个队列。
Queue:队列,以队列的消息存储消息实体,先进先出。
Consumer:消费者,接收server端推送过来的消息并进行处理。
Virtual host:即虚拟主机,当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue。
生产者发送消息流程:
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
5、如果开启持久化,进行进行持久化
5、ack回复
消费者接收消息流程:
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
6、ack回复
补充:connect 与channel
connect 是客户端与broker 之间的真实的TCP/IP 连接,channel 是建立在connect 上的虚拟连接。这种架构设计主要是考虑以下场景:一个应用内部有多个线程在生产或者消费消息。原因在于,如果只有connect ,必然会导致每个线程都需要走TCP/IP连接,而建立和销毁TCP连接是非常昂贵的开销,如果遇到使用高峰,会有比较明显的性能瓶颈。而这种模式,底层采用的是NIO多路复用,极大的提高的性能。
二、Rabbitmq 消息模式
rabbitmq 生产者产生的消息都需要经过exchange将消息转发至queue中。对于rabbitmq 的exchange ,我们可以自己定义,也可以使用直接使用默认的exchange。基于这种两种方式,可以组合以下的几种消息模式:
默认的exchange:1、基本消息模式,2、work 消息模式
在使用自定义exchange时,可以指定exchange 的类型。exchange 支持三种类型:fanout,direct,topics,因此也对应了三种消息模式:1、广播模式(fanout),2、路由模式(direct),3、通配符模式(topics)
1、基本消息模式
生产者发送消息的时候,直接指定queue,将消息通过默认的exchange 发送至指定的queue中。基本消息模式,对应的消费客户端只有一个。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gL8naZXS-1622446734342)(https://i.loli.net/2021/05/25/V9NcqQY5uE6rBZK.png)]
生产者代码示例:
public class Send {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 1、获取到连接
Connection connection = getConnection();
// 2、从连接中创建通道,使用通道才能完成消息相关的操作
Channel channel = connection.createChannel();
// 3、声明(创建)队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 4、消息内容
String message = "Hello World!";
// 向指定的队列中发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props,消息的属性
* 4、body,消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//关闭通道和连接(资源关闭最好用try-catch-finally语句处理)
channel.close();
connection.close();
}
}
消费端代码示例:
public void process() throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(queueName, false, false, false, null);
//实现消费方法
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws java.io.IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(queueName, true, consumer);
}
2、work 模式
work模式是基本模式的进阶版,当消费端是多个的时候,就是work模式。值得注意的是,当一个queue 对应多个消费端时,queue中的一条消息,只能被一个消费端获取。
备注:
对于实现AMQP协议的RabbitMQ,其消费端存在一个消息缓冲区,缓冲区带下可以通过prefetch_count 参数进行设置。设置代码如下:
public void process() throws Exception { // 获取连接 Connection connection = ConnectFactory.getConnection(); // 获取channel Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(queueName, false, false, false, null); // 设置消费端消息缓冲区大小 channel.basicQos(30); //实现消费方法 DefaultConsumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // body 即消息体 String msg = new String(body,"utf-8"); System.out.println(" [x] received : " + msg + "!"); } }; channel.basicConsume(queueName, true, consumer); }
参考其中的 ***channel.basictQos(30)***。这个参数生效有一个前提:消费端不能设置为自动ack。
关于prefetch_count 的设置参考:https://www.jianshu.com/p/cd9548e7a0f8
更多prefetch_count详情参考:https://www.rabbitmq.com/consumer-prefetch.html
3、广播模式(fanout)
当生产者产生的消息需要发送给多个消费者时,可以采用这种模式。在广播模式下,生产者将消息发送给指定的exchange,而消费者除了声明队列外,还需要将队列绑定到该exhange。只要绑定到该exchange的队列,都会收到消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cl5iQxGi-1622446734346)(https://i.loli.net/2021/05/27/YGgkfUbHPEwMZC1.png)]
生产者代码示例:
与上面两种模式不同的是,生产者不在声明queue,而是声明exchange,发送消息时,需要明确发送至哪个exchange。
public void sendByExchange(byte[] message) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明exchange
// 参数:String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments
/**
* 参数明细
* 1、exchange: 交换器名称
* 2、type: 交换器类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
* 3、durable: 是否持久化,durable设置为true表示持久化,反之是非持久化,持久化的可以将交换器存盘,在服务器重启的时候不会丢失信息.
* 4、autoDelete是否自动删除,设置为TRUE则表是自动删除,自删除的前提是至少有一个队列或者交换器与这交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑,一般都设置为fase
* 5、internal: 是否内置,如果设置 为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器的方式
* 6、arguments: 其它一些结构化参数比如:alternate-exchange
*/
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false, false, false, null);
// 发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange, 交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey, 路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props, 消息的属性
* 4、body, 消息内容
*/
channel.basicPublish(exchangeName, "", null, message);
//关闭通道
channel.close();
// 关闭连接
connection.close();
}
消费者代码示例:
消费者不仅要声明queue,同时需要将声明的queue 绑定到 指定的exchange 上。
// routingKey = ""
public void processByRoutingKey(String routingKey) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(queueName, false, false, false, null);
// 绑定队列到交换机
// 参数: String queue, String exchange, String routingKey
/**
* 1、queue: 队列名称
* 2、exchange: 交换机名称
* 3、routingKey:
*/
channel.queueBind(queueName, exchangeName, routingKey);
// 设置消费端消息缓冲区大小
channel.basicQos(30);
//实现消费方法
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws java.io.IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(queueName, true, consumer);
}
4、Routing 模式(路由模式,exchange 类型:direct)
当生产者在声明exchange时,将exchange的类型指定为direct,这个时候rabbitmq就运行在Routing 模式。在这个模式下,生产者发送消息时,需要指定消息的routing key。消费者在将声明的queue绑定至指定的exchange下时,也需要声明routing key,指定接收这些routing key 的消息。exchange 会根据消息的routing key 将消息发送至不同的queue。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nAKLYyiF-1622446734348)(https://i.loli.net/2021/05/27/5RIivs2l3OjChQP.png)]
如上图所示的Routing 模式示意图,其中
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
生产者代码示例:
public void sendByExchangeAndRoutingKey(byte[] message, String routingKey) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明exchange
// 参数:String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments
/**
* 参数明细
* 1、exchange: 交换器名称
* 2、type: 交换器类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
* 3、durable: 是否持久化,durable设置为true表示持久化,反之是非持久化,持久化的可以将交换器存盘,在服务器重启的时候不会丢失信息.
* 4、autoDelete是否自动删除,设置为TRUE则表是自动删除,自删除的前提是至少有一个队列或者交换器与这交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑,一般都设置为fase
* 5、internal: 是否内置,如果设置 为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器的方式
* 6、arguments: 其它一些结构化参数比如:alternate-exchange
*/
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false, false, false, null);
// 发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange, 交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey, 路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props, 消息的属性
* 4、body, 消息内容
*/
channel.basicPublish(exchangeName, routingKey, null, message);
//关闭通道
channel.close();
// 关闭连接
connection.close();
}
消费者代码示例:
// routingKey = 'error'
public void processByRoutingKey(String routingKey) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(queueName, false, false, false, null);
// 绑定队列到交换机
// 参数: String queue, String exchange, String routingKey
/**
* 1、queue: 队列名称
* 2、exchange: 交换机名称
* 3、routingKey:
*/
channel.queueBind(queueName, exchangeName, routingKey);
// 设置消费端消息缓冲区大小
channel.basicQos(30);
//实现消费方法
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws java.io.IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(queueName, true, consumer);
}
5、topics 模式,通配符模式(exchange 类型:topics)
消费端如果需要接收多个routing key 的消息,除了直接声明明确的routing key 外,可以使用通配符来指定routing key 。在使用通配符的情况下,exchange 的类型需要声明为topics。这个时候,rabbitmq 运行的模式就是topics 模式,其示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Svvi551q-1622446734349)(https://i.loli.net/2021/05/27/DNikzQKuFsaOc7B.png)]
生产者代码示例:
public void sendByExchangeAndRoutingKey(byte[] message, String routingKey) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明exchange
// 参数:String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments
/**
* 参数明细
* 1、exchange: 交换器名称
* 2、type: 交换器类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
* 3、durable: 是否持久化,durable设置为true表示持久化,反之是非持久化,持久化的可以将交换器存盘,在服务器重启的时候不会丢失信息.
* 4、autoDelete是否自动删除,设置为TRUE则表是自动删除,自删除的前提是至少有一个队列或者交换器与这交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑,一般都设置为fase
* 5、internal: 是否内置,如果设置 为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器的方式
* 6、arguments: 其它一些结构化参数比如:alternate-exchange
*/
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false, false, false, null);
// 发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange, 交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey, 路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props, 消息的属性
* 4、body, 消息内容
*/
channel.basicPublish(exchangeName, routingKey, null, message);
//关闭通道
channel.close();
// 关闭连接
connection.close();
}
消费者代码示例:
// routingKey = "*.rabbit.*"
public void processByRoutingKey(String routingKey) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(queueName, false, false, false, null);
// 绑定队列到交换机
// 参数: String queue, String exchange, String routingKey
/**
* 1、queue: 队列名称
* 2、exchange: 交换机名称
* 3、routingKey:
*/
channel.queueBind(queueName, exchangeName, routingKey);
// 设置消费端消息缓冲区大小
channel.basicQos(30);
//实现消费方法
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws java.io.IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(queueName, true, consumer);
}}
通配符规则备注:
Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,对于每个单词可以采用如下通配符
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
三、如何确保消息不丢失
消息在整个流程中,有三个地方存在丢失的可能:
-
生产者发送消息时消息丢失。(生产者发送消息时,server端宕机,生产者误认为消息发送成功)
-
消息在rabbitmq server 端中,server 宕机,消息丢失。(消息在rabbitmq server 内存中,server 宕机导致消息丢失)
-
消费者消费消息时宕机。(消息者已经获取到消息,在消息进行处理的过程中,消费者宕机,消息还没有处理完)
为了解决上面三种丢失消息的可能,rabbitmq提供消息确认,事务,持久化三种机制来避免消息的丢失。
1、事务机制
基于AMQP协议,Rabbitmq 对生产者发送消息的过程提供了事务机制。使用事务可以确保消息一定发送至了服务端,代码示例如下:
try {
channel.txSelect();
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
channel.txCommit();
} catch (Exception e) {
e.printStackTrace();
channel.txRollback();
}
txSelect 用于将channel设置成transaction模式。
txCommit 用于提交事务。
txRollback 用于回滚事务。
在调用txSelect将channel设置transaction模式后,生产者将消息发送至broker,如果txCommit提交成功了,则消息一定到达了broker。因此对于生产者来说,可以采用事务机制来明确消息一定到达了server端,从而避免生产者发送消息时消息的丢失。
2、持久化机制
Rabbitmq queue 里面的消息都是放在内存中,因此当server端突然宕机时,内存中的消息就会丢失。为了避免这种情况,rabbitmq提供了持久化机制,将消息写入磁盘。在宕机重启时,从磁盘将消息重新读入内存,确保信息不丢失。
**为了实现消息的持久化,需要将queue,exchange和Message都持久化。**因为rabbitmq queue,exchange 都是由客户端声明的,queue、exchange 的meta 信息都是存储在内存中,如果服务端宕机这些信息也会丢失。当服务器重启后,如果queue,exchange 没有持久化,将会丢失这些信息,也就是对应的queue,exchange 都会缺失,也就没办法将消息持久化。
队列的持久化:
在声明队列的时候,通过api 参数可以将队列持久化。如下代码所示:
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(queueName, true, false, false, null);
如果将queue的持久化标识durable设置为true,则代表声明了一个持久的队列。持久化的队列在服务重启之后,也会存在,因为server会把持久化queue的元信息存放在磁盘上。当服务重启的时候,会重新加载之前被持久化的queue。
消息的持久化
当将队列持久化之后,在发送消息时,将消息也声明为持久化,这时候server 才会将消息也写入磁盘。在服务重启时,server会将这些消息重新加载进queue。声明消息持久化代码示例如下
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange, 交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey, 路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props, 消息的属性
* 4、body, 消息内容
*/
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
将其中第三个参数设置为MessageProperties.PERSISTENT_TEXT_PLAIN就是将消息声明为持久化。
exchange 持久化
exchange 是否持久化对于消息的可靠性来说没有影响,因为消息不存储在exchange 中,exchange 只是转化消息。因此如果已经将queue和消息声明为持久化之后,服务重启消息就不会丢失。但是如果不将exchange 声明为持久化,服务重启后,exchange 就会丢失,如果producer不重新声明这个exchange ,而直接发送信息会出现发送报错的情况。因此建议将exchange 也声明为持久化。exchange 的持久化声明也比较简单,代码示例如下:
// 声明exchange
// 参数:String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments
/**
* 参数明细
* 1、exchange: 交换器名称
* 2、type: 交换器类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
* 3、durable: 是否持久化,durable设置为true表示持久化,反之是非持久化,持久化的可以将交换器存盘,在服务器重启的时候不会丢失信息.
* 4、autoDelete是否自动删除,设置为TRUE则表是自动删除,自删除的前提是至少有一个队列或者交换器与这交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑,一般都设置为fase
* 5、internal: 是否内置,如果设置 为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器的方式
* 6、arguments: 其它一些结构化参数比如:alternate-exchange
*/
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);
将其中的第三个参数设置为true就可以将exchange 声明为持久化。
3、消息确认机制(confirm 模式)
上面提到的事务机制可以保证生产者发送消息时可以确认消息是否一定发送到了broker。除了事务机制外,rabbitmq还提供消息确认机制来实现这一个功能。rabbitmq 生产者和消费者都有消息确认机制。rabbitmq 消息确认机制流程示例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbonceOc-1622446734352)(https://i.loli.net/2021/05/28/1gQiF4jW7sd6xqo.png)]
producer端设置消息确认机制
producer 投递消息的流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EFX2cJ1P-1622446734354)(https://i.loli.net/2021/05/28/bIWgVuyoxpMTsz6.png)]
1、producer 将消息投递至 rabbitmq broker cluster
2、消息进入exchange
3、消息通过exchange转发进入queue
只有到消息进入到queue 后,才能确保消息完整的到达了server端,并且由于queue,消息的持久化设置,才能确保这条消息不丢失。
producer 的confirm 模式有三种:
1、普通模式(每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了)
普通confirm模式最简单,publish一条消息后,等待服务器端confirm,然后调用channel.waitForConfirms,如果返回false,则表示消息发送失败,然后客户端再进行消息重传。
//调用confirmSelect 将channel设置为confirm模式
channel.confirmSelect();
String msg="hello confirm!";
channel.basicPublish("",queueName,null,msg.getBytes());
if (!channel.waitForConfirms()){
System.out.println("消息发送失败!");
// 这里进行重试
}else {
System.out.println("消息发送成功!");
}
2、批量模式 (每发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm)
批量模式比普通模式稍微复杂,producer 发送多条消息后,再调用channel.waitForConfirms 等待server端的confirm。批量模式提高了confirm效率,但是当出现confirm返回false或者超时的情况时,客户端需要将这一批次的消息全部重发,这会导致消息重复。并且当消息经常丢失时,批量confirm性能应该是不升反降的。(值得注意:批量模式下,channel.waitForConfirms 返回false,代表这一批消息中,至少一个消息发送失败,但是可能存在发送成功的消息。所以这时候将这一批消息重新发送,会导致消息的重复。)
//调用confirmSelect 将channel设置为confirm模式
channel.confirmSelect();
//批量发送消息
for (int i = 0; i < 10; i++) {
String msg = "hello confirm! -" + i;
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
//等待确认
if (!channel.waitForConfirms()) {
System.out.println("消息发送失败!");
} else {
System.out.println("消息发送成功!");
}
3、异步模式(提供一个回调方法,服务端confirm了一条或者多条消息后Client端会回调这个方法)
异步模式比较复杂,我们需要自己维护一个unconfirm的消息集合,在发送每条消息的时候,同时将消息append到这个集合中。然后通过ConfirmListener监听server端的confirm。异步模式示例代码如下:
//调用confirmSelect 将channel设置为confirm模式
channel.confirmSelect();
//unconfirm的消息集合
final SortedSet<Long> unconfirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
//通道添加监听
channel.addConfirmListener(new ConfirmListener() {
//ack
@Override
public void handleAck(long deliverTag, boolean multiple) throws IOException {
if (multiple) {
unconfirmSet.headSet(deliverTag + 1).clear();
} else {
System.out.println("false");
unconfirmSet.remove(deliverTag);
}
}
// nack
@Override
public void handleNack(long deliverTag, boolean multiple) throws IOException {
if (multiple) {
unconfirmSet.headSet(deliverTag + 1).clear();
} else {
System.out.println("false");
unconfirmSet.remove(deliverTag);
}
}
});
String msg = "hello confirm——ack! ";
while (true) {
// 获取消息deliveryTag
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", queueName, null, msg.getBytes());
unconfirmSet.add(seqNo);
}
值得注意:
server 端不是每个消息都有ack/nack,server端执行的是批量回传给发送者ack消息,并且批量的大小不是固定的。这也就是上面代码示例中使用SortedSet的原因,当收到某个ack,并且mutiple 为true时,说明这个deliverTag之前(含当前deliverTag)的消息都已经ack,当mutiple为false时,仅代表当前deliverTag消息进行了ack。
consumer端设置消息确认机制
消费端确认机制,有autoAck 和手动ack两种模式。当设置autoAck时,消息从server端推送至consumer时,就会自动ack,server端就会将这些消息删除。因此采用autoAck不能避免消息的丢失。
手动ack是消费者依据自己的业务逻辑,进行消息的ack,因此可以控制何时发送ack,从而避免消息的丢失。手动ack 代码示例:
public void processByRoutingKey(String routingKey) throws Exception {
// 获取连接
Connection connection = ConnectFactory.getConnection();
// 获取channel
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(queueName, exchangeName, routingKey);
// 设置消费端消息缓冲区大小
channel.basicQos(30);
//实现消费方法
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
// 手动ack
// 参数:long deliveryTag, boolean multiple
/**
* 1、deliveryTag server端生成的消息唯一标识
* 2、multple true|false,为true 时,代表确认DeliverTag这个编号之前(含当前DeliverTag)的消息;为false时,代表仅确认当前DeliverTag消息;
*/
channel.basicAck(envelope.getDeliveryTag(), false)
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(queueName, false, consumer);
}}
关键代码:channel.basicConsume(queueName, false, consumer) 和 channel.basicAck(envelope.getDeliveryTag(), false)。
四、如何保证消息顺序消费
消息在进入queue后,由于queue的特性,消息这时候时天然具有顺序性的。因此对于如何需要保证消息顺序的消息而言,需要保证producer,consumer 两端的顺序。
生产者保证消息的顺序,可以采用confirm模式的基本模式,等收到前一条消息的ack之后再发送下一条消息。
而对于consumer来说。当一个queue对应多个consumer时,由于每个consumer的效率不一样,因此无法保证消息消费的顺序。因此对于需要保证顺序的消息来说,一个queue只能确保一个消费者。而为了保证消息处理效率,可以采用topic模式,将需要保证顺序的一块消息发送至同一个队列,但是多个消息块之间采用不同队列,从而提供消息的处理效率。另一种方法是,提高consumer的消费能力,利用多线程来处理消息。
五、如何解决重复消费问题
对于mq来说,消息的重复是无法避免的情况。要解决重复消息的问题,只能从consumer来处理。
重复消费问题,关键在于保证consumer 的幂等性。
六、集群部署
rabbitmq 可以采用集群部署来提供系统的可用性。rabbitmq有两种集群部署方式:普通模式,镜像模式。这里简单介绍两种模式的原理,不提供两种方式的部署步骤。
普通模式(结构同步,消息实体只存在一个节点中,但consumer在非消息节点获取是,节点间存在消息拉取,易产生性能瓶颈。)
1、集群会在所有实例上,同步queue,exchange 这些结构信息。
2、对于某个消息实体,只会存在于某一个实例。当消费者连接的是非消息实体所在的实例,集群需要临时从消息实体所在的实例同步至该实例,然后在推送至消费者。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AZ8qJHA7-1622446734355)(https://i.loli.net/2021/05/28/hLK6pHNAUyDeajb.jpg)]
普通模式优缺点:
优点:提高消费的吞吐量
缺点:并没有真的做到HA,如果存储消息实体的节点宕机,如果没有做持久化,这部分消息就丢失了。
镜像模式(集群中一个master,负责调度,处理消息实体,其他节点保存一份数据到本地;性能主要靠master承载)
镜像集群模式与普通镜像模式相比,区别在于:镜像模式除了会将queue,exchange这些结构信息同步至每个实例外,对于每个消息实体,也会在各个rabbitmq实例之间同步。因此消费者连接任意节点都一样。
镜像模式优缺点:
优点:不会因为某个节点宕机而丢失消息,真实做到HA
缺点:消息需要在每个实例间同步会导致网络带宽压力,性能开销比较大。另外没有扩展性,新增的机器同样包含了所有的数据,并没有办法线性扩展。
的消费能力,利用多线程来处理消息。
五、如何解决重复消费问题
对于mq来说,消息的重复是无法避免的情况。要解决重复消息的问题,只能从consumer来处理。
重复消费问题,关键在于保证consumer 的幂等性。
六、集群部署
rabbitmq 可以采用集群部署来提供系统的可用性。rabbitmq有两种集群部署方式:普通模式,镜像模式。这里简单介绍两种模式的原理,不提供两种方式的部署步骤。
普通模式(结构同步,消息实体只存在一个节点中,但consumer在非消息节点获取是,节点间存在消息拉取,易产生性能瓶颈。)
1、集群会在所有实例上,同步queue,exchange 这些结构信息。
2、对于某个消息实体,只会存在于某一个实例。当消费者连接的是非消息实体所在的实例,集群需要临时从消息实体所在的实例同步至该实例,然后在推送至消费者。
[外链图片转存中…(img-AZ8qJHA7-1622446734355)]
普通模式优缺点:
优点:提高消费的吞吐量
缺点:并没有真的做到HA,如果存储消息实体的节点宕机,如果没有做持久化,这部分消息就丢失了。
镜像模式(集群中一个master,负责调度,处理消息实体,其他节点保存一份数据到本地;性能主要靠master承载)
镜像集群模式与普通镜像模式相比,区别在于:镜像模式除了会将queue,exchange这些结构信息同步至每个实例外,对于每个消息实体,也会在各个rabbitmq实例之间同步。因此消费者连接任意节点都一样。
[外链图片转存中…(img-hh4Ldpk0-1622446734357)]
镜像模式优缺点:
优点:不会因为某个节点宕机而丢失消息,真实做到HA
缺点:消息需要在每个实例间同步会导致网络带宽压力,性能开销比较大。另外没有扩展性,新增的机器同样包含了所有的数据,并没有办法线性扩展。