MQ高级应用(其一)

86 阅读7分钟

MQ高级应用

消息可靠性

当MQ数据出现消息丢失,怎么解决?

首先常见的出现问题的场景:

  • 发送消息时丢失:
    • 生产者发送消息时连接MQ失败
    • 生产者发送消息到达MQ后未找到Exchange
    • 生产者发送消息到达MQ的Exchange,未找到合适的Queue
    • 消息到达MQ后,处理消息的进程发生异常
  • MQ导致消息丢失:
    • 消息到达MQ,保存到队列后,尚未消费就突然宕机
  • 消费者处理消息时:
    • 消息接收后尚未处理突然宕机
    • 消息接收后处理过程中抛出异常

1. 生产消息可靠性

1.1 生产者重试机制

就是生产者发送消息时,出现了网络故障,导致与MQ的连接中断的问题 。

SpringAMQP提供的消息发送时的重试机制

rabbitmq:

connection-timeout: 1s # 设置MQ的连接超时时间

template:

retry:

enabled: true # 开启超时重试机制

initial-interval: 1000ms # 失败后的初始等待时间

multiplier[倍增]: 1 # 失败后下次的等待时长倍数,下次等待时长 = 上次等待时长 *

multiplier max-attempts: 3 # 总共尝试次数

重试是阻塞式的重试, 多次重试等待的过程中,当前线程是被阻塞的,

所以需要合理配置, 性能敏感责任需要考虑禁用,可以使用异步线程发送消息

1.2 生产者确认机制

双机制: publisher Confirm(确认)和Publisher Return(回调)

publisher Confirm: 消息投递成功返回ack,投递失败返回nack

Publisher Return: 消息投递成功但路由失败会调用Publisher Return回调方法返回异常信息

针对复杂情况,可以结合使用两种完成确认

情景其一:

消息投递成功但可能路由失败了,此时会通过Publisher Confirm返回ack,通过Publisher Return回调方法返回异常信息。(查看具体原因)

配置参数:

publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型

publisher-returns: true # 开启publisher return机制

- none:关闭confirm机制
- simple:同步阻塞等待MQ的回执(回调方法)
- correlated:MQ异步回调返回回执
1.3 发送失败处理机制

java程序接收:

ReturnCallback() [回调检视路由是否失败]

ConfirmCallback()[确认消息投递成功成败,-[RabbitClient 工具类]使用sendMsg方法发送correlationData,在correlationData对象中实现回调逻辑 ↓↓↓↓↓]

如果失败:==>返回nack 持久化在数据表 ,之后定时获取数据重新发送,重发成功删除表中数据,失败继续发, 重发次数累计, 超过界限,通过人工转发

2. 消息持久化

为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置持久化 .

2.1 交换机持久化

交换机持久化是指将交换机的定义信息(元数据)持久化到RabbitMQ的数据库,重启后交换机定义仍然存在.

在RMQ中交换机创建默认为持久化 (Durable) [ 对应临时模式Transient]

2.2 队列持久化

队列持久化也是将队列的定义信息(元数据)持久化到RabbitMQ的数据库中。队列中存储的是消息的在队列中的位置、消息的ID、存储位置等. 队列持久化不能保证消息数据不会丢失。

2.3 消息持久化

3. 消费消息可靠性

需要知道消费者的处理状态,因为消息投递给消费者并不代表就一定被正确消费

消息投递过程中可能出现的问题

  • 投递的过程中出现了网络故障
  • 收到消息后突然宕机
  • 收到消息后,因处理不当导致异常( )
3.1消费者确认机制

当消费者处理消息结束后,应该向RabbitMQ发送一个回执

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

一般reject方式用的较少[使用场景-消息存在失误需要RMQ来解决问题]

SpringAMQP可以通过配置文件设置消息确认的处理方式,有三种模式:

  • none:不处理。即消息投递给消费者后消息会立刻从MQ删除。非常不安全,不建议使用
  • manual:手动模式。需要自己在业务代码中调用api,发送ackreject,存在业务入侵,但更灵活
通过 api指定是否重新入队,具体返回ack或nack通过手动编程实现。
由于手动模式需要通过api编程,需要在监听方法添加Channel、Message类型的参数,如下:
Message:是spring AMQP封装的底层消息对象。
Channel:是消费端与MQ基于通道的操作对象。   
  • auto:自动模式。当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:
    • 业务异常,会自动返回nack
    • 消息处理或校验异常,自动返回reject返回
      • MessageConversionException(消息转换异常)
      • MethodArgumentTypeMismatchException(方法参数类型不匹配异常)等
rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 自动ack
manual模式测试
3.2 消费者失败重试机制

极端情况就是消费者一直无法执行成功,消息投递就会无限循环,导致mq的消息处理飙升,带来不必要的压力

rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval(最初时间间距): 1000ms # 初识的失败等待时长为1秒
          multiplier(倍增): 1 # 失败的等待时长倍数,下次等待时长 = 上次等待时长 * multiplier
          max-attempts: 3 # 最大重试次数

本地重试发送 1-2-4累计重试,失败后最终reject

失败数据之后去哪里呢?

Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:

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

===>推荐使用RepublishMessageRecoverer,将失败消息投递到固定的交换机,通过交换机将消息转发到失败消息队列,程序监听失败消息队列,接收到失败消息,将失败消息存入失败消息表,通过定时任务进行处理。

3.3MQ消息幂等性--消费重复问题
幂等性是什么?

在程序开发中,是指同一个业务,执行一次或多次对业务状态的影响是一致的,成为幂等性(数据影响一致性)

天然支持幂等性的操作

  • 根据id删除数据 一次, 二次删除[数据已经清空,所以重复的操作不影响]
  • 查询数据

非幂等性:

  • 添加操作: ==>多次执行,会插入多少数据==>唯一约束:只能解决一部分情况
  • 更新: ==> 重复执行可能就会出现数据不一致的问题
  • 业务相关:==>新建订单/库存扣除 由于诸多因素

数据的更新,新增一般是不符合幂等性,需:要进行额外处理

解决方案;

方案:数量统计 分布式锁

方案一: 唯一性ID -->数量统计 确定重复则放弃

方案二; (推荐) redis辅助存储

生产者发送消息设置携带id,若存在及证明有人正在消费,停止操作

如果不存在,表明为第一次消费,正常消费,并向存储Key 为消息id的数据存到redis,同时设置过期时间

业务判断幂等性:

保证性并非100%

举例;业务流转:

写入成功订单需要 更改支付状态就需要判断修改是否是未支付状态提前,,未对应,则状态流转失败

幂等性的适用性 情况一:运行之外的状态--宕机

支付状态确认-->手动确认/定时任务 手动则直接比对完成流程,或者定时任务自动确认

4、MQ可靠性保证并不是100%,所以有可能消费者没有收到MQ的消息,那么后续就无法执行

了。我们可以主动发起查询去获取最新结果,然后继续完成后面的业务操作.