MQ的可靠性

MQ的可靠性

在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:

  • 一旦MQ宕机,内存中的消息会丢失。
  • 内存空间有限,当消费者故障或者处理过慢时,会导致消息积压,引发MQ阻塞。
    在这里插入图片描述

解决方案

1.数据持久化
在早期MQ3.6之前,会采用这种模式,RabbitMQ实现数据持久化包括3个方面:

  • 交换机的持久化
    在这里插入图片描述
    Transient属于临时的交换机,重启后不存在。默认是持久化创建

  • 队列的持久化
    和交换机的持久化类似

  • 消息的持久化
    在这里插入图片描述

发送消息时,delivery_mode设置为2表示持久化消息。

2.Lazy Queue
要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可:

public Queue lazyQueue(){
	return QueueBuilder.durable("lazy.queue")
						.lazy()  //开启lazy模式
						.build();
}
@RabbitListener(queuesToDeclare=@Queue(
name="lazy.queue",durable="true",arguments=@Argument(name="x-queue-mode",value="lazy")))
public void listenLazyQueue(String msg){
	log.info("接收到lazy.queue的消息:{}",msg);
}

消费者确认机制

SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:

none: 不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
manual: 手动模式。需要自己在业务代码中调用api,发送ack或rehect,存在业务入侵,但更灵活
auto: 自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack.
当业务出现异常时,根据异常判断返回不同结果

  • 如果是业务异常,会自动返回nack
  • 如果是消息处理或校验异常,会自动返回reject
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: none

消息失败处理

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。
我们可以利用spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        retry:
          enable: true  #开启消费者失败重试
          initial-interval: 1000ms #初始的失败等待时长为 1秒
          multiplier: 1 #下次失败的等待时长倍数,下次等待时长=multiplier * last-interval
          max-attempts: 3 #最大重试次数
          stateless: true # true无状态;false有状态,如果业务中包含事务,这里改为false

失败消息处理策略

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer: 重试耗尽后,直接reject,丢弃消息。默认就是这种方式
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer: 重试耗尽后,将失败消息投递到指定的交换机。
    在这里插入图片描述

将失败处理策略改为RepublishMessageRecoverer:
①首先,定义接受失败消息的交换机,队列及其绑定关系,此处略;
②然后,定义ReplishMessageRecoverer:

@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled",havingValue = "true")
public class ErrorConfiguration {

    @Bean
    public DirectExchange errorExchange(){
        return new DirectExchange("error.direct");
    }

    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

    @Bean
    public Binding directQueueBindingerror(Queue errorQueue, DirectExchange errorExchange){
        return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
    }
    @Bean
    public MessageRecoverer recoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }

在这里插入图片描述

通过配置RepublishMessageRecoverer来设置本地重试后发往收集异常信息的交换机。同时可以通过@ConditionalOnProperty设置是否开启

总结

消费者如何保证消息一定被消费?

  • 开启消费者确认机制为auto,由spring确认消息处理成功后返回ack,异常时返回nack
  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理。

业务幂等性

幂等是一个数学概念,用函数表达式来描述是这样的:f(x)=f(f(x))。在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。

解决方案:
唯一消息id
方案一,是给每个消息都设置一个唯一id,利用id区分是否是重复消息:
① 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
② 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库。
③ 如果下次又收到相同的消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。

@Bean
    public MessageConverter messageConverter(){
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        //配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }

配置消息转换器时,可以设置自动写入唯一id

在这里插入图片描述
方案二,是结合业务逻辑,基于业务本身做判断。以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其他状态不做处理。在这里插入图片描述
问题:
1.如何保证支付服务和交易服务之间的订单状态一致性

  • 首先,支付服务会在用户支付成功后利用MQ消息通知交易服务,完成订单状态同步。
  • 其次,为了保证MQ消息的可靠性,我们采用了生产者确认机制,消费者确认,消费者失败重试等策略,确保消息投递和处理的可靠性。同时也开启了MQ的持久化,避免因为服务宕机导致的消息丢失。
  • 最后,我们还在交易服务更新订单状态时做了业务幂等性判断,避免因为消息重复消费导致订单状态异常。

2.如果交易服务消息处理失败,有没有什么兜底方案?

  • 我们可以在交易服务设置定时任务,定期查询订单支付状态。这样即使MQ通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。
### MQ 消息可靠性的实现方案与原理 #### 一、消息可靠性定义 消息可靠性是指在分布式系统中,确保消息能够被正确传递而不发生丢失或重复的现象。其核心目标是满足 **“不重不漏”** 的原则[^1]。 --- #### 二、常见实现方式 ##### 1. 生产端的可靠性保障 为了防止生产者发送的消息因网络或其他原因而丢失,通常采用以下措施: - **事务消息**:通过引入两阶段提交协议,在数据库和消息队列之间建立强一致性关系。只有当消息成功写入消息队列后,才会提交本地事务[^4]。 - **同步刷盘**:某些消息中间件(如 RocketMQ)提供同步刷盘模式,即每条消息都会先持久化到磁盘后再返回 ACK 给生产者,从而降低数据丢失风险[^3]。 ```python from rocketmq.client import Producer, Message producer = Producer('test_producer') producer.set_namesrv_addr('localhost:9876') def send_message(): msg = Message('TopicTest') msg.set_keys('Key') msg.set_body('Message body'.encode('utf-8')) ret = producer.send_sync(msg) # 同步发送并等待确认 print(f"Send Result Status:{ret.status}") ``` --- ##### 2. 存储端的可靠性设计 存储层的设计直接影响着整个系统的稳定性,以下是几种典型策略: - **多副本复制**:大多数现代消息队列都支持多副本机制(如 Kafka 和 RabbitMQ),通过将消息分发至多个节点来提高容错能力。一旦某个节点失效,其他备份节点可继续服务请求[^2]。 - **日志追加写法**:类似于 LSM 树结构的操作逻辑,每次新增记录均以顺序追加形式存入文件尾部,减少随机 IO 开销的同时增强了性能表现。 --- ##### 3. 消费端的一致性维护 为了避免消费者重复消费同一份数据或者遗漏部分重要信息,需采取如下手段加以防范: - **幂等性控制**:对于可能多次触发相同操作的任务场景,应提前规划好对应的去重校验规则,比如利用唯一 ID 或时间戳字段判断当前动作是否已经执行完毕。 - **手动签收机制**:允许客户端显式告知服务器已完成某项工作之后再清除对应缓存中的原始内容,这样即便中途出现问题也能及时发现并补救。 ```java // Java伪代码展示如何设置手动ACK模式 Channel channel = connection.createChannel(); channel.basicConsume(queueName, false, (consumerTag, message) -> { try { processMessage(message); channel.basicAck(message.getEnvelope().getDeliveryTag(), false); // 手动确认已接收 } catch (Exception e) { System.err.println("Failed to process and ack the message"); } }); ``` --- #### 三、解决实际问题的方法论 针对不同类型的错误情况,有相应的应对办法: | 错误类型 | 描述 | 推荐解决方案 | |----------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------| | 消息丢失 | 发送过程中由于各种意外状况造成的数据遗失 | 使用定时轮询补偿机制定期检查未完成状态下的任务列表,并重新发起尝试 | | 数据冗余 | 单次事件引发多重响应 | 设计具备防抖特性的接口函数;借助 Redis 缓存临时保存最近访问历史 | | 跨库协调失败 | 当涉及多方协作时难以达成共识 | 构建专门用于跟踪进度变化的日志表格,配合双相提交流程逐步推进直至最终定稿 | 上述提到的 “本地消息表” 方法就是一种典型的跨库一致性的实践案例[^5]。它通过额外增加一张映射关联关系的辅助型实体对象集合,用来记录待办事项的状态流转轨迹,进而达到全局视角下统一管理的目的。 --- ### 总结 综上所述,要构建一套健壮可靠的 MQ 系统架构并非易事,需要综合考量各方面因素共同作用的结果。从源头抓起做好预防准备固然重要,但事后救济同样不可或缺。唯有如此才能真正意义上做到全方位无死角覆盖所有潜在隐患点位。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值