创建项目:
新建一个项目,并且引入RabbitMq依赖。
spring:
rabbitmq:
host: 110.41.51.65
port: 5673 #默认为5672
username: study
password: study
virtual-host: bite
简单模式
生产者发送channel时,就会记录一个deliveryTag,这个tag就是记录下来该信道发送几次消息,如果重新启动并且创建的话,就会有新的信道,这个值就会重置。
生产者
package com.syx.rabbitmqspring.Controller;
import com.syx.rabbitmqspring.Constant.Constants;
import jakarta.annotation.Resource;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/producer")
@RestController
public class ProducerController {
@Resource
RabbitTemplate rabbitTemplate;
@RequestMapping("/work")
public String work(){
//发送消息
/*
*void convertAndSend(String exchange//交换机名字, String routingKey//队列名字, Object message发送信息) throws AmqpException;
*
*使用内置交换机,routingkey和和队列名字一样,
*
* */
rabbitTemplate.convertAndSend("" , Constants.WORK_QUEUE,"hello mq hello spring");
return "发送成功";
}
}
消费者
package com.syx.rabbitmqspring.Listener;
import com.syx.rabbitmqspring.Constant.Constants;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class WorkListener {
//监听要接收的队列,只要有消息就会立马进行监听
@RabbitListener(queues = Constants.WORK_QUEUE)
public void queueListener(Message message){
System.out.println(Constants.WORK_QUEUE+"接收到消息"+message);
}
}
在这里一个rabbitlistener就是一个消费者,就是spring框架用于监听mq的注解, 可以接收message参数,或者String类型,如果是string类型就是发送消息的一个具体内容。
都可以在消费者部分进行指定。
交换机已经创建好了,并且已经指定好类型了,就不能进行修改了,比如原本是direct,就不能修改成topic。并且也不能修改队列持久化的参数.
基于springboot和rabbitmq的通信
订单系统接收到消息,发送给mq,mq接收到消息再发送给物流系统,然后物流系统开始发货,订单系统就是生产者,物流系统就是消费者。
@Component
@RabbitListener(queues = Constant.ORDER_QUEUE)
public class OrderListener {
@RabbitHandler
public void handMessage(Message message,String orderInfo){
System.out.println("接收到订单消息1"+orderInfo);
}
@RabbitHandler
public void handMessage(Integer orderInfo){
System.out.println("接收到订单消息1"+orderInfo);
}
}
如果这里是将rabbitlistener加到类上面去,这时候就要在类上面加上rabbitHandler,就会根据收到信息的类型进行自动匹配。 但是这里需要对双方都进行配置config如果有一方没有配置的话,另外一方就接收不到。一个类型只能有一个。
package com.syx.rabbitmq.orderserver.Config;
import com.syx.rabbitmq.orderserver.Constant.Constant;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMqConfig {
//简单模式
@Bean("orderQueue")
public Queue orderQueue(){
return QueueBuilder.durable(Constant.ORDER_QUEUE).build();
}
@Bean("jackson2JsonMessageConverter")
public Jackson2JsonMessageConverter jackson2JsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,@Qualifier("jackson2JsonMessageConverter") Jackson2JsonMessageConverter jackson2JsonMessageConverter){
RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter);
return rabbitTemplate;
}
}
Rabbitmq高级特性
消息确认机制(消费者确认)
rabbitmq发送消息到消费者后,可能中间生产者成功发送消息到mq服务器,但是当mq服务器发送给消费者的时候,就可能出现消息丢失,mq以为消费者接收到了消息,然后mq把消息删除了,但是实际上发送失败(消费者处理失败才是关键)。
要保证消费者确认,必须要消息确认机制。
自动确认:
只要发送成功给消费者就可以自动确认,不管消费者是否处理成功,这种场景对消息可靠性不高的场景。消费者不需要进行任何处理。
手动确认:
消费者显示调用ack确认信号,mq才认为消息已经正确到达消费者,否则则认为没有收到,适合对消息可靠性要求高的场景。
当然了这里在basicconsume有个autoack。
如果消费端没有确认,就说明消息消费者处理失败了,这时候就可能要求mq服务器重发,如果确认可能就可以在mq中删除这个数据。
拒绝消息:
requeue会在拒绝的时候,是否重新放入队列,如果是true的话就会重新放入队列, 否则就会丢弃。
批量拒绝消息,既可以批量,也可以单独拒绝消息。
Spring-ampq三种策略:
- AcknowledgeMode.AUTO(默认):在这种模式下,消息一旦投递给消费者就直接删除,不管消费者内部有没有处理成功。
- AcknowledgeMode.NONE:这种模式下,消费者在消息确认会自动确认消息,如果处理过程中抛出异常则不会确认消息。
- AcknowledgeMode.MANUAL:必须显示调用basicnck来确认消息,即使消息处理导致丢失,消息也不会丢失,而是可以被重新处理。
Rabbit持久性
持久性也是可靠性的保证之一。图中有多个环节会出现消息丢失。
生产者自己把数据弄丢了,broker自己把数据弄丢了(断电了),也有可能消费者自己弄丢数据(消费者处理异常)。
生产者到达broker可以通过发送方确认,比如对方保管好了返回一个ack确认,消息确认机制,分为手动确认和自动确认。
交换机的持久化
在设置交换机的时候,有一个durable属性,把durable设置为true,就把交换机的一些数据存储在磁盘上而不是内存上了,即使重启,交换机还是会有数据。如果是长期使用,建议使用持久化。默认是持久化的。这时候就设置为非持久性了。
队列持久化
队列设置为持久化,队列的消息不会消失,默认是持久化的。
消息持久化
channel发送数据
消息持久化是和队列有一定关系的,消息持久化需要队列和消息双持久化才可以,消息是存储在队列中的,所以需要消息和队列同时持久化,如果只设置其中一个持久化,mq重启之后消息都会丢失。
使用rabbitTemplate
只有在队列持久化和消息持久化的时候消息才不会丢失,其他情况都会丢失。
Rabbitmq数据丢失
但是并不是只要交换机,队列,消息都持久化了就可以,实际上,mq是把消息存储在缓存中,等固定时间后一起写入硬盘,如果在缓存上的时候,还没来得及写入硬盘(比如电脑挂机了),就会出现消息丢失的情况。
发送方确认两种模式(保证消息可靠性)
生产者发送消息给broker,如果消息都没有到达服务器,更不用说后续的持久化了。
confirm确认模式
不管对方收没收到,监听都会被执行,如果exchange收到,ack返回为true,否则为false。
return模式
消息到达exchange,可能到达exchange,但是无法到达queue,就是routingkekey不匹配,退回发送者,发送者对消息进行处理。将消息退回给发送方。
实际上confirm和return是结合一起使用的。
如何确保Rabbitmq的可靠传输(面试题)
从生产者到交换机可能有错误,交换机到队列可能有问题,队列到消费者可能有问题。
生产者到broker:
采用发送方确认,生产者发送后,不管对方有没有收到,监听都会被执行,如果exchange收到,ack返回为true,否则为false。
exchange到queue:
通过return 模式,如果routingkey不同导致没有成功发到队列中,就可以使用return方法,来进行下一步的逻辑处理。
broker内部出现问题:
消息成功到达队列,但是服务器忽然宕机了 ,这时候就通过交换机持久化,队列持久化,消息持久化,默认就是队列消息持久化。但是并不是每一条消息都进行持久化,可能还没有持久化,主机就挂了,这时候也会让数据丢失。
broker到consumer
消息到达consumer的时候消息就会自动删除(自动确认),但是可能因为消费者代码逻辑和内部错误导致消费消息失败了,从而找不到消息,就需要手动确认,消费者消费后手动确认才会删除(手动确认)。
rabbitmq重试机制(交换机和消费者)
Rabbitmq提供的SDK方式,自动确认,消息到达消费者就自动删除,手动确认,消息发送成功需要进行确认。SpringAMQP提供了三种方式,none,manual,auto三种方法,auto当消费者处理消息的时候抛出异常就会进行重试。
但是比如出现代码问题的话即使重试也没有用。
未配置重试策略,消息就会一直发送,不会停止,如果配置了就可以根据配置进行重试发送。重试机制在自动配置的机制下才生效。
spring:
rabbitmq:
listener:
simple:
prefetch: 1
retry:
enabled: true # 开启消费者失败重试
initial-interval: 5000ms # 初始失败等待时长为5秒
max-attempts: 5 # 最大重试次数
publisher-confirm-type: correlated #消息自动确认模式
TTL(过期时间)
当消息到达过期的时间还没有被消费掉就会被清除。
设置队列的TTL
比如在队列设置20s,那么存储在该队列中所有的消息的ttl和队列的ttl都是20s,如果消息的ttl为10s,队列为20s,那么就是10s,取最小的那个。
比如我买了个平板,订单系统发送到mq,mq再发送到支付系统,但是我超时未支付,订单消息就会丢失。
设置消息的ttl
@RequestMapping("ttl")
public String ttl(){
System.out.println("设置ttl时间");
//设置过期时间
MessagePostProcessor messagePostProcessor=new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
};
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE,"ttl","hello ttl hello we are ttl",messagePostProcessor);
return "发送ttl成功";
}
如果设置队列的ttl,如果消息过期就会马上从队列中删除,如果设置消息过期时间的话,会在投递到消费者之前进行判定。
当为队列设置 TTL 时,意味着队列中的所有消息都有一个统一的存活时间。RabbitMQ 会在内部维护队列中消息的时间信息,一旦某个消息在队列中的存在时间达到了设定的 TTL,RabbitMQ 会立即将其从队列中移除。
当为单个消息设置过期时间时,RabbitMQ 不会像处理队列 TTL 那样实时监控并删除过期消息。这是因为不同消息的过期时间可能各不相同,实时删除会带来较大的性能开销。如果设置消息过期时间的话,会在投递到消费者之前进行判定。也会在队列中存在,在即将被消费的时候才被判定。
死信队列
由于种种原因无法被消费的消息就称之为死信,比如设置ttl之后,消息超过ttl规定的时间后已经过期了,就称之为死信。存储死信消息就称之为死信队列。
变成死信的几种情况
- 消息发送后被拒绝,并且没有进行requeue操作,也就是无法重新入队。
- 消息可能过期了,无法被consumer1消费掉。
- 队列达到最大长度了。
如果时间过期了后,消息就会自动放到死信队列中 。
死信面试题
死信队列是一种特殊的队列用来存储无法被正常消费的队列 ,它指的是那些无法被正常消费和处理的消息,被存储在一个新的队列中。
死信的几种情况:
- 消息发送后被拒绝,并且没有进行requeue操作,也就是无法重新入队。
- 消息可能过期了,无法被consumer1消费掉。
- 队列达到最大长度了,已经满了。
nack可能会出现一个问题,比如发送给消费者但是消费者由于代码问题出现逻辑错误吗,抛出异常,导致消息重新入队,后面还有源源不断的消息发送过来,就会导致信息积压。
这时候我们可以不重新入队,将消息发送到新的死信队列,通过新的死信队列进行消费。死信接收后,人工进行处理。
比如订单出现问题,mq就会将出现问题的订单进行发送到死信队列,然后将死信队列中的数据重新发送给消费者进行处理。
延迟队列
发送消息之后,并不想消费者直接拿到数据,而是希望消费者过一会额儿才拿到数据。
如图就是延迟队列的实现,由于mq没有实现延迟队列,所以只能自己实现一个延迟队列。设置消息的ttl可能会出现问题。先发送30s的ttl,再发送10s的ttl,会在30s过期之后才进行处理。
第二个消息比第一个先过期了,就会发生第二个延迟不生效。
插件安装
https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq
在该页面中打开该连接,跳转到github上面获取安装包。
安装第一个。
在/usr/lib/rabbitmq/plugins下面安装该插件,将我们在window上面的插件拖进来就可以看到插件安装成功了。(要使用相匹配版本的插件,比如这里就是需要3.9.x的)。
在交换机下面就可以看见我们刚刚添加的类型。如果想禁用插件,就把
的enable变成diasable就可以了。
基于插件的延迟队列
@Bean("delayExchange")
public DirectExchange delayExchange(){
//表明是延迟队列
return ExchangeBuilder.directExchange(Constants.DELAY_EXCHANGE).delayed().build();
}
注意这边要声明队列是延迟队列,上面ttl加死信队列会发生消息乱序的情况下,但是这里官方提供的插件就不会出现这件事。
延迟队列面试题
基于死信实现的延迟队列:
- 优点:灵活不需要额外的插件支持。
- 缺点:存在消息顺序的问题,需要额外的逻辑来处理消息死信顺序的问题。增加系统复杂性。
基于插件实现的延迟队列:
- 优点:可以通过插件直接创建延迟队列,增加系统复杂性。
- 缺点:只能用特定版本(有运维的成本)。
rabbitmq事务机制
@Bean
public RabbitTemplate transRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate transRabbitTemplate=new RabbitTemplate(connectionFactory);
//设置事务为true,开启事务。
transRabbitTemplate.setChannelTransacted(true);
return transRabbitTemplate;
}
@Bean
public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory){
return new RabbitTransactionManager(connectionFactory);
}
@Transactional
@RequestMapping("trans")
public String trans(){
System.out.println("发送trans消息");
//没有绑定的话就直接发给队列名字
transRabbitTemplate.convertAndSend("",Constants.TRANS_QUEUE,"发送trans消息1");
int a=3/10;
transRabbitTemplate.convertAndSend("",Constants.TRANS_QUEUE,"发送trans消息2");
return "success";
}
采用事务的方式两条消息同时成功或者同时失败。
可以看到当中间有异常的时候就会回滚,使得两个都没有成功插入。如果没有异常就都成功。
消息分发
正常rabbitmq是以轮询的方式进行分发,mq为消费端计数,如果达到消费端上限了,mq就不会再发送消息了。
限流:
在秒杀的情况下,抢购物品就会导致订单系统压力很大,就可以基于mq实现限流机制。这里就可以限制最多有五千个流量,如果没处理完,就不会给订单系统发送消息。
非公平分发(负载均衡):
按照一定的情况进行分发数据,干的快就多分发一点数据量,慢的话就少分发一点数据量。
listener:
simple:
acknowledge-mode: manual #手动确认
#acknowledge-mode: auto #消息接收确认
#acknowledge-mode: manual
#acknowledge-mode: none 自动确认
#消息接收确认
prefetch: 1
retry:
enabled: true # 开启消费者失败重试
initial-interval: 5000ms # 初始失败等待时长为5秒
max-attempts: 5 # 最大重试次数
具体代码可以参考我的gitee链接syxcyt (syxcyt) - Gitee.com。