在学习RabbitMQ之前,先认识以下两种服务调用:同步调用和异步调用。同步调用的优势在于:时效性强,等待到结果后返回,但同步调用拓展性差,当需求变更时都需要对代码进行修改,并且会导致性能下降以及出现级联失败问题。
异步调用的优势在于能够降低耦合,拓展性强;并且进行异步调用时,业务无需等待,性能好;能够做到故障隔离,下游服务故障不影响上游业务;缓存消息,能够做到流量削峰填谷。但异步调用的缺点也很明显:不能立即得到调用结果,时效性差;不能够确定下游任务是否执行成功;业务安全依赖于Broker消息代理的可靠性。
异步调用可以应用的场景:
1. 业务无需得到下游任务的结果
2. 下游任务等待时间过长
MQ技术选型
基本介绍
注意事项:
1. MQ中交换机仅用于路由转发消息,不能够存储消息,因此需要在交换机和队列之间建立关系从而将消息通过路由转发到对应的队列中。
2. 在RabbitMQ中只有将交换机和队列绑定之后两者才会建立连接,向这个交换机发送消息可以转发到其绑定的队列中。
快速入门
SpringAMQP是一个实现MQ各种操作接口框架,简化了使用JAVA代码编写RabbitMQ各种操作的代码,下面对使用JAVA代码简单实现发送接收消息
1. 发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void testSendMess2Que(){
String queueName = "simple.queue";
String msg = "hello,world";
// 向队列发送消息
rabbitTemplate.convertAndSend(queueName,msg);
}
通过RabbitTemplate提供的convertAndSend向指定的队列queueName发送消息msg。注意,在convertAndSend中msg的类型为Object,传递任何类型的消息都可以,其余交给spingAMQP。
2. 接收消息
@RabbitListener(queues = "simple.queue")
void testSendMes2Que(String msg){
System.out.println("消费者收到simple.queue消息:"+msg);
}
只需要给方法添加RabbitListener()注释并指定消息队列,springAMQP会自动从消息队列中取出消息并赋予给参数msg。
Work模型
1. 当向队列投递消息时,如果这个队列绑定了多个消费者,会采用轮询机制,每个消费者依次受到一个消息。
2. 每个消息只能消费一次
3. 如果每个消费者的处理消息能力不一样,仍然会采用轮询机制,可设置配置中preFetch值为1,确保同一时刻最多投递给消费者1条消息,这样只有在消费者处理完消息后再接收消息
4. 一个队列绑定多个消费者是为了加快消息处理速度,避免消息堆积问题。
面试问题:如何避免缓存堆积?
1. 采用work模型的队列绑定,多个消费者绑定一个队列,加快消息处理速度
2. 采用缓存
3. 优化业务代码
Fanout交换机
Direct交换机
Topic交换机
消息会通过交换机发送给Key为#.news的队列
利用JAVA代码创建MQ
问题:如果仅采用上述功能,当消费者处理消息过程中出现异常时,已经发送给消费者且未完成的消息会丢失,出现消息丢失问题。
消息可靠性问题
如果用户支付成功且更新支付状态后,将更新订单状态及以后交给消息队列进行异步处理,此时如果网络出现问题,发送的消息可能未消费,因此需保证每个消息至少消费一次。(不是所有业务都需要保证消息可靠性)在发送者、MQ和消费者中都可能出现可靠性问题。
生产者重连
上述重试只是在连接消息队列失败时,消息传输异常无法结果。
生产者确认
开启回调机制后需要编写回调函数,且每个生产者只能配置一个ReturnCallback,因此需要在项目启动过程中配置。
而ConfirmCallback由于针对每一个消息,因此每一个消息都需要指定一个ConfirmCallback。
其中每一个消息都有一个UUID生成的ID,在初始化CorrelationData会对cd分配消息id,onFailure不是指消息发送,而是消息回调失败,onSuccess也只是指消息回调成功
面试问题:如何保证向MQ发送消息可靠?
1. 可以开始生产者重连机制,避免因为网络波动造成连接失败的问题。
2. RabbitMQ提供了生产者确认机制,通过回调返回信息来判断消息是否入队成功,如果返回ack说明入队成功,如果返回nack说明消息发送失败,此时再针对失败原因进行修改
3. 但是生产者确认需要额外的网络和系统资源开销,除非在对消息可靠性有很高要求的业务场景下,尽量不要使用,如果一定要使用的话可以不开始Publisher-Return机制,因为路由失败一般是自己的业务问题,同时对于nack消息进行有限次数的重试,多次重试后依然失败则记录异常消息。
MQ可靠性
当队列堆满时,会将堆放时间久的消息移动到磁盘上,这个移动过程是阻塞的,因此当发生消息积压时会引发MQ阻塞
MQ可靠性解决方案:数据持久化、Lazy Queue
消息持久化将delevert mode设置为2
消息持久化后,消息会在磁盘和内存中各存储一份。
消费者可靠性
不需要手动实现消费者确认机制,SpringAMQP实现了消费者确认功能,允许通过配置文件选择ACK处理方式
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.dirct","error");
}
保证消息可靠性:生产者可靠性、MQ可靠性、消费者可靠性
业务幂等性
修改操作可以实现乐观锁
状态一致性总结
延迟消息
如可以解决超时未付款问题,可通过死信交换机和延迟插件实现,但延迟消息过多会给CPU带来巨大压力。
基于MQ的延迟消息适用于延迟时间较短的业务
取消超时订单
将一个原本30分钟的延迟消息改为多个时间较短的延迟消息,由于大部分订单都会在一分钟之内完成,因此避免了消息堆积造成MQ压力过大。