169. 如何保证MQ消息的可靠性(不丢失)


之前我们有一篇文章介绍过MQ的主要使用场景以及常见的问题和解决办法:
141. MQ的7种常用场景
111. 使用MQ可能存在的问题以及解决办法

思维导图如下:
在这里插入图片描述

本文主要介绍一下如何保证消息的可靠性,也就是消息不丢失

一、消息的使用过程

在这里插入图片描述
从上图不难看出,需要从三个方向考虑消息是否可靠投递并被消费了

  1. 如何确保生产者这边业务执行成功,消息一定会投递到MQ?
  2. 如何确保消息到达MQ后,MQ这边不会丢失?
  3. 如何确保消费者一定能消费到这条消息?

二、如何确保生产者这边业务执行成功,消息一定投递成功?

这块涉及到消息投递的整个过程,下面咱们来通过一个案例来了解消息投递的整个过程,以及在这个过程中,如何确保业务执行成功,消息一定会投递成功。

电商中有这样的一个场景

下单成功之后送积分的操作,我们使用mq来实现

下单成功之后,投递一条消息到mq,积分系统消费消息,给用户增加积分

下面会介绍4种方式,来看下这个业务中消息投递的一个过程。

消息投递方式一:业务事务中投递消息

正常流程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:投递消息到mq
  • step4:提交本地事务(这里仅控制DB语句是否回滚,只是是否回滚由step3是否成功决定)

这种方式是将发送消息放在了事务提交之前。

异常情况

  • step3发生异常:导致step4失败,商品下单失败,直接影响到商品下单业务
  • step4发生异常,其他step成功:商品下单失败,消息投递成功,给用户增加了积分

消息投递方式二:业务事务提交后、后投递消息

下面我们换种方式,我们将发送消息放到事务之后进行。

正常流程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:提交本地事务
  • step4:投递消息到mq

异常情况

  • step4发生异常,其他step成功:导致商品下单成功,投递消息失败,用户未增加积分

上面两种是比较常见的做法,也是最容易出错的。

消息投递方式三:事务消息(二阶段投递)

需要再本地业务库添加一张本地消息表(t_msg_record)

  • id
  • body:消息体
  • status:消息状态:0:待投递,1:投递成功,2:投递失败
  • 根据具体业务,可能还需要其他必要字段,用于消息重试发送

正常流程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:本地库中插入一条需要发送消息的记录t_msg_record,status为0(待投递)
  • step4:提交本地事务
  • step5:若事务提交成功,则投递消息到MQ,然后将t_msg_record中的status置为1(投递成功);若本地事务提交失败,则将t_msg_record表中的消息记录删掉

注:这里step2和step3是在同一个数据库中的不同表,可以直接用DB框架保证事务性

说明

这种方式借助了数据库的事务,业务和消息记录作为了一个原子操作,业务成功之后,消息记录必定是存在的。

异常情况

若step4成功,step5失败了,会导致下单业务执行成功,而消息投递失败,此时我们需要有个定时旁路job对待发送的消息进行补偿投递。

消息投递补偿job

这个job负责从本地t_msg_record表中查询出状态为0记录,重新投递。

对于投递失败的,采用衰减的方式进行重试,比如第1次失败了,则10秒后,继续重试,若还是失败,则再过20秒,再次重试,需要设置一个最大重试次数,最终还是投递失败,则需要告警+人工干预。

消息投递方式四:独立出来一个消息服务

增加一个消息服务消息库,负责消息的落库、将消息发送投递到mq,注意这里新增的一个消息服务可以是一个微服务(MQ消息中台)。

业务库需要添加一张消息日志表(t_msg_log)

  • id
  • bus_id:业务id
  • bus_type:业务类型

消息服务需要一张消息表(t_msg)

  • id:主键,消息id
  • msg_log_id:业务方t_msg_log表的id
  • body:消息体
  • msg_log_url:业务方t_msg_log记录回查的接口
  • status:状态,0:待投递,1:投递成功,2:投递失败
  • fail_msg:投递失败原因

消息投递的过程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:本地库t_msg_log表写入一条记录:insert into t_msg_log (bus_id,bus_type) values (‘订单id’,‘CREATE_ORDER’)
  • step4:调用消息服务,需携带(t_msg_log.id,消息体,消息日志回查的url),消息服务接收到请求后,向t_msg表插入记录(status=0,待发送),并返回消息id:msg_id
  • step5:提交本地事务
  • step6:如果上面都成功,使用step4中的msg_id调用消息服务,消息服务则将消息投递到mq中,修改消息记录状态为投递成功(t_msg.status=1);如果上面有失败的情况,则消息服务将消息删掉

可能存在的问题

若step6失败,消息服务t_msg表中的这条消息,将处于待发送状态,但是业务库订单已经生成了,以及t_msg_log表也是有记录的,对于这种情况,消息服务需新增一个job,对于t_msg表中记录为0的消息,拿到t_msg表中的msg_log_id去回查msg_log_url这个接口,去查一下业务库中的t_msg_log 表是否有记录,有记录说明业务是执行成功的,此时消息服务补发消息到MQ就可以了;对于回查不到的,有可能业务方本地事务还未提交,不能认定为业务方本地事务执行失败了,建议等到1天之后,再清理下这种消息。

三、如何确保消息到达MQ后,在MQ这边不会丢失?

有些MQ为了性能,收到消息后,会将消息放在内存中,并没有立即持久化到磁盘,此时MQ挂了,消息会丢失。

若要确保MQ收到消息后,消息不会丢失,则收到投递过来的消息后,立即持久化,这个操作基本上所有的MQ都是支持的,使用的时候配置一下就可以了。

为了防止MQ单节点故障,MQ还需要做主备,这样才可以最大限度的确保消息不会丢失。

四、消费者如何确保消息一定会被消费?

消费者这边可以采用下面的过程,可以确保消息一定会被消费。

  • step1:从MQ中拉取消息

  • step2:执行业务逻辑(需要做幂等)

  • step3:通知MQ删除这条消息

由于step3这个步骤涉及到网络通信,网络通信存在不可靠的因素,可能会失败,导致消息没有被删掉,就会出现该消息再次消费的情况,所以step2需要做幂等处理,这种方式可以确保消息必然会被成功消费一次。

### 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、付费专栏及课程。

余额充值