文章目录
1. 如何确保消息正确地发送至RabbitMQ?
RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。
发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
2. 如何确保消息接收方消费了消息?
接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。
这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。
下面罗列几种特殊情况:
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
3. 如何避免消息重复投递或重复消费?
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
4. 消息基于什么传输?
由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。
5. 消息如何分发?
若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。
6. 消息怎么路由?
从概念上来说,消息路由必须有三部分:交换器、路由、绑定。
- 生产者把消息发布到交换器上;
- 消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定;
- 绑定决定了消息如何从路由器路由到特定的队列;
- 通过队列路由键,可以把队列绑定到交换器上。
- 消息最终到达队列,并被消费者接收。
消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入 “黑洞”。
常用的交换器主要分为一下三种:
- direct:如果路由键完全匹配,消息就被投递到相应的队列
- fanout:如果交换器收到消息,将会广播到所有绑定的队列上
- topic:可以使来自不同源头的消息能够到达同一个队列。 使用topic交换器时,可以使用通配符,比如:“*” 匹配特定位置的任意文本, “.” 把路由键分为了几部分,“#” 匹配所有规则等。特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是由"."隔开的一系列的标识符组成。
7. 如何确保消息不丢失?
消息持久化的前提是:将交换器/队列的durable属性设置为true,表示交换器/队列是持久交换器/队列,在服务器崩溃或重启之后不需要重新创建交换器/队列(交换器/队列会自动创建)。如果消息想要从Rabbit崩溃中恢复,那么消息必须:
- 在消息发布前,通过把它的 “投递模式” 选项设置为2(持久)来把消息标记成持久化
- 将消息发送到持久交换器,消息到达持久队列
RabbitMQ确保持久性消息能从服务器重启中恢复的方式是:
将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit会在消息提交到日志文件后才
发送响应(如果消息路由到了非持久队列,它会自动从持久化日志中移除)。一旦消费者从持久队列中消费了一条持久化消
息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前RabbitMQ重启,那么Rab
bit会自动重建交换器和队列(以及绑定),并重播持久化日志文件中的消息到合适的队列或者交换器上。
8. 使用RabbitMQ有什么好处?
- 应用解耦(系统拆分)
- 异步处理(预约挂号业务处理成功后,异步发送短信、推送消息、日志记录等)
- 消息分发
- 流量削峰
- 消息缓冲
…
9. 其他
RabbitMQ是 消息投递服务,在应用程序和服务器之间扮演路由器的角色,而应用程序或服务器可以发送和接收包裹。其通信方式是一种 “发后即忘(fire-and-forget)” 的单向方式。
其中消息包含两部分内容:有效载荷(payload)和标签(label)。
有效载荷是需要传输的数据,可以是任意内容。
标签描述了有效载荷,RabbitMQ会根据标签的描述,把消息发送给感兴趣的接收方。
10.RabbitMQ实现延时队列
1. 背景介绍
实际开发中,存在着下面这些场景:
- 滴滴打车订单完成后,如果用户一直不评价,48小时后自动五星好评
- 在电商系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,则这个订单会进行后续一些处理
- 用户希望通过手机远程遥控家里的智能设备在指定的时间后进行工作
…
针对这些场景,常见的方案是:启动一个cron定时任务,定时运行并查询符合时间条件的数据并进行处理。该方案存在以下几点不足:
- 轮询效率比较低
每次扫库,已经被执行过的记录,仍然会被扫描(不会出现在结果集中),有重复计算的嫌疑,若数据量过大对数据库也有压力 - 时效性不够,如果轮询时间间隔较长,时间误差比较大
若为了降低时间误差而提高轮询频率,则1、2问题更加凸显,显然这并不是一个明智之举,下面介绍通过延时队列实现。
2. 延时队列
延时队列存储的对象是对应的延时消息,所谓 延时消息 是指消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
Java提供delayedQueue可以实现本地的延时队列,但利用delayedQueue只能实现单机版,而且保存在内存中,需要在宕机时、消息消费异常时做相应的逻辑处理,非常麻烦。
3. 利用RabbitMQ实现延时队列功能
RabbitMQ本身没有直接支持延迟队列功能,但是可以通过RabbitMQ的两个特性来曲线实现延迟队列:Time To Live(TTL) 和 Dead Letter Exchanges(DLX),结合Time To Live(TTL) 和 Dead Letter Exchanges(DLX)两个特性,就可以模拟出延时消息的功能。
1. Time To Live(TTL):RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter。
RabbitMQ针对队列中的消息过期时间有两种方法可以设置:
- Per-Message TTL:通过队列属性设置,队列中所有消息都有相同的过期时间。
- Queue TTL:对消息进行单独设置,每条消息的TTL可以不同。
如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter。
Per-message ttl代码
// java client声明队列时,统一设置该队列中的消息过期时间
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);
// java client发送一条只能驻留60秒的消息到队列(设置单条消息过期时间)
byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000");
channel.basicPublish("my-exchange", "routing-key", properties, messageBodyBytes);
Queue ttl代码 收藏代码
// java client设置队列的过期时间
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-expires", 1800000);
channel.queueDeclare("myqueue", false, false, false, args);
2.Dead Letter Exchanges(DLX): RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由。
- x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
- x-dead-letter-routing-key:指定routing-key发送
队列出现dead letter的情况有:
- 消息或者队列的TTL过期
- 队列达到最大长度
- 消息被消费端拒绝(basic.reject or basic.nack)并且requeue=false
利用DLX,当消息在一个队列中变成死信后,它能被重新publish到另一个Exchange。这时候消息就可以重新被消费。
Dead letter exchanges代码 收藏代码
channel.exchangeDeclare("some.exchange.name", "direct");
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "some.exchange.name");
// args.put("x-dead-letter-routing-key", "some-routing-key");
channel.queueDeclare("myqueue", false, false, false, args);
4. 利用RabbitMQ实现延时队列功能
在rabbitmq 3.5.7及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时该插件依赖Erlang/OPT 18.0及以上。
插件安装及启用
- 进入插件安装目录:{rabbitmq-server}/plugins/
下载插件
wget https://bintray.com/rabbitmq/community-plugins/download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.ez - 启用/关闭插件
(启用插件)
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
(关闭插件)
rabbitmq-plugins disable rabbitmq_delayed_message_exchange - 插件使用:通过声明一个x-delayed-message类型的exchange来使用delayed-messaging特性(x-delayed-message是插件提供的类型,并不是rabbitmq本身的),发送消息的时候通过在header添加”x-delay”参数来控制消息的延时时间
// ... elided code ...
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-delayed-type", "direct");
channel.exchangeDeclare("my-exchange", "x-delayed-message", true, false, args);
// ... more code ...
// ... elided code ...
byte[] messageBodyBytes = "delayed payload".getBytes("UTF-8");
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("x-delay", 5000);
AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder().headers(headers);
channel.basicPublish("my-exchange", "", props.build(), messageBodyBytes);
// ... more code ...
插件使用示例:
消息接收端代码
import java.text.SimpleDateFormat;
import java.util.Date;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
public class Recv {
// 队列名称
private final static String QUEUE_NAME = "delay_queue";
private final static String EXCHANGE_NAME="delay_exchange";
public static void main(String[] argv) throws Exception,
java.lang.InterruptedException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.12.190");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.queueDeclare(QUEUE_NAME, true,false,false,null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
channel.basicConsume(QUEUE_NAME, true, queueingConsumer);
SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
System.out.println("****************WAIT***************");
while(true){
QueueingConsumer.Delivery delivery = queueingConsumer
.nextDelivery(); //
String message = (new String(delivery.getBody()));
System.out.println("message:"+message);
System.out.println("now:\t"+sf.format(new Date()));
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
消息发送端代码
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Send {
// 队列名称
private final static String EXCHANGE_NAME="delay_exchange";
private final static String ROUTING_KEY="key_delay";
@SuppressWarnings("deprecation")
public static void main(String[] argv) throws Exception {
/**
* 创建连接连接到MabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.12.190");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
// 声明x-delayed-type类型的exchange
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-delayed-type", "direct");
channel.exchangeDeclare(EXCHANGE_NAME, "x-delayed-message", true,
false, args);
Map<String, Object> headers = new HashMap<String, Object>();
//设置在2016/11/04,16:45:12向消费端推送本条消息
Date now = new Date();
Date timeToPublish = new Date("2016/11/04,16:45:12");
String readyToPushContent = "publish at " + sf.format(now)
+ " \t deliver at " + sf.format(timeToPublish);
headers.put("x-delay", timeToPublish.getTime() - now.getTime());
AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder()
.headers(headers);
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, props.build(),
readyToPushContent.getBytes());
// 关闭频道和连接
channel.close();
connection.close();
}
}
启动接收端,启动发送端,运行结果如下:
****************WAIT***************
message:publish at 2018-08-12 16:44:16.887 deliver at 2018-08-12 16:45:12.000
now: 2018-08-12 16:45:12.023
注意:使用rabbitmq-delayed-message-exchange插件时发送到队列的消息数量不可见,不影响正常功能使用。
注意:使用过程中发现,当一台启用了rabbitmq-delayed-message-exchange插件的RAM节点在重启的时候会无法启动,查看日志发现了一个Timeout异常,开发者解释说这是节点在启动过程会同步集群相关数据造成启动超时,并建议不要使用Ram节点。