消息队列可以看作是一个存放消息的容器,其中,生产者负责生产数据到消息队列中,而消费者负责消费数据。消息队列是分布式系统中重要的组件,目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ。
消息队列主要解决了应用耦合、异步处理、流量削锋等问题。
RabbitMQ 是一款使用Erlang语言开发的,实现AMQP(高级消息队列协议)的开源消息中间件,它实现了高效、可靠、可扩展的消息传递机制。以下是 RabbitMQ 的一些主要特点:
如图所示,RabbitMQ的工作流程分为以下几个部分
在RabbitMQ中,消息发送方不是直接将消息发送到queue中,而是先发送给exchange,由exchange再发送给对应的队列。exchange的类型有:**direct
, topic
, headers
, fanout
。**默认的exchange,名为"",是一个没有名字的direct类型的exchange。发送到它的消息是基于路由键(routing key)路由到队列的。
direct,需要将一个队列绑定到交换机上,要求该消息与一个特定的routing key完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求routing key为 “green”,则只有routing key为“green”的消息才被转发,不会转发routing key为"red",只会转发routing key为"green”
可以理解为通配符匹配,将routing key和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配0个或多个词,符号“*”只能匹配一个词。
Fanout 不需要指定routing key。你只需要简单的将队列绑定到交换机上。一个发送到该类型交换机的消息都会被广播到与该交换机绑定的所有队列上。
1.rabbitMQ的消息持久化的做法不是每接受一条消息就立即调用fsync,而是对接收到的消息进行缓存,接着再批量进行持久化。
3.rabbitMQ中的**AnonymousQueue
** 默认是非持久化、自动删除的队列。
当消费者连接断开时,AnonymousQueue会被自动删除,队列中的消息也会被删除,不会持久化。
不处理routing key,而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型。而fanout,direct,topic 的路由键都需要要字符串形式的
首先在application.yml中配置RabbitMQ信息
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
接着添加配置类,在配置类中实现相关bean的注入
@Configuration
public class DirectRabbitConfig {
@Bean
public Queue rabbitmqDemoDirectQueue() {
return new AnonymousQueue();
/**
* 等价于 return new Queue("direct_demo",false,false,false);
* 1、name: 队列名称
* 2、durable: 是否持久化
* 3、exclusive: 是否独享、排外的。如果设置为true,定义为排他队列。则只有创建者可以使用此队列。也就是private私有的。
* 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
* */
}
@Bean
public DirectExchange rabbitmqDemoDirectExchange() {
//Direct交换机
return new DirectExchange("demo_exchange", true, false);
}
@Bean
public Binding bindDirect() {
//链式写法,将队列和交换机进行绑定,并设置匹配键
return BindingBuilder
//绑定队列
.bind(rabbitmqDemoDirectQueue())
//到交换机
.to(rabbitmqDemoDirectExchange())
//并设置匹配键
.with("demo");
}
@Bean
public Sender sender() {
//生产者类
return new Sender();
}
@Bean
public Receiver receiver() {
//消费者类
return new Receiver();
}
}
接下来,创建发送消息的Sender类和消费消息的Receiver类
/**
* 生产者类
*/
public class Sender {
@Autowired
private RabbitTemplate template;
@Autowired
private DirectExchange direct;
private final String MESSAGE = "Hello world";
private final String key = "demo";
@Scheduled(fixedDelay = 1000, initialDelay = 500)
public void send() {
template.convertAndSend(direct.getName(), key, MESSAGE);
System.out.println("sending message: " + MESSAGE);
}
}
/**
* 消费者类
*/
@RabbitListener(queues = "#{rabbitmqDemoDirectQueue.name}")
public class Receiver {
@RabbitHandler
public void receive(String msg) {
System.out.println("receive message: " + msg);
}
}
最后,创建启动类,并加上@EnableScheduling
注解
@SpringBootApplication
@EnableScheduling
public class RabbitDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RabbitAmqpTutorialsApplication.class, args);
}
}
打开http://localhost:15672,可以看到RabbitMQ消息发送情况等信息
一般情况下,RabbitMQ中的broker向消费者发送一条消息后,便立即将该消息标记为删除。由于消费者处理一个消息可能需要一段时间,假如在处理消息中途消费者挂掉了,我们会丢失其正在处理的消息以及后续发送给该消费这的消息。为了保证消费者消费的可靠性,RabbitMQ 引入消息应答机制,即:**消费者在接收消息并且处理完该消息之后,才告知 RabbitMQ 可以把该消息删除了。**RabbitMQ 中消息应答方式有两种:自动应答(默认)、手动应答。
自动应答是RabbitMQ默认采用的消息应答方式,是指broker不在乎消费者对消息处理是否成功,都会告诉队列删除消息。如果处理消息失败,又没有捕获异常,则会实现自动补偿(队列重新向消费者投递消息);如果捕获异常了,broker以为消息消费成功,就会将消息从队列中删除,导致数据丢失。
手动应答,是指消费者处理完业务逻辑之后,手动返回ack(通知)告诉broker消息处理完了,你可以删除消息了;或者手动返回nack消息,告诉broker消息处理失败,别删除消息。如果消费者挂了而没有发送ACK或NACK,那么RabbitMQ会认为该消息未被处理,会将该消息重新分发给其他消费者,直到有一个消费者成功处理并发送ACK或NACK为止。这个过程被称为消息的重新入队列。在重新入队列的过程中,RabbitMQ会在消息的属性中增加一个计数器,表示该消息被重新分发的次数。如果该计数器的值超过了一个预定的阈值,那么RabbitMQ可能会将该消息标记为“死信”,并将其发送到一个指定的死信交换机(dead letter exchange)中。这个机制可以避免消息在系统中无限制地被重新分发,从而引起系统性能问题。
在springboot的application.yml中可以设置RabbitMQ的应答机制
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
消费者
@RabbitListener(queues = "#{Queue1.name}")
public void onMessage1(Message message, Channel channel) throws Exception {
try {
MessageProperties messageProperties = message.getMessageProperties();
byte[] body = message.getBody();
String content = new String(body, 0, body.length, messageProperties.getContentEncoding());
System.out.println("receiver1: " + content);
channel.basicAck(messageProperties.getDeliveryTag(), true);
} catch (Exception e) {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它会清空队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(String message) {
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 设置消息的持久化属性
Message rabbitMessage = new Message(message.getBytes(), properties);
rabbitTemplate.convertAndSend("exchangeName", "routingKey", rabbitMessage);
}
RabbitMQ 默认分发消息采用的轮询分发模式,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中 consumer01 处理任务的速度非常快,而 consumer02 处理速度却很慢,此时如果我们还是采用轮询分发,就会使处理速度快的 consumer01 很大一部分时间处于空闲状态,而 consumer02 一直在干活。
可以通过设置channel的prefetchCount
为1,来实现不公平分发。该参数表示,该消费者当前只能处理一个消息。
channel.basicQos(1);
或者application.yml中设置
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=1
该值定义channel上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在channel上传递更多消息,除非至少有一个未处理的消息被确认。假设在channel上有未确认的消息 5、6、7,8,并且channel的预取计数设置为 4,此时 RabbitMQ 将不会在该channel上再传递任何消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 的消息刚刚被确认 ACK,RabbitMQ 将会感知这个情况到并再发送一条消息。对于自动确认机制,其预取值可以看做是无限。
生产者将channel设置成 confirm 模式,一旦channel进入 confirm 模式,所有在该channel上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等channel返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
发布确认机制有三种策略:单个确认发布、批量确认发布、异步确认发布。
是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,
要开启发布确认,需要配置application.yml
spring:
#项目名称
application:
name: rabbitmq-provider
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: rabbitMQ
password: rabbitMQ
#确认消息已发送到交换机(Exchange)
# publisher-confirm-type: SIMPLE
publisher-confirm-type: CORRELATED
#确认消息已发送到队列(Queue)
publisher-returns: true
然后,在rabbitTemplate中进行设置
@Slf4j
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
// Mandatory为true时,消息通过交换器无法匹配到队列会返回给生产者,为false时匹配不到会直接被丢弃
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* ConfirmCallback机制只确认消息是否到达exchange(交换器),不保证消息可以路由到正确的queue;
* 需要设置:publisher-confirm-type: CORRELATED;
* springboot版本较低 参数设置改成:publisher-confirms: true
*
* 以实现方法confirm中ack属性为标准,true到达
* config : 需要开启rabbitmq得ack publisher-confirm-type
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("ConfirmCallback 确认结果 (true代表发送成功) : {} 消息唯一标识 : {} 失败原因 :{}",ack,correlationData,cause);
}
});
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* ReturnsCallback 消息机制用于处理一个不可路由的消息。在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定路由 key 路由不到,这个时候我们需要监听这种不可达的消息
* 就需要这种return机制
*
* config : 需要开启rabbitmq发送失败回退; publisher-returns 或rabbitTemplate.setMandatory(true); 设置为true
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
// 实现接口ReturnCallback,重写 returnedMessage() 方法,
// 方法有五个参数
// message(消息体)、
// replyCode(响应code)、
// replyText(响应内容)、
// exchange(交换机)、
// routingKey(队列)。
log.info("ReturnsCallback returned : {}",returned);
}
});
return rabbitTemplate;
}
}
死信就是无法被消费的消息。一般来说,producer 将消息投递到 broker 或者直接到 queue 中,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
死信产生的原因:
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。一条消息如果在TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的TTL 和消息的TTL,那么较小的那个值将会被使用。
代码架构图如下所示,其中有两个direct类型的交换机X
、Y
,其中Y为死信交换机;还有三个队列QA
、QB
、QD
,QA和QB为普通队列,其中QA中消息的ttl为10s,QB中消息的ttl为40s,QD为死信队列。队列与交换机之间的routing-key如图中连线上标注所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bbRRw2rv-1691716049857)(RabbitMQ%2079b5ad38df29400fa52ef0085a14b02f/Untitled%208.png)]
以上延时队列的实现目前只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
因此我们需要做出一些优化,在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间,我们通过指定消息的 TTL 来实现消息的延迟
参见:https://blog.csdn.net/qq_45173404/article/details/121687489
所谓幂等性就是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。比如用户购买商品后支付后已经扣款成功,但是返回结果时出现网络异常,用户并不知道自己已经付费成功,于是再次点击按钮,此时就进行了第二次扣款,这次的返回结果成功。但是扣了两次用户的钱,这就出现了不满足幂等性,即用户对统一操作发起了一次或多次请求
对应消息队列 MQ 中出现的幂等性问题就是消息重复消费。比如消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者的重复消费。
幂等性解决方案:https://zhuanlan.zhihu.com/p/176944177
在RabbitMQ中,队列需要设置为优先级队列的同时消息也必须设置消息的优先级才能生效,而且消费者需要等待消息全部发送到队列中才去消费因为这样才有机会对消息进行排序。
代码实现队列优先级
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。
从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下: