简介
何为消息中间
利用高效可靠的消息传递机制,基于数据通信来进行分布式系统集成的程序(软件)。
消息传递模式
1.点对点模式
基于队列,消息生产者发送消息到队列,消息消费者从队列中接收消息
2.发布订阅模式
基于主题,消息发布者发送消息到主题,消息订阅者从主题中订阅消息。该模式在消息的一对多广播时采用
消息中间件作用
解耦
比如A系统在某个业务中要调用B/C/D系统的接口获取数据,后续如果还有E/F系统也需要提供数据给A,那么A系统还需要开发去调用E/F系统的接口,这种方案A系统和B/C/D/E/F系统耦合度高。采用消息中间件的方案,B/C/D/E/F系统各自将数据发送至队列中,A消费队列消息即可,达到解耦的目的。
异步
比如用户使用手机号+短信进行登录,主流程处理到发送短信步骤时候,通过发送消息给消息中间件,由队列消费者去发短信给用户,主流程在消息发给消息中间件后立即返回给用户短信发送成功,用户等待接收到的短信进行登录。异步对于提高用户体验非常有用。
削峰
如果一台机器只能承受100并发,突然哪天瞬时出现了流量高峰,增加到1万并发,那么一台机器承受不住,可以新增10台机器来承担。如果并发到1万的时间可能只集中在周末的上午,那么其它时间机器资源是浪费的。此时可以用MQ来进行流量削峰,当出现流量高峰的时候,消息可以积压在MQ里面,然后那一台机器慢慢的消费,等高峰期过了,再消费一段时间,积压的消息就消费完了,恢复到了平时流量状态
存储
某些情况下,数据会丢失,消息中间件可以把数据进行持久化直到他们被处理
RabbitMQ特点
采用Erlang实现AMQP协议的消息中间件,具有可靠性(持久化、发布确认、消费确认)、灵活路由、支持多语言客户端、提供管理界面
安装
略
RabbitMQ组件
模型架构
生产者producer
发送消息
消息包含消息标签和消息体,消息标签指的是交换器和路由键,请注意不同的交换器类型不一定需要路由键
路由键routingkey
指定消息发送到哪个队列,生产者将消息发送给交换器的的时候一般会指定一个路由键
服务节点broker
一个RabbitMQ实例
交换器exchange
从生产者接受消息并投递消息到队列。交换器分为direct、topic、fanout、header4种类型:
direct交换器:路由键必须与绑定键一致才能投递到队列
topic交换器:路由键必须符合绑定键的模糊匹配或者精确匹配才能投递到队列
fanout交换器:该类型路由键、绑定键均无效。将消息投递到与交换器连接的所有队列
header交换器:该类型路由键、绑定键均无效。将消息投递到与消息内容中headers属性匹配的队列
队列queue
消息的容器,多个消费者可以订阅同一个队列,队列的消息会被轮询给各个消费者处理
绑定键bindkey
关联交换器和队列,在绑定时候一般会指定一个绑定键
消费者consumer
订阅队列并消费消息,这里的消息指的是消息体,消息标签已经在路由的过程被丢弃
运转过程
RabbitMQ基本使用
客户端依赖包
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.2.1</version>
</dependency>
创建连接和通道
RabbitMQ的连接工厂全局有一个实例就可以,这里采用了单例模式实现全局唯一实例。通道可以复用同一个连接,避免了连接频繁的创建和释放。但是,如果从相同连接出来的通道的流量大最终还是会受到一个连接资源的限制。
创建连接和通道代码如下
public class RabbitMQUtil {
private enum RabbitMQFactory{
C_FACTORY;
private ConnectionFactory connectionFactory;
RabbitMQFactory(){
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("47.114.41.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("app");
connectionFactory.setPassword("#EDFVRFVBTG");
}
public ConnectionFactory getConnectionFactory() {
return connectionFactory;
}
}
/**
* 从连接工厂获取新连接
* @return
* @throws IOException
* @throws TimeoutException
*/
public static Connection getConnection() throws IOException, TimeoutException {
return RabbitMQFactory.C_FACTORY.getConnectionFactory().newConnection();
}
/**
* 从新连接获取新通道
* @return
* @throws IOException
* @throws TimeoutException
*/
public static Channel getChannel() throws IOException, TimeoutException {
return getConnection().createChannel();
}
/**
* 从指定连接获取新通道
* @param connection
* @return
* @throws IOException
* @throws TimeoutException
*/
public static Channel getChannel(Connection connection) throws IOException, TimeoutException {
return connection.createChannel();
}
}
常用API
交换器
1.创建交换器
Exchange.DeclareOk exchangeDeclare(String exchange,
String type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments) throws IOException;
参数说明:
exchange-交换器名称
type-交换器类型,4种类型的一种
durable-是否持久化,如果设置为true则重启的时候该交换器不会丢失
autoDelete-是否自动删除,至少有一个队列或者交换器与这个交换器绑定的前提下,之后所有与这个交换器绑定的队列或交换器都与此解绑,如果设置为true则会自动删除
internal-是否为内置交换器,客户端无法直接发送消息到该交换器,只能通过交换器路由到交换器的方式
arguments-其它结构化参数
void exchangeDeclareNoWait(String exchange,
String type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments) throws IOException;
此方法与上述exchangeDeclare方法唯一不同是异步方法。通常不建议使用,因为如果在异步未创建完成客户端就是用该交换器会发生异常。
2.判断交换器是否存在
Exchange.DeclareOk exchangeDeclarePassive(String name) throws IOException;
参数说明:
name-交换器名称
3.删除交换器
#直接删除交换器
Exchange.DeleteOk exchangeDelete(String exchange) throws IOException;
#ifUnused=true,则在交换器未被使用的情况才会删除;否则,直接删除交换器
Exchange.DeleteOk exchangeDelete(String exchange, boolean ifUnused) throws IOException;
#与上同,不过此方法是异步的
void exchangeDeleteNoWait(String exchange, boolean ifUnused) throws IOException;
4.绑定/解绑交换器
Exchange.BindOk exchangeBind(String destination,
String source,
String routingKey,
Map<String, Object> arguments) throws IOException;
参数说明:
destination-目标交换器名称
source-源交换器名称
routingKey-绑定键
arguments-其它结构化参数
Exchange.BindOk exchangeBindNoWait(String destination,
String source,
String routingKey,
Map<String, Object> arguments) throws IOException;
此方法上述queueBind唯一不同是异步方法
以上述绑定方法对应的解绑方法:
Exchange.UnbindOk exchangeUnbind(String destination,
String source,
String routingKey,
Map<String, Object> arguments) throws IOException;
Exchange.UnbindOk exchangeUnbindNoWait(String destination,
String source,
String routingKey,
Map<String, Object> arguments) throws IOException;
队列
1.创建队列
Queue.DeclareOk queueDeclare(String queue,
boolean durable,
boolean exclusive,
boolean autoDelete,
Map<String, Object> arguments) throws IOException;
参数说明:
queue-队列名称
durable-是否持久化,如果设置为true则重启的时候该队列不会丢失
autoDelete-是否自动删除,至少有一个消费者连接到这个队列的前提下,之后所有与这个队列连接的消费者都断开,如果设置为true则会自动删除
exclusive-是否排他,如果为ture,则仅该可以使用该队列,其它的不行(其它连接不能创建,使用...,但是该连接的通道是可以使用的);且如果连接断开该队列会自动删除。
arguments-其它结构化参数
Queue.DeclareOk queueDeclareNoWait(String queue,
boolean durable,
boolean exclusive,
boolean autoDelete,
Map<String, Object> arguments) throws IOException;
此方法与上述queueDeclare方法唯一不同是异步方法。通常不建议使用,因为如果在异步未创建完成客户端就是用该队列有可能发生异常。
2.判断队列是否存在
Queue.DeclareOk queueDeclarePassive(String queue) throws IOException;
参数说明:
name-队列名称
3.删除队列
#直接删除队列
Queue.DeleteOk queueDelete(String queue) throws IOException;
#ifUnused=true,则在删除的时候需要满足队列未在使用;ifEmpty=true,则在删除的时候要满足队列为空
Queue.DeleteOk queueDelete(String queue, boolean ifUnused, boolean ifEmpty) throws IOException;
#与上同,不过此方法是异步的
void queueDeleteNoWait(String queue, boolean ifUnused, boolean ifEmpty) throws IOException;
4.清空队列
Queue.PurgeOk queuePurge(String queue) throws IOException;
5.绑定/解绑交换器
Queue.BindOk queueBind(String queue,
String exchange,
String routingKey,
Map<String, Object> arguments) throws IOException;
参数说明:
queue-队列名称
exchange-要绑定的交换器名称
routingKey-绑定键
arguments-其它结构化参数
Queue.BindOk queueBindNoWait(String queue,
String exchange,
String routingKey,
Map<String, Object> arguments) throws IOException;
此方法上述queueBind唯一不同是异步方法
与上述绑定方法对应的解绑方法:
Queue.UnbindOk queueUnbind(String queue,
String exchange,
String routingKey,
Map<String, Object> arguments) throws IOException;
发送消息
public void basicPublish(String exchange,
String routingKey,
boolean mandatory,
boolean immediate,
AMQP.BasicProperties props,
byte[] body) throws IOException {
...
}
参数说明:
exchange-交换器名称
routingKey-路由键
mandatory-指定消息无法路由到队列时候的策略。如果为true则会返回生产者;false则丢弃消息
immediate-指定队列无消费者时候是否投递消息。如果为true且队列有消费者,则投递消息,如果无消费者则返回生产者;------请注意3.0版本该参数已经不再支持
props-消息基本属性
body-消息体
消费消息
消费消息有两种模式
1.推模式push
服务节点会持续向消费者传递消息,同一队列有多个消费者会轮询推送传递消息。
public String basicConsume(String queue,
boolean autoAck,
String consumerTag,
boolean noLocal,
boolean exclusive,
Map<String, Object> arguments,
Consumer callback) throws IOException {
...
}
参数说明:
queue-队列名称
autoAck-是否自动确认,消费者接收到后立刻向服务节点确认
consumerTag-消费者标签,用于区分不同消费者
noLocal-true表示不能将同一个连接生产者发送的消息传递给这个连接中的消费者
exclusive-是否排他
arguments-其他参数
callback-回调函数,处理服务节点推送过来的消息
autoAck=false的时候,消费者关闭了自动确认机制,消费者需要在消费了消息的时候显式的调用确认方法
void basicAck(long deliveryTag, boolean multiple) throws IOException;
参数说明:
deliveryTag-消息编号
multiple-true表示确认deliveryTag之前(包括deliveryTag在内的)消息;false表示仅确认当前消息
试想下,autoAck=false的时候,如果消费了但是没有确认呢?或者调用了确认方法,但是网络原因服务节点没有接受到确认? 在这些情况下,RabbitMQ服务节点在一直未收到消费者的确认信号并且消费此消息的消费者已经断开连接,则服务节点会安排消息重新进入队列。
autoAck=false的时候,消费者除了可调用确认方法确认消息,还可以拒绝消息,方法如下
#拒绝一条消息
public void basicReject(long deliveryTag, boolean requeue) throws IOException {
...
}
参数说明:
deliveryTag-消息编号
requeue-true表示服务节点会重新将这条消息存入队列;false表示服务节点会立即会把消息从队列中移除
#批量拒绝消息
public void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException {
...
}
参数说明:
deliveryTag-消息编号
multiple-true表示拒绝消息编号之前未被当前消费者确认的消息;false则该方法效果与basicReject方法一样
requeue-true表示服务节点会重新将这条消息存入队列;false表示服务节点会立即会把消息从队列中移除
autoAck=false的时候,消费者除了可调用确认方法确认消息,调用拒绝方法拒绝消息,还可以调用恢复方法恢复消息,方法如下
#请求服务节点重新将未被确认的消息加入队列,对于同一条消息可能会被分配给与之前不同的消费者
Basic.RecoverOk basicRecover() throws IOException;
#requeue=true效果与上一方法一样;false表示请求服务节点重新将未被确认的消息加入队列,对于同一条消息要求分配给与之前相通的消费者
Basic.RecoverOk basicRecover(boolean requeue) throws IOException;
关闭自动确认机制,服务节点推送消息的个数会受到以下API的限制(个数指服务节点已经传递但是没有接收到消费者确认的消息个数)
#限制单个消费者上限
void basicQos(int prefetchCount) throws IOException;
#global=false效果同上;global=true则限制通道上所有消费者总数上线
void basicQos(int prefetchCount, boolean global) throws IOException;
#maximum amount of content that the server will deliver, 0 if unlimited
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
2.拉模式pull
GetResponse basicGet(String queue, boolean autoAck) throws IOException;
参数说明:
queue-队列名称
autoAck-是否自动确认,消费者接收到后立刻向服务节点确认
推模式属于持续订阅,服务节点会持续的将消息投递到消费者。拉模式一次只会消费一个消息。如果想从服务节点获取单条消息,建议使用pull模式;如果想持续订阅,建议使用推模式,不建议通过pull模式+循环的方式达到持续订阅的效果。
RabbitMQ进阶使用
生产者确认机制
确保生产者能将消息发送到了RabbitMQ服务节点的交换器。如果发送到了交换器则返给生产者ack通知;否则返回生产者nack通知。
public class T7 {
Logger logger = LoggerFactory.getLogger(T7.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare("exchange_8", "direct", true, false, false, null);
channel.queueDeclare("queue_8", true, false, false, null);
channel.queueBind("queue_8", "exchange_8", "q8_e8");
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
logger.info("ACK deliveryTag={}, multiple={}", deliveryTag, multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
logger.info("NACK deliveryTag={}, multiple={}", deliveryTag, multiple);
}
});
String message = "hello world";
for (int i=0; i<10; i++){
message += "-"+i;
channel.basicPublish("exchange_8", "q8_e8", false, null, message.getBytes());
}
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("主线程有监听功能不能停哦,否则无法监听了");
}
}
}
mandatory
把视线拉回上一部分的发送消息的函数basicPublish,其中有一个mandatory参数。规则:如果为false,在无法将消息路由到队列(交换器存在且无法匹配到队列)则丢弃消息,客户端不报错;如果为true,在无法将消息路由到队列(交换器存在且无法匹配到队列)则返回给生产者。在发布消息时候指定不存在的交换器,无论该值是什么都会丢失消息,客户端不报错。
为了方便演示,提前建立好交换器exchange_1
1.验证-如果为false,在无法将消息路由到队列(交换器存在且无法匹配到队列)则丢弃消息,客户端不报错;
public class T1 {
Logger logger = LoggerFactory.getLogger(T1.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
channel.basicPublish("exchange_1", "q1_e1", false, null, message.getBytes());
}
while (true){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("主线程有监听功能不能停哦,否则无法监听了");
}
}
}
2.如果为true,在无法将消息路由到队列(交换器存在且无法匹配到队列)则返回给生产者。生产者需要添加返回监听器,否则也会忽略返回的消息。
public class T1 {
Logger logger = LoggerFactory.getLogger(T1.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
logger.info("监听到返回,replyCode={},replyText={},exchange={},routingKey={},properties={},body={}",
replyCode, replyText, exchange, routingKey, properties, new String(body));
}
});
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
channel.basicPublish("exchange_1", "q1_e1", true, null, message.getBytes());
}
while (true){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("主线程有监听功能不能停哦,否则无法监听了");
}
}
}
3.验证-在发布消息时候指定不存在的交换器,无论该值是什么都会丢失消息,客户端不报错。
将上述两个例子的交换器exchange_1替换成不存在的交换器名称exchange_2,运行时候消息丢失,客户端也不报错。
备份交换器
通过上述描述已经了解了mandatory参数的作用,但是为true的时候客户端需要编写监听器去处理返回的消息。备份交换器可以替代这个方案,备份交换器是一个普通的交换器,可以有关联的队列,消费者只需要消费这个队列的消息做业务处理。其它交换器声明的时候需要设置属性alternate-exchange指定一个备份交换器,如果消息无法路由到队列则会路由到备份交换器,再通过绑定键路由到备份交换器的队列。另外,特别注意重新发送到备份交换器的路由键和从生产者发出的路由键是一样的。
public class T2 {
Logger logger = LoggerFactory.getLogger(T2.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//创建备份交换器和队列
channel.exchangeDeclare("exchange_back1", "direct", true, false, true,null);
channel.queueDeclare("queue_back1", true, false, false, null);
channel.queueBind("queue_back1", "exchange_back1", "qb1_eb1");
//申明交换器exchange_1并指定备份交换器exchange_back1
Map<String, Object> args = new HashMap<>();
args.put("alternate-exchange", "exchange_back1");
channel.exchangeDeclare("exchange_1", "direct", true, false, false, args);
channel.queueDeclare("queue_1", true, false, false, null);
channel.queueBind("queue_1", "exchange_1", "q1_e1");
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
//向交换器发送消息,由于没有指定队列,所以会启动备份交换器
channel.basicPublish("exchange_1", "qb1_eb1", false, null, message.getBytes());
}
}
}
运行上述代码,发现消息最终存在在queue_back1队列中
注意:
1.备份交换器不存在或备份交换器未绑定队列或备份交换器没有任何匹配的队列,客户端和服务节点都不会异常,消息丢失
2.备份交换器通常设置为fanout类型,否则可能会因为绑定键和路由键不同导致消息丢失
3.如果mandatory参数和备份交换器一起使用则mandatory参数无效
immediate
略,3.0版本已不再支持。可以使用TTL+DLX实现该参数效果。
过期时间
过期时间的对象可以是消息也可以是队列本身
消息过期时间
1.申明队列的时候设置队列参数x-message-ttl(单位ms),队列中的所有消息都遵从这个过期时间。该方式如果消息过期,就会从队列中抹去。
public class T3 {
Logger logger = LoggerFactory.getLogger(T3.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare("exchange_2", "direct", true, false, false, null);
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 30000);
channel.queueDeclare("queue_2", true, false, false, args);
channel.queueBind("queue_2", "exchange_2", "q2_e2");
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
//向交换器发送消息,由于没有指定队列,所以会启动备份交换器
channel.basicPublish("exchange_2", "q2_e2", false, null, message.getBytes());
}
}
}
2.发送消息的时候设置消息过期属性。如果消息过期,不会马上从队列抹去,在即将投递到消费者之前判定并抹去。
public class T4 {
Logger logger = LoggerFactory.getLogger(T3.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare("exchange_3", "direct", true, false, false, null);
channel.queueDeclare("queue_3", true, false, false, null);
channel.queueBind("queue_3", "exchange_3", "q3_e3");
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
//向交换器发送消息,由于没有指定队列,所以会启动备份交换器
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.expiration("60000") //设置消息的有效期为60s
.deliveryMode(2) //设置消息持久化
.build();
channel.basicPublish("exchange_3", "q3_e3", false, properties, message.getBytes());
}
}
}
如果同时设置了队列属性x-message-ttl和消息属性,过期时间以时间短的为准;
如果ttl=0,表示除非消息可以直接投递到消费者,否则消息被丢弃;
如果ttl未设置,则无过期时间
队列过期时间
声明队列时候设置x-expires(单位ms,不能为0)参数实现, 该参数指定在过期时间内队列上没有任何消费者、没有调用过Basic.Get命令、队列也没有被重新声明过,则会删除队列。
public class T5 {
Logger logger = LoggerFactory.getLogger(T5.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare("exchange_5", "direct", true, false, false, null);
Map<String, Object> args = new HashMap<>();
args.put("x-expires", 30000);
channel.queueDeclare("queue_5", true, false, false, args);
channel.queueBind("queue_5", "exchange_5", "q5_e5");
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
//向交换器发送消息,由于没有指定队列,所以会启动备份交换器
channel.basicPublish("exchange_5", "q5_e5", false, null, message.getBytes());
}
}
}
死信队列
当消息在队列中变成死信后,能被重新发送到死信交换器,与死信交换器绑定的队列为死信 队列。
消息在哪些情况下会变成死信呢?
- 消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false
- 消息过期
- 队列达到最大长度
如何启用死信队列呢?
死信交换器是一个普通交换器,只不过在其它队列申明的时候通过参数x-dead-letter-exchange参数为队列添加死信交换器,x-dead-letter-routing-key参数设置消息发到死信交换器的路由键,如果未指定x-dead-letter-routing-key则使用原队列的路由键(绑定键)。
public class T6 {
Logger logger = LoggerFactory.getLogger(T6.class);
@Test
public void p() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
channel.exchangeDeclare("exchange_dl6", "direct", true, false, false, null);
channel.queueDeclare("queue_dl6", true, false, false, null);
channel.queueBind("queue_dl6", "exchange_dl6", "qdl6_edl6");
channel.exchangeDeclare("exchange_6", "fanout", true, false, false, null);
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 30000);
args.put("x-dead-letter-exchange","exchange_dl6");//设置死信交换器
args.put("x-dead-letter-routing-key","qdl6_edl6");//设置消息被发送到死信交换器时候的路由键
channel.queueDeclare("queue_6", true, false, false, args);
channel.queueBind("queue_6", "exchange_6", "");
String message = "hello world";
for (int i = 0; i < 10; i++) {
message += "-"+i;
//向交换器发送消息,由于没有指定队列,所以会启动备份交换器
channel.basicPublish("exchange_6", "", false, null, message.getBytes());
}
}
}
死信交换器可以通过不同的绑定键关联不同的队列,其它队列设置私信交换器的时候可以指定不同的路由键路由到不同的死信队列。
如何实现immediate的效果?
通过以上TTL和DLX的特性,可知TTL设置为0,并且为队列设置DLX,就可以使用immediate的作用。
持久化
RabbitMQ的持久化分为三个部分:交换器持久化、队列持久化、消息持久化
怎样开启持久化?
持久化设置在前述的常用API有对应的参数。设置了持久化则在重启实例不会丢失,但是消息设置了持久化、从队列未设置持久化,那么重启之后消息会丢失,因为队列是消息的容器,容器不存在了,消息也就不存在了,因此单独设置消息持久化是没有意义的。
开启持久化还会丢失消息嘛?
如果消费者开启了自动确认机制,那么消费者在接收到消息后,由于消费者宕机了会导致消息未被消费,但是RabbitMQ已经删除了消息,最终导致消息丢失
消息分发
RabbitMQ的队列通过轮询的的分发范式发送给消费者,每条消息只会发给订阅列表里的一个消费者。默认情况下,如果有n个消费者,那么RabbitMQ会将第m条消息分发给第m%n(取余)个消费者,RabbitMQ不管消费者是否消费并已经确认了消息。
消息分发的模式会引发什么问题呢?
如果某些消费者消费慢,还一直给该消费者发送消息,会引起吞吐量下降;并且如果某些消费者消费快,可能导致进程空闲,也会引起吞吐量下降。
RabbitMQ如何处理这种情况?
通过Basic.Qos方法限制未确认的消息个数,当达到限制的时候,RabbitMQ则不会推送消息到受限制的消费者,当受限制的消费者未确认个数小于指定个数的时候,又会恢复推送消息。详细API见“常用API”部分
消息顺序性
消息顺序性是指消费者消费到的消息和生产者发布的消息是顺序一致的。
RabbitMQ不能保证消息顺序性。如下情形会打破消息顺序性:
-
生产者发布的消息设置了不通的过期时间并且设置了死信队列,整体来说相当于一个延迟队列,那么消费者消费这个延迟队列的时候,消息的顺序必然不会和生产者发送消息的顺序一致。
-
优先级队列中消息设置了不通优先级,那么消费的顺序和生产者发送消息的顺序也是不一致。
-
普通队列轮询,消费者A接收到消息msg1、msg3,消费者B接收到消息msg2、msg4,A先接收到msg1, 但是调用了Basic.Reject、Basic.Nack拒绝并重新入队列或者Basic.recover重新入队列,然后A正常消费了msg2, B消费了msg2、msg4, 重回队列的msg1可能发给A,则A先消费msg3,再消费msg1;重回队列的msg1可能发给B,则B先消费了msg2、msg4,再消费msg1。从上可知也是没有顺序性的
没有消息顺序性的情况包括但是不限以上描述的情形。RabbitMQ要保证消息的顺序性一般可以为每个队列只分配一个消费者来简单实现。
RabbitMQ消息可靠性
从模型结构图可以看到,从消息诞生到被消费经历了四个步骤
1.生产者发布消息到RabbitMQ的交换器中
2.交换器根据路由键将消息投递到队列中
3.队列将消息推送到订阅该队列的消费者
4.消费者消费消息
针对以上步骤保证消息可靠性需要以下几方面
1.生产者开启publish confirm消息确认机制,确保消息传输到RabbitMQ的交换器
2.生产者开启mandatory或者设置备份交换器,确保消息能路由到队列中,不会被丢弃
3.队列和消息开启持久化,确保RabbitMQ服务节点异常的时候不会造成消息丢失
4.消费者关闭自动确认机制(autoAck设置为false),改为手动确认去确认已经正确消费的消息,避免在消费端引起消息丢失
怎样做到100%投递呢?
上述publish confirm消息确认机制,可能出现两种情况:RabbitMQ有返回给监听器,监听器判断是否投递成功,不成功可以重新投递等;另一种由于网络无法返回给监听器,监听器无法做操作。如果由于网络问题,监听器监听不到RabbitMQ返回的消息,那么消息可能投递成功,也可能投递失败,如果投递失败岂不是丢失了消息吗?如何避免该问题呢?
通过消息落库,确认后修改消息状态,对于未确认状态的消息采用定时任务轮询重新发布消息,流程如下图所示:
以上方案可以保证消息100%投递,但是有一个明显的劣势:在发布到RabbitMQ之前多次操作了数据库,在高并发场景下,数据库会是性能的瓶颈。如何解决操作数据库的问题呢?是否可以使用缓存来减少或者替代数据库操作达到提高性能的目的呢?
另外,消息可能会重复发送,比如由于网络原因step3无法返回,定时任务轮询时候发现status=0,则重新发布消息,但是step3无返回,消息有可能成功投递到了队列。因此,造成了消息重复发送。
怎样做到消费不被重复消费呢?
试想下以下两种情形
1.生产者重复发送了消息
2.当消费者消费了消息并且发送Basic.Ack给RabbitMQ服务节点,但是服务节点无法获取到的时候,连接断开了,RabbitMQ会将消息重新入队列。队列轮询投递到该消息的时候就属于第二次消费了
在保证消费端消息不丢失的情况下,通过幂等性来实现队列中的消息只能被消费一次。幂等性表示多次一样的操作,都和一次的效果是一样的。幂等性可以基于数据库乐观锁、分布式锁的来实现