五、消息队列 RabbitMQ
高可用、高可靠、低延迟、单机吞吐量一般的消息队列框架,异步通信
1、概述与部署
概述:MQ 的基本结构是:publisher(生产者) -- exchange(交换机,负责消息路由) -- queue(队列,存储消息) -- consumer(消费者)
部署:只需要下载镜像使用 docker 进行安装即可
2、基础使用
项目中使用 SpringAMQP,主要提供了三个功能:自动声明队列、交换机及其绑定关系;基于注解的监听器模式,异步接收消息;封装了 RabbitTemplate 工具,用于发送消息。所有的项目都可以使用这一个MQ,但是要使用MQ不同的虚拟机来进行数据隔离,即登陆不同到账号访问的数据不一样
1)使用
- 引入
spring-boot-starter-amqp
依赖 - 在 publisher 服务的 yml 配置文件中配置 rabbitmq 的环境:主机、端口、虚拟主机、用户名、密码
- 在 publisher 服务中利用 RabbitTemplate 实现消息发送:注入 rabbitTemplate,使用
rabbitTemplate.convertAndSend("交换机名称","路由的key,广播模式无需指定给null","消息")
发送消息,第四个参数CorrelationData
可以指定回调函数 - 在 consumer 服务中配置同样的 rabbitmq 环境
- 在 consumer 服务中创建配置类来创建指定类型的交换机(Exchange)、队列(Queue)的 Bean,并将它们绑定(Binding)。交换机类型:Fanout:广播,将消息交给所有绑定到交换机的队列;Direct:定向,把消息交给符合指定 routing key 的队列;Topic:通配符,把消息交给符合 routing pattern(路由模式) 的队列,
#
匹配一个或多个词
,*
匹配一个词,如 item.# 能够匹配 item.spu.insert 或者 item.spu - 在 consumer 服务中的组件类中使用
@RabbitListener(queues = "队列名称")
添加到方法上来接受队列消息,方法参数就是收到的消息。写多个带注解的方法可以实现多个消费者平均接受一个队列的消息来提高处理消息速度,想让能者多劳,需要在 yml 配置文件中添加spring:rabbitmq:listener:simple:prefetch: 1
让每个消费者只能获取一条消息,处理完成才能获取下一个消息。5 步骤麻烦,也可以使用注解进行绑定
java
// 5.在 consumer 服务中创建配置类来创建指定类型的交换机(Exchange)、队列(Queue)的 Bean,并将它们绑定(Binding),比较麻烦,也可以直接使用注解进行绑定
@Configuration
public class FanoutConfig {
/**
* 声明交换机
* @return Fanout 类型交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
/**
* 第1个队列;可以创建多个队列与一个交换机进行绑定
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
/**
* 绑定队列和交换机,如果多个队列绑定,就需要创建多个 Bean
*/
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
}
java
// 6. 5 步骤麻烦,也可以使用注解进行绑定,DIRECT 类型交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"} //key 是 routing key,只有发送的消息包含指定 key,这个队列才能收到此消息
))
public void listenDirectQueue1(String msg){
System.out.println("消费者接收到 direct.queue1 的消息:【" + msg + "】");
}
2)消息转换器
发送消息不仅可以发送字符串,还能发送对象,Spring 会把发送的消息序列化为字节发送给 MQ,接收消息的时候,还会把字节反序列化为 Java 对象,默认情况下 Spring 采用的序列化方式是 JDK 序列化,这种并不好,数据体积庞大,有安全漏洞,可读性差。所以我们需要 JSON 转换器
使用 JSON 转换器
- 生产者和消费者服务都引入
jackson-dataformat-xml
依赖 - 创建配置类,注入一个
MessageConverter
类型的对象,new Jackson2JsonMessageConverter()
来注入
3、消息可靠性
消息从生产者发送到交换机 exchange ,再到队列 queue,再到消费者,中间可能会发生消息的丢失问题
1)生产者消息确认
确保消息发送到队列中,发生宕机还会出现数据丢失无法到达消费者
exchange 收到消息返回 ack,没收到返回 nack,queue 失败可以使用 ReturnCallback 进行处理。由于消息很多,所以每个消息都要设置一个全局 id,比如使用 UUID
使用
- 修改消息生产者的 yml 配置文件,开启生产者确认机制,基于异步回调
- 定义 Return 回调:到队列的消息发送失败:每个 RabbitTemplate 只能配置一个 ReturnCallback,因此可以实现
ApplicationContextAware
接口在项目加载时进行配置 - 定义 ConfirmCallback:到交换机的消息发送失败:ConfirmCallback 可以在发送消息时指定,因为每个业务处理 confirm 成功或失败的逻辑不一定相同
yml
// 1.开启生产者确认机制
spring:
rabbitmq:
publisher-confirm-type: correlated #生产者的确认模式。异步回调。生产者在发送消息后,会等待 MQ 的确认消息,以确保消息已被成功接收和处理
publisher-returns: true #开启发布者返回机制,一般不需要开启
template:
mandatory: true #如果没有匹配的队列可以接收消息,MQ 会将消息返回给生产者
java
// 2.定义 Return 回调,路由失败的时候会触发,对于mq发送者来说是发送成功的
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 投递失败,记录日志
log.info("到队列的消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果有业务需要,可以重发消息
});
}
}
java
// 3.定义 ConfirmCallback,在生产消息时指定
public void testSendMessage2SimpleQueue() throws InterruptedException {
// 1.消息体
String message = "hello, spring amqp!";
// 2.全局唯一的消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 3.添加callback,到交换机的信息失败,然后自己可以根据业务写接下来的操作
correlationData.getFuture().addCallback(
result -> {
if(result.isAck()){
// 3.1.ack,消息成功
log.debug("消息发送成功, ID:{}", correlationData.getId());
}else{
// 3.2.nack,消息失败
log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
}
},
ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
);
// 4.发送消息
rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);
}
2)消息持久化
发生宕机会出现数据丢失无法到达消费者的情况,所以我们需要把内存中的数据持久化
要进行交换机持久化、队列持久化、消息持久化,在创建他们的时候可以指定持久化,但默认他们都是持久化的,所以正常创建即可
3)消费者消息确认
RabbitMQ 是阅后即焚机制,RabbitMQ 确认消息被消费者消费后会立刻删除,所以可能会出现消费者拿到消息处理出现异常导致消息丢失的问题,所以我们需要设置消费者消息确认
使用
- 在消费者 yml 配置文件中添加:
listener:simple:acknowledge-mode: auto
# none关闭ack;auto自动发送ack;manual我们自己发送ack - 当出现问题时,队列接受到失败信息 nack 就会
一直重发
消息直到收到成功消息 ack - 若是一直不成功一直重发会使 MQ 有很大压力,所以可以利用 Spring 的 retry 机制,在消费者出现异常时利用本地重试,如果本地重试指定次数后还不成功,默认就丢弃这个任务
- 若是一些重要的消息,不希望被丢弃,所以我们可以使用
MessageRecovery
接口来处理::RejectAndDontRequeueRecoverer:重试耗尽后,直接 reject,丢弃消息,默认就是这种方式;ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队;RepublishMessageRecoverer
:重试耗尽后,将失败消息投递到指定的交换机。一般使用第三种方案,消费者
将消息内容以及错误信息发送到新队列中,然后接受新队列消息,可以将消息发送给管理员来亲自处理
yml
# 3.spring 的 retry 机制配置
spring:
rabbitmq:
listener:
simple:
retry: # template也有retry,是连接mq失败的重试配置,阻塞式
enabled: true #开启消费者失败重试
initial-interval: 1000 #初始的失败等待时长为1秒
multiplier: 2 #失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 #最大重试次数,第一次1秒,第二次2秒,第三次4秒
stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false
java
// 4.将失败后的消息发送到指定交换机的队列中
// 提前声明好交换机和队列并绑定,都在配置类中进行
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
4、死信交换机
当消息被消费失败、超时无人消费、队列满无法投递时,消息就会变成死信,我们可以把死信通过死信所在的
队列
发送给指定的交换机,这个指定的交换机就是死信交换机,使用QueueBuilder.deadLetterExchange("dl.direct")
为队列指定死信交换机
结合 TTL 实现延迟接受消息
TTL:存活时间,可以为队列和消息设置,都有则以最短的为主,超过超时时间就会变成死信进入死信交换机,实现延迟接受消息
通过QueueBuilder.ttl(毫秒值)
方法为队列设置超时时间,通过MessageBuilder.setExpiration("毫秒值")
为消息设置超时时间
5、延迟队列
使用 TTL 结合死信交换机实现延迟接受消息过于复杂,可以使用 DelayExchange 插件来实现,它是基于交换机延迟转发消息来实现的
1)使用
- 声明一个交换机,添加 delayed 属性为 true
- 发送消息时,添加 x-delay 头,值为超时时间
2)其他延迟任务
JDK DelayQueue,JDK 中自带的延迟队列功能,存入队列的元素可以指定延迟执行的时间。实现简单,存在 JVM 内存中,可能丢失
Redisson,基于 Redis 数据结构模拟 JDK 的 DelayQueue。性能高,实现繁琐
时间轮算法可以实现延迟任务或定时任务。其中 Netty 中有开源的实现。自己编写算法实现,复杂
6、惰性队列
1)消息堆积问题
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题
解决思路:增加更多消费者,提高消费速度;扩大队列容积,提高堆积上限
2)惰性队列:扩大队列容积
Lazy Queues:接收到消息后直接存入磁盘而非内存;消费者要消费消息时才会从磁盘中读取并加载到内存;支持数百万条的消息存储
好处:消息上限高,性能比较稳定;坏处:基于磁盘存储,消息时效性会降低,性能受限于磁盘的 IO
可以使用命令让运行中的队列变成一个惰性队列,也可创建的时候使用QueueBuider.lazy()
方法创建一个惰性队列