本知识点主要是结合springboot来阐述的;
主要知识点如下:
消息可靠性投递
Consumer ACK
消费端限流
TTL队列
死信队列
延迟队列
消息幂等性保障(重复消费消息)
如何保证消息的顺序性(顺序消费)
如何处理MQ的消息积压
RabbitMQ集群
springboot整合rabbitmq
1.消息的可靠投递:确保生产者准确投递到目标队列
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式;
confirm 确认模式
return 退回模式
rabbitmq 整个消息投递的路径为:
producer—>rabbitmq broker—>exchange—>queue—>consumer
消息从 producer 到 exchange 则会返回一个 confirmCallback 。
当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理
消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。
当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage
我们将利用这两个 callback 控制消息的可靠性投递,比如我们可以记录投递失败的消息,然后后期进行处理,达到弱一致性的效果。
代码演示:
1、在配置文件加上配置来分别开启两个模式
1、
#开启确认模式
spring.rabbitmq.publisher-confirm-type=correlated
#开启返回模式
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
2、在RabbitTemplate对象中设置confirmCallback 和returnCallback 回调函数:
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//mandatory必须为true才会触发ReturnCallback方法
rabbitTemplate.setMandatory(true);
//消息不可达触发confiremcallback。例如发送的消息到交换机上,如果没有这个交换机,就会触发这个方法
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: " + "相关数据:" + correlationData);
System.out.println("ConfirmCallback: " + "确认情况:" + ack);
System.out.println("ConfirmCallback: " + "原因:" + cause);
}
});
/**
* 消息可达。但没有指定的队列去接收。会触发此方法。
* 例如发送消息到交换机上(exchange-1)。交换机绑定的队列是(queue-1)。绑定的路由key是(springboot.#)
* 你发送消息是发送到exchange-1上。但是你指定的路由key如果是错的。就会找不到队列去消费。
* 这个时候就会调用这个方法
*/
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback: " + "消息:" + message);
System.out.println("ReturnCallback: " + "回应码:" + replyCode);
System.out.println("ReturnCallback: " + "回应信息:" + replyText);
System.out.println("ReturnCallback: " + "交换机:" + exchange);
System.out.println("ReturnCallback: " + "路由键:" + routingKey);
}
});
return rabbitTemplate;
}
2.消费者的ACK机制:
AUTO模式:在springboot中默认开启的是AUTO模式,这比RabbitMq原生多了一种,而AUTO其实也是手动模式,只不过是Spring的一层封装,其就是当业务代码出现异常后,会回退消息,该消息重新进入队列中,并再次被该消费者获取到,所以出现 这个同一个报错消息被无限消费的情况。
NONE:其实就是RabbitMq的自动确认,只要消息被消费者接受,则认为被成功消费。
MANUAL:手动确认模式,需要开发者自己确认收到消息channel.basicAck(deliveryTag, true)第二个参数为,ture 签收之前所有未签收的消息,并将相应 message 从 RabbitMQ 的消息缓存中移除;
如果手动签收模式下,代码出现异常,则调用channel.basicNack()方法,让其自动重新发送消息,这个拒绝签收有个参数可以控制丢弃消息,或者重回队列,但是重回队列还是会被这个消费者消费到又会出现错我,这就是AUTO模式一样,解决方案:
在代码里重试消费此消息,如果还是报错,则记录这个消息,后期人工处理,
channel.basicNack(deliveryTag, true, false)第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端;我们还可以设置成把消息退回到死信队列(我没操作过,但是可以自己设置),然后再异步处理这些失败的消息。
注意:
如果设置成手动模式,当每个消费者的线程未签收的消息的数据量达到prefetchCount后,将不会接收任何消息,当此消费者断开连接后,才能释放这些为签收的消息,待其它消费者进行消费;
prefetchCount这个值表示的是一个Channel预取的消息数量,这个参数只会在手动确认的消费者才生效,客户端利用这个参数来提高性能和做流量控制;
如果prefetch设置的是10,当这个Channel上unacked的消息数量到达10条时,
,RabbitMq便不会在向你发送消息;
以下是设置每个消费者最小的消费线程和最大的消费线程,但并发量大的时候,会自动增加处理消费线程,一个线程对应一个channel通道。
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(2);
问题:消息的可靠性(消息不丢失)如何保证?
1、持久化
exchange要持久化
queue要持久化
message要持久化
2、生产方确认Confirm和return模式
3、消费方的Ack机制;
4、Broker集群的高可用
3.消费端限流:
如果一个系统每秒只能处理1000个请求,但是在活动期间,请求瞬间增多,我们可以把这些请求先放在MQ中,然后后台系统慢慢的消费消息;
我们需要把ack设置成手动确认的模式,并且设置prefetch属性设置消费端一次拉取多少消息,消费完之后再从mq中取,一次性将所有消息都发送给消费端,有很大几率会导致消费端崩掉。
4.TTL队列:
TTL 全称 Time To Live(存活时间/过期时间);
当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间;
对消息设置过期时间的代码为:
rabbitTemplate.convertAndSend("boot_exchange_ttl","ttl.add",message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(5*1000+"");
return message;
}
});
设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。QueueBuilder.durable(“boot_queue_ttl”).withArgument(“x-message-ttl”, 10000);
设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
如果两者都进行了设置,以时间短的为准
5.死信队列:(接受那些异常消息的队列,比如过期或者处理异常的消息)
英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message,也就是消息在TTL队列到期后,可以被重新发送到另一个交换机,这个交换机就是DLX。
队列消息长度到达限制;
2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
3. 原队列存在消息过期设置,消息到达超时时间未被消费;
队列绑定死信交换机:
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
TTL队列绑定死信交换机
// x-dead-letter-exchange 死信交换机名称
.withArgument("x-dead-letter-exchange", "boot_exchange_dlx")
// x-dead-letter-routing-key 发送给死信交换机的routingkey。
.withArgument("x-dead-letter-routing-key", "dlx.xx.asa")
死信交换机绑定死信队列,注意绑定的routingKey要能接收到TTL队列的死信消息:
BindingBuilder.bind(dlxOneQueue()).to(dlxExchange()).with(“dlx.#”).noargs()
- 死信交换机和死信队列和普通的没有区别
- 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
- 消息成为死信的三种情况:
- 队列消息长度到达限制;
- 消费者拒接消费消息,并且不重回队列;
- 原队列存在消息过期设置,消息到达超时时间未被消费;
6.延迟队列
即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费;
延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费;
RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果
使用场景:
- 下单后,30分钟未支付,取消订单,回滚库存。
- 新用户注册成功7天后,发送短信问候。
很可惜,在RabbitMQ中并未提供延迟队列功能,
但是可以使用:TTL+死信队列 组合实现延迟队列的效果。
即先将消息发送到TTL队列,30分钟到期后,消息会被自动清除而进入绑定的死信交换机,然后进入到绑定死信交换机的队列,我们监听着这个队列就可以了。
代码如下:
/**
* 声明TTL队列
*
* @return
*/
@Bean
public Queue bootQueueTtl() {
return QueueBuilder.durable("boot_queue_ttl").withArgument("x-message-ttl", 10000)
//以下是重点:当变成死信队列时,会转发至 路由为x-dead-letter-exchange及x-dead-letter-routing-key的队列中
// x-dead-letter-exchange 死信交换机名称
.withArgument("x-dead-letter-exchange", "boot_exchange_dlx")
// x-dead-letter-routing-key 发送给死信交换机的routingkey。
.withArgument("x-dead-letter-routing-key", "dlx.xx.asa")
.withArgument("x-message-ttl", 1 * 10 * 1000)//10秒时间(单位:毫秒),当过期后 会变成死信队列,之后进行转发
.build();
}
/**
* 声明ttl exchange
*
* @return
*/
@Bean
public Exchange ttlExchange() {
return ExchangeBuilder.topicExchange("boot_exchange_ttl").durable(true).build();
}
/**
* 绑定ttl
*
* @return
*/
@Bean
public Binding ttlBindQueueExchange() {
return BindingBuilder.bind(bootQueueTtl()).to(ttlExchange()).with("ttl.#").noargs();
}
/**
* 声明死信队列
*
* @return
*/
@Bean
public Queue dlxOneQueue() {
return QueueBuilder.durable("boot_queue_dlx")
.build();
}
/**
* 声明死信交换机
*
* @return
*/
@Bean
public Exchange dlxExchange() {
return ExchangeBuilder.topicExchange("boot_exchange_dlx").durable(true).build();
}
/**
* 绑定死信队列和交换机
*
* @return
*/
@Bean
public Binding dlxBindQueueExchange() {
return BindingBuilder.bind(dlxOneQueue()).to(dlxExchange()).with("dlx.#").noargs();
}
//先TTL交换机发送消息
rabbitTemplate.convertAndSend("boot_exchange_ttl", "ttl.add", message);
//或者给消息设置过期时间后,发送到TTL队列,以最小时间为准
rabbitTemplate.convertAndSend("boot_exchange_ttl","ttl.add",message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(5*1000+"");
return message;
}
});
7.消息幂等性保障(重复消费消息)
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同;
在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果;
解决办法:
① 利用数据库的唯一约束
在进行消息消费,需要取一个唯一个标识,比如 id 作为唯一约束字段,先添加数据,如果添加失败,后续做错误提示,或者不做后续操作;
② Redis 设置全局唯一id
每次生产者发送消息前设置一个全局唯一id放在消息体中,并存放的 redis 里;在消费端接口上先找在redis 查看是否存在全局id,如果存在,调用消费接口并删除全局id,如果不存在说明已被消费,不做后续操作。
③ 多版本(乐观锁)机制
给业务数据添加一个版本号,比如version=1,每次更新数据前,比较当前版本和消息中的版本是否一致,如果一致就更新数据并且版本号+1,如果不一致就不跟新。这有点类似乐观锁处理机制。
例如数据库中相对应的version=1,消息体中的version=1,这时候更新数据库中的version=2,这时候再收到version=1的消息则不做处理。如果后续还有操作该数据,必须生产者和消费者的版本要鲍茨一致。
8.如何保证消息的顺序性(顺序消费)
从根本上说,异步消息是不应该有顺序依赖的。在MQ上估计是没法解决。要实现严格的顺序消息,
简单且可行的办法就是:保证生产者 - MQServer - 消费者是一对一对一的关系;
1、如果有顺序依赖的消息,要保证消息有一个hashKey,类似于数据库表分区的的分区key列。保证对同一个key的消息发送到相同的队列;
2、把原本一个接收全部消息的Queuq,拆分多个queue,每个queue一个consumer(可以通过配置来决定是往spring否注入这个监听器),就是多一些queue而已,确实是麻烦点;
3、一个 queue 对应一个 consumer,但是consumer里面进行了多线程消费(也可以设置为单线程),然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 线程来处理
9.如何处理MQ的消息积压
先看一下造成消息积压的常见原因:
消费者宕机积压
消费者消费能力不足积压
发送者发流量太大
解决方案:
上线更多的消费者,进行正常消费;
上线专门的队列消费服务;
将消息先批量取出来,记录数据库,再慢慢处理
10.RabbitMQ集群
一般来说,如果只是为了学习RabbitMQ或者验证业务工程的正确性那么在本地环境或者测试环境 上使用其单实例部署就可以了,但是出于MQ中间件本身的可靠性、并发性、吞吐量和消息堆积能 力等问题的考虑,在生产环境上一般都会考虑使用RabbitMQ的集群方案;
集群方案的原理
RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的cookie来实现)。RabbitMQ本身不需要像ActiveMQ、Kafka那样通 过ZooKeeper分别来实现HA方案和保存集群的元数据。消费者和生产者通过访问HAProxy-负载均衡代理来访问RbbitMQ集群中的节点;每个节点的数据都是一样的,每个节点的数据都会相互同步。
负载均衡-HAProxy
HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。
11.springboot整合rabbitmq
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
添加配置:
spring:
rabbitmq:
host: 192.168.244.136 # ip
port: 5672
username: bijian
password: 123456
virtual-host: bjjian-vi-host
listener:
simple:
acknowledge-mode: auto
auto-startup: true
#消息确认发送
publisher-returns: true
#消息确认接收
publisher-confirm-type: correlated
template:
mandatory: true
可以使用@RabbitListener注解来监听消息:
/**
* 利用@RabbitListener可以声明队列。交换机。绑定。避免反锁的配置
*/
@RabbitListener(containerFactory = "myMessageListenerContainer",
ackMode = "MANUAL",
bindings = @QueueBinding(
value = @Queue(value = QUEUE_NAME, durable = "true"),
exchange = @Exchange(value = EXCHANGE_NAME, durable = "true", type = "topic", ignoreDeclarationExceptions = "false"),
key = "boot.#"
), concurrency = "1")
public void process(Message testMessage, Channel channel,
@Header(name = "amqp_deliveryTag") long deliveryTag,
@Headers Map<String, Object> map) throws IOException {
try {
System.out.println("1接受到的消息为:" + new String(testMessage.getBody()));
System.out.println("Header=" + map);
//第二个参数为,ture 签收之前所有未签收的消息
int i = 1 / 0;
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
System.out.println("异常信息为:"+e);
//拒绝签收
/*
第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag, true, false);
}
}
@RabbitListener注解中containerFactory 属性是指定RabbitListenerContainerFactory监听容器工厂,这个接口有有两个主要的实现类DirectRabbitListenerContainerFactory和SimpleRabbitListenerContainerFactory,这监听容器的区别我具体不清楚;
使用这个注解会生成一个SimpleMessageListenerContainer消息监听容器,这个容器可以配置监听队列和监听对象,在下面做代码演示。
使用@RabbitListener注解可以在内部自动帮你创建队列和任意类型的交换机,并进行队列和交换机绑定,可以指定消息的ackMode 签收模式,可以指定concurrency 最小工作线程。
我们还可以在RabbitListenerContainerFactory监听容器工厂指定这个监听者具体其它的参数:代码如下这个工厂里的Prefetch参数还是比较重要的。
@Bean("myMessageListenerContainer")
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//factory.setMessageConverter(new Jackson2JsonMessageConverter());
/**
* 关于spring的AcknowledgeMode需要说明,他一共有三种模式:NONE,MANUAL,AUTO,默认是AUTO模式。这比RabbitMq原生多了一种。
* 这一点很容易混淆,这里的NONE对应其实就是RabbitMq的自动确认,MANUAL是手动。而AUTO其实也是手动模式,
* 只不过是Spring的一层封装,他根据你方法执行的结果自动帮你发送ack和nack。如果方法未抛出异常,则发送ack。
* 如果方法抛出异常,并且不是AmqpRejectAndDontRequeueException则发送nack,并且重新入队列,并且无限接收此消息。
* 如果抛出异常时AmqpRejectAndDontRequeueException则发送nack不会重新入队列。
*/
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(2);
/**
* 还有一点需要注意的是消费者有一个参数prefetch,
* 它表示的是一个Channel预取的消息数量,
* 这个参数只会在手动确认的消费者才生效。
* 可以客户端利用这个参数来提高性能和做流量控制。
* 如果prefetch设置的是10,当这个Channel上unacked的消息数量到达10条时,
* RabbitMq便不会在向你发送消息,客户端如果处理的慢,便可以延迟确认在方法消息的接收。
* 至于提高性能就非常容易理解,因为这个是批量获取消息,
* 如果客户端处理的很快便不用一个一个去等着去新的消息。
* SpringAMQP2.0开始默认是250,这个参数应该已经足够了。
* 注意之前的版本默认值是1所以有必要重新设置一下值。
* 当然这个值也不能设置的太大,RabbitMq是通过round robin这个策略来做负载均衡的,
* 如果设置的太大会导致消息不多时一下子积压到一台消费者,不能很好的均衡负载。
* 手动确认模式才生效
*/
factory.setPrefetchCount(5);
factory.setChannelTransacted(false);
factory.setDefaultRequeueRejected(true);
// factory.setErrorHandler(errorHandler);
return factory;
}
使用一下方式声明队列和交换机,是不会创建队列和交换机的,要想创建,必须达到下面任意条件:
在声明的交换机和队列的项目中发送消息给这个交换机;
有使用任意的@RabbitListener注解;
//1、声明交换机
@Bean
public Exchange bootExchange() {
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
//2、声明队列
@Bean
public Queue bootQueue() {
return QueueBuilder.durable(QUEUE_NAME).build();
}
//3、队列与交换机进行绑定
@Bean
public Binding bindQueueExchange() {
return BindingBuilder.bind(bootQueue()).to(bootExchange()).with("boot.#").noargs();
}
/**
* 消息监听容器
* 一个监听器,一个容器
*
* @param connectionFactory
* @return
*/
@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(connectionFactory);
// 设置监听的队列
// simpleMessageListenerContainer.setQueueNames(RabbitmqConstant.QUEUE_SPRING_TOPIC);
// 指定要创建的并发使用者的数量,默认值是1,当并发高时可以增加这个的数值,同时下方max的数值也要增加
simpleMessageListenerContainer.setConcurrentConsumers(3);
// 最大的并发消费者
simpleMessageListenerContainer.setMaxConcurrentConsumers(10);
// 设置是否重回队列
simpleMessageListenerContainer.setDefaultRequeueRejected(false);
// 设置签收模式
simpleMessageListenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 设置非独占模式
simpleMessageListenerContainer.setExclusive(false);
// 设置consumer未被 ack 的消息个数
simpleMessageListenerContainer.setPrefetchCount(1);
// 接收到消息的后置处理
simpleMessageListenerContainer.setAfterReceivePostProcessors((MessagePostProcessor) message -> {
message.getMessageProperties().getHeaders().put("接收到消息后", "在消息消费之前的一个后置处理");
return message;
});
// 设置 consumer 的 tag
simpleMessageListenerContainer.setConsumerTagStrategy(new ConsumerTagStrategy() {
private AtomicInteger consumer = new AtomicInteger(1);
@Override
public String createConsumerTag(String queue) {
return String.format("consumer:%s:%d", queue, consumer.getAndIncrement());
}
});
// 设置消息监听器
// simpleMessageListenerContainer.setMessageListener(springMQListener());
/** ================ 消息转换器的用法 ================
simpleMessageListenerContainer.setMessageConverter(new MessageConverter() {
// 将 java 对象转换成 Message 对象
@Override public Message toMessage(Object object, MessageProperties messageProperties) {
return null;
}
// 将 message 对象转换成 java 对象
@Override public Object fromMessage(Message message) {
return null;
}
});
*/
/** ================ 消息适配器的用法,用于处理各种不同的消息 ================
MessageListenerAdapter adapter = new MessageListenerAdapter();
// 设置真正处理消息的对象,可以是一个普通的java对象,也可以是 ChannelAwareMessageListener 等
adapter.setDelegate(null);
adapter.setDefaultListenerMethod("设置上一步中delegate对象中处理的方法名");
ContentTypeDelegatingMessageConverter converters = new ContentTypeDelegatingMessageConverter();
// 文本装换器
MessageConverter txtMessageConvert = null;
// json 转换器
MessageConverter jsonMessageConvert = null;
converters.addDelegate("text", txtMessageConvert);
converters.addDelegate("html/text", txtMessageConvert);
converters.addDelegate("text/plain", txtMessageConvert);
converters.addDelegate("json", jsonMessageConvert);
converters.addDelegate("json/*", jsonMessageConvert);
converters.addDelegate("application/json", jsonMessageConvert);
adapter.setMessageConverter(converters);
simpleMessageListenerContainer.setMessageListener(adapter);
*/
return simpleMessageListenerContainer;
}