MQ就是MessageQueue,存放消息的队列,也就是异步调用中的Broker。
在日常开发过程中,常见的消息队列有四种,RabbitMQ、ActiveMQ、RocketMQ、Kafka。 这四中的对比性下图可以看到,其中RabbitMQ是Rabbit公司专门研究的,相较于其他消息中间件它支持SMTP协议,并且它的消息延迟更是达到了恐怖的微秒级。当然它的消息可靠性以及可用性也是非常高的,所以一般项目开发没有特殊要求都是使用的是RabbitMQ。
交换机和队列都有自己的VirtualHost,不同的VirtualHost都有自己不同的交换机和队列。一个MQ中可以有多个VirtualHost,在发消息的时候去连接对应的VirtualHost就行。每个user可以去操作自己创建的的VirtualHost,查看的话时根据管理员创建user时分配的权限决定。
SpringAMQP
<!--RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: # 用户名
password: # 密码
加入RabbitMQ依赖,在yml文件中配置,然后通过RabbitTemple向队列中发送消息。
默认情况下,RabbitMQ会将消息依次轮询投递给绑定在队列上的每一个消费者。并没有考虑到消费者是否已经处理完消息,这种情况可能出现的问题就是当我们不知道消费者消费能力的时候容易出现消息堆积。比如此时有两个消费者,消费者a一秒钟可以处理50条数据,消费者b一秒钟只能处理5条数据,此时有1000条消息发送到队列中,一次轮询绑定消费者,每一个消费者绑定了500个数据,但是消费者a10秒钟就处理完成,此时消费者b还在处理消息,a此时就空闲着,可用性比较低。
因此我们需要在yml中设置prefetch值为1,确保同一时刻最多投递给消费者1条消息,处理完之后才能获取下一条消息。
Rabbitmq:
listener:
simple:
prefetch: 1
交换机主要分为三种类型:Fanout(广播)、Direct(定向)、Topic(话题)。
1. Spring AMQP提供了几个类,用来声明队列、交换机以及其绑定关系。
@Bean
public FanoutExchange fanoutExchange(){
// ExchangeBuilder.fanoutExchange("").build();
return new FanoutExchange("shuqg.fanout2");
}
@Bean
public Queue fanoutQueue3(){
// QueueBuilder.durable("").build();
return new Queue("shuqg.queue3");
}
@Bean
public Binding fanoutBinging3(Queue fanoutqueue3, FanoutExchange fanoutExchange){
// 如果需要绑定bindingkey在后面.with("")
return BindingBuilder.bind(fanoutqueue3).to(fanoutExchange);
}
2. 基于注解声明
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2", durable = "true"),
exchange = @Exchange(name = "shuqg.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) throws InterruptedException {
System.out.println("消费者2 收到了 direct.queue2的消息:【" +msg+ "】");
Thread.sleep(200);
}
Spring对消息处理默认实现的是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
但是存在以下问题,JDK的序列化有安全风险、JDK序列化的消息太大、JDK序列化的消息可读性差。
建议采用JSON序列化代替默认序列化,在SpringAMQP中有JSON的接口,只不过没有生效,我们只需要引入JSON依赖。
com.fasterxml.jackson.core
jackson-databind
然后在publisher和consumer中都要配置MessageConverter
@Bean
public MessageConverter jacksonMessageConvertor(){
return new Jackson2JsonMessageConverter();
}
有时候由于网络波动,可能会出现客户端连接MQ失败的情况。我们可以通过配置开启失败后的重连机制。 当网络不稳定时,使用重试机制可以有效提高消息发送成功概率。不过SpringAMQP消息重试机制是阻塞式的重试,也就是多次重试等待过程中,线程是被阻塞的,会影响业务性能。如果对业务性能有要求,建议禁用重试机制,如果要使用就合理配置等待时长和重试次数,也可以考虑使用异步线程来执行发送消息的代码。
rabbitmq:
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
RabbitMQ有Publisher Confirm和Publisher Return两种确认机制。开启确认机制后,在MQ成功发送消息后返回确认消息给生产者。
配置生产者确认机制
rabbitmq:
publisher-confirm-type: correlated # 开启publisher-confirm机制,并设置confirm类型
# 这里有三种参数,默认none关闭,其次simple是同步阻塞等待MQ回执消息,然后是correlated是MQ异步回调方式返回回执消息。
publisher-returns: true # 开启publisher return机制
每一个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置
@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());
});
}
}
生产者发送消息
@Test
void testConfirmCallback() throws InterruptedException {
// 创建cd
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
// 添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback() {
@Override
public void onFailure(Throwable ex) {
log.error("消息回调失败", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
log.debug("收到confirm callback回执");
if (result.isAck()) {
// 消息发送成功
log.debug("消息发送成功,返回ack");
}else{
// 消息发送失败
log.error("消息发送失败,返回nack,原因{}", result.getReason());
}
}
});
rabbitTemplate.convertAndSend("shuqg.direct", "red","hello", cd);
Thread.sleep(2000);
}
生产者确认需要额外的网络和系统资源开销,尽量不要使用。如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务的问题。对于nack消息可以设置有限的重试次数,依然失败则记录异常消息到日志中。
RabbitMQ如何保证消息可靠性
在默认情况下,RabbitMQ会将接收到的消息保存到内存中以降低消息收发的延迟。这样会导致两个问题:
数据持久化
LazyQueue
从mq3.6之后开始增加了LazyQueue的概念,也就是惰性队列。惰性队列有以下特点:
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement),当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。
开启消费者确认机制为auto,由Spring确认消息处理成功后返回ack。开启消费者确认机制,RabbitMQ支持消费者确认机制,当消费者处理消息之后可以向MQ发送ack回执,MQ收到ack回执之后才会去删除该消息。 SpringAMQP中允许配置三种确认模式:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: none # none, manual手动, auto自动
当消费者出现异常后,会不断requeue(重新入队到队列),再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq消息处理飙升,带来不必要的压力。 我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
幂等性是一个数学概念,就是f(x) = f(f(x)),在程序开发中,指的是同一个业务,执行一次或多次对业务状态的影响是一致的。重复消费问题。
查询删除这些业务天生就是幂等的,新增修改这些业务就不是幂等的。
给每一个消息都设置一个唯一id,利用id判断是否重复消费
每一个消息都生成一个唯一id,与消息一起投递给消费者。
消费者接收到消息后处理自己的业务,业务处理成功后将消息id保存到数据库中。
如果下次又收到相同的消息,去数据库查询判断是否存在,存在则为重复消息,放弃处理。
使用自带的Jackson2JsonMessageConverter,可以实现自动生成唯一id,当将CreateMessageIds设置为true,底层会自动创建唯一id,并返回。
@Bean
public MessageConverter jacksonMessageConverter(){
// 定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 配置自动创建消息id,用于识别不同消息,也可以在业务中基于id判断是否重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}
业务判断
如何保证支付服务与交易服务之间的订单状态一致性
使用MQ完成订单状态同步->为了保证mq可靠,使用了生产者确认,消费者确认,生产者重试,同时开启mq持久化,最后做了幂等性判断。
可能导致消息丢失的场景:生产者发送消息没有到达交换机或者没有到达队列,MQ宕机,消费者服务宕机。
开启生产者确认机制,确保生产者的消息能到达队列。RabbitMQ中提供了一个确认机制用来避免消息发送到MQ过程中丢失,消息发送到MQ之后,会返回一个结果给发送者,表示消息是否处理成功。
开启消息持久化功能,确保消息未消费前在队列中不会丢失。MQ默认是在内存中存储消息,开启持久化功能可以将数据存储在磁盘上,即使MQ宕机或重启也不会丢失数据。
开启消费者确认机制为auto,由Spring确认消息处理成功后返回ack。
开启消费者确认机制,RabbitMQ支持消费者确认机制,当消费者处理消息之后可以向MQ发送ack回执,MQ收到ack回执之后才会去删除该消息。SpringAMQP中允许配置三种确认模式:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: none # none, manual手动, auto自动
一般使用在下单的时候,当下单之后当下单之后会有一个过期的时间,当在指定时间内未支付,就会将这个订单销毁。如果使用定时任务,设置key value在redis中设置过期时间,我们需要定时去查询数据库中用户支付状态,如果到达过期时间还没有支付,就会删除订单表,这个时候,如果设置时间间隔较短,对数据库的压力会非常巨大,但是如果设置间隔时间较长,就会导致时效性较差。
延迟队列就是进入队列的消息会被延迟消费的队列,我们当时的某一个业务使用到了延迟队列(超时订单、限时优惠、定时发布。。)
其中延迟队列就用到了死信交换机和TTL实现的。
当队列中的消息满足下面情况之一,就可以成为死信
一般死信消息是会被直接丢弃的,但是我们可以给该队列配置一个dead-letter-exchange属性,指定一个交换机,队列中的死信就会投递到该交换机中,这个交换机就是死信交换机。这个交换机也可以绑定一个队列,死信消息可以直接从交换机投递到该队列中,其他消费者可以去消费该队列中的消息。
RabbitMQ中有一个延迟队列插件实现延迟队列DelayExchange
产生消息堆积的情况,当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信。可能会被丢弃,这就是消息堆积。