面试
1 如何确保消息可靠性
确保消息的可靠性分为三部分: 生产者方 mq服务方 消费者方
生产者方 :发生在生产者给交换机消息时,如果生产者给交换机信息,不管成功不成功都会回调返回ACK 或者 NACK 使用全局ReturnCallBack,当生产者给交换机信息成功时,交换机需要给队列信息,如果交换机给队列失败就会调用ReturnCallBack,如果成功就不会调用
mq服务方:将交换机 队列 消息 持久化,但是mq默认这三个都是持久化的
消费者方: 一共有三种失败策略:第一种: 当接收到信息就会默认成功 第二种:当接收到以后如果失败会重复,如果超出最大次数还没有成功那么mq默认为已经成功 第三种:接收到以后如果超过最大次数就会将消息发送到指定的一个专门接收错误信息的队列
2 什么是消息幂等性?如何确保?
什么是消息幂等性:当数据第一次失败以后但是已经操作了数据库,中途失败以后,mq会再次尝试发送信息,这时候会再次操作数据库。导致一个信息被操作多次。一次或多次的访问结果影响一致
如何确保:
利用数据库的唯一约束
redis
另建一张表用来存储上次操作流水
分布式锁
3 什么样的消息会变为死信?声明死信的注意事项?死信队列的用处
什么样的消息会变为死信:消费者声明消费失败的消息 过期消息 队列满了之后还投递的消息
声明死信的注意事项:死信交换机名称 死信交换机和死信队列绑定的RountingKey
死信队列的用处: 将死信的信息给人工来处理,增强消息的可靠性
4 TTL消息超时时间如何声明?延时消息是基于什么来实现的?
TTL消息超时时间如何声明:第一种:给队列设置超时时间 第二种:给消息设置超时时间
延时消息是基于什么来实现的:TTL+死信交换机
5 延时队列如何声明?原理是什么?
设置交换机为delay类型 在发送消息的时候,设置头 x-delay 后面加时间
6 Rabbitmq如何实现消息积压?其实现后的特点是什么?
消息堆积问题:生产者发送消息的速度超过了消费者处理消息的速度,并且队列中存储消息达到上限,之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。
1.RabbitMQ如何实现消息积压?
- 队列上绑定多个消费者,提高消费速度
- 使用惰性队列,可以在mq中保存更多消息(百万条)
2.其实现后的特点是什么?
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
3.惰性队列的优缺点
优点:
- 基于磁盘存储,消息上限高
- 没有间歇性的page-out,性能比较稳定
缺点:
- 基于磁盘存储,消息时效性会降低
- 性能受限于磁盘的IO
消息可靠性
生产者消息确认
失败会返回NACK 成功就返回ACK
第一步:在生参方添加配置到配置文件
# publisher-confirm-type 代表的是开启publisher-confirm confirmCallback是生产者给交换机信息时发生的。不管给交换机信息成功与否都会调用confirmCallback
# 支持两种类型 1:simple 同步等待confirm结果知道超时 2: correlated 异步调用 定义confirmCallback 返回结果时会调用confirmCallback
publisher-confirm-type: correlated
# 开启publisher-returns 这个定义的是ReturnCallback 发生在交换机和队列之间,交换机给队列信息如果成功就不会回调,失败就会回调失败的函数
publisher-returns: true
# 失败路由策略,试过为true就会调用ReturnCallback false就直接丢弃
template:
mandatory: true
第二步:添加
confirmCallBack
public void testSendMessage2SimpleQueue() throws InterruptedException {
String routingKey = "red";
String message = "hello, spring amqp!";
//生成唯一标识
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(
confirm -> {
if (confirm.isAck()) {
//消息顺利给了交换机
System.out.println("发送成功ACK,confirm = " + confirm + ", id:" + correlationData.getId());
} else {
//消息给交换机失败
System.out.println("发送失败NACK,confirm = " + confirm + ", id:" + correlationData.getId());
}
},
throwable -> {
//发生错误,链接mq异常,mq未打开等...报错回调
System.out.println("发送失败throwable = " + throwable + ", id:" + correlationData.getId());
}
);
//不管成功失败都会调用confirm或者throwable 这是异步调用
rabbitTemplate.convertAndSend("test.direct", routingKey, message, correlationData);
}
消息持久化
交换机持久化 默认就是交换机就是持久化
@Bean
public DirectExchange simpleExchange(){
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new DirectExchange("simple.direct", true, false);
}
队列持久化 默认的队列就是持久化
@Bean
public Queue simpleQueue(){
// 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();
}
消息持久化 默认的消息就是持久化
@Test
public void testSendDurableMessage() throws InterruptedException {
// 1.消息体
Message message = MessageBuilder.
withBody("hello, spring amqp!".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
// 2.发送消息
rabbitTemplate.convertAndSend("simple.queue", message);
}
消费者确认
一共有三种:none auto manual
none 当消费者接收到信息就默认是成功的,队列就会将信息给清除 如果接收消息是正确的,但是实现代码错误,这时候队列也会删除,所以存在一定的不好的地方 auto 相当于是事务,spring检测代码当出现了异常就会返回nack 没有异常就会返回ack并且删除 相当MySQL的transaction 代码出现异常会回调,如果代码出现异常就会将接收的消息再返还给队列,队列继续请求,但是一直这样会无限次的重复访问知道成功,会消耗电脑的性能,所以这个可以设置次数以及多久访问一次 manual 手动调用ack回调,也就是说需要我们自己写代码,自己写回调
一般默认的都是auto
使用步骤
第一步:编写配置文件
spring:
rabbitmq:
host: 192.168.94.129 # rabbitMQ的ip地址
port: 5672 # 端口
username: itcast
password: 123321
virtual-host: /
listener:
simple:
prefetch: 1
# 有三种值 1:none 当消费者接收到信息就默认是成功的,队列就会将信息给清除 如果接收消息是正确的,但是实现代码错误,这时候队列也会删除,所以存在一定的不好的地方
# 2:auto 相当于是事务,spring检测代码当出现了异常就会返回nack 没有异常就会返回ack并且删除 相当MySQL的transaction 代码出现异常会回调
# 如果代码出现异常就会将接收的消息再返还给队列,队列继续请求,但是一直这样会无限次的重复访问知道成功,会消耗电脑的性能,所以这个可以设置次数以及多久访问一次
# 3:manual 手动调用ack回调,也就是说需要我们自己写代码,自己写回调
# 一般都是 auto
acknowledge-mode: auto
retry:
enabled: true # 开启消费者失败重试
initial-interval: 3000ms # 初始的失败等待时长为1秒
multiplier: 2 # 失败的等待时长倍数,下次等待时长 = multiplier * last 根据以上配置也就是说第一次失败等1s发送,第二次因为multiplier是2,所以就是1*2=2秒 第三次就是2*2=4s 第四次 4* 2=8 第五次 8*2=16s
max-attempts: 3 # 最大重试次数 当失败了三次spring会返回ack 这样消息就是丢弃了,这是spring内部机制
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
第二步:编写队列 交换机 接收信息
上边的retry机制,是为了防止auto模式下,消息一致失败导致mq会重复一直访问消费者,导致性能降低,所以这时候使用retry先将消息接收到本地,默认情况下是失败了就将消息丢弃,这样就会消息不可靠,所以需要使用失败策略
失败策略
当交换机失败以后有三种失败策略
失败交换机配置如下
/**
* 当消费信息发生错误超过设置的最大尝试次数,就会调用RepublishMessageRecoverer,将消息发送给指定的交换机,这样数据就不会丢失,将错误的数据收集起来等待人工处理
*/
@Bean
public MessageRecoverer republishMessageRecover(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
也就是说当消费者消费信息失败超过最大次数,就会将信息发送到储存失败信息的交换机中,让人工来处理
延迟队列
利用的是ttl+死信交换机,让消费者延迟收到消息
使用延迟队列需要安装mq插件
安装插件第一步:docker volume inspect mq-plugins 找到mq在docker中卷的位置
第二步:进入目录 将插件上传到该目录
第三步:docker exec -it mq在此虚拟机中的容器名称 bash
第四步: rabbitmq-plugins enable rabbitmq_delayed_message_exchange 完成
使用延迟队列:
消费者
//添加交换机时在交换机中添加 delayed = "true" 属性
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = {"red"}
))
public void delay(String msg) {
System.out.println("延迟队列msg = " + msg);
}
生产者:
public void delayExchange() {
String routingKey = "red";
// String message = "hello, spring amqp!";
//生成唯一标识
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(
confirm -> {
if (confirm.isAck()) {
log.info("发送到交换机成功ACK,id为{}", correlationData.getId());
} else {
log.info("发送到交换机失败NACK,id为{}", correlationData.getId());
}
},
throwable -> {
log.info("发送到交换机异常,id为{}", correlationData.getId());
}
);
rabbitTemplate.convertAndSend("lazy.directTest", routingKey,
// 这里需要设置的是消息 withBody里面是消息内内容
MessageBuilder.withBody("delay".getBytes(StandardCharsets.UTF_8))
//毫秒 这里必须设置头 x-delay 声明为delay类型
.setHeader("x-delay", 5000).build(),
correlationData);
}
惰性队列
/**
* 设置惰性队列
* 直接将内存中的队列信息保存到内存中,这样就会防止内存中的消息太多达到存储上限,把队列弄满造成死信
* 如果将数据存储到这个惰性队列那么就会将数据全部持久化
* @param msg
*/
@RabbitListener(queuesToDeclare = @Queue(
// 设置正则表达式 匹配”lazy.queue“ 的队列
name = "lazy.queue",
durable = "true",
//设置策略为lazy队列,只要是正则表达式满足name中的值就设置为lazy 因为value中设置了值为lazy类型
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void lazyQueue(String msg) {
System.out.println("惰性队列msg = " + msg);
}
这样就完成了惰性队列