消息消费失败如何处理?

开发 前端
在介绍消息中间件 MQ 之前,我们先来简单的了解一下,为何要引用消息中间件。

[[384109]]

本文转载自微信公众号「Java极客技术」,作者鸭血粉丝。转载本文请联系Java极客技术公众号。   

一、介绍

在介绍消息中间件 MQ 之前,我们先来简单的了解一下,为何要引用消息中间件。

例如,在电商平台中,常见的用户下单,会经历以下几个流程。

当用户下单时,创建完订单之后,会调用第三方支付平台,对用户的账户金额进行扣款,如果平台支付扣款成功,会将结果通知到对应的业务系统,接着业务系统会更新订单状态,同时调用仓库接口,进行减库存,通知物流进行发货!

 

试想一下,从订单状态更新、到扣减库存、通知物流发货都在一个方法内同步完成,假如用户支付成功、订单状态更新也成功,但是在扣减库存或者通知物流发货步骤失败了,那么就会造成一个问题,用户已经支付成功了,只是在仓库扣减库存方面失败,从而导致整个交易失败!

一单失败,老板可以假装看不见,但是如果上千个单子都因此失败,那么因系统造成的业务损失,将是巨大的,老板可能坐不住了!

因此,针对这种业务场景,架构师们引入了异步通信技术方案,从而保证服务的高可用,大体流程如下:

 

当订单系统收到支付平台发送的扣款结果之后,会将订单消息发送到 MQ 消息中间件,同时也会更新订单状态。

在另一端,由仓库系统来异步监听订单系统发送的消息,当收到订单消息之后,再操作扣减库存、通知物流公司发货等服务!

在优化后的流程下,即使扣减库存服务失败,也不会影响用户交易。

正如《人月神话》中所说的,软件工程,没有银弹!

当引入了 MQ 消息中间件之后,同样也会带来另一个问题,假如 MQ 消息中间件突然宕机了,导致消息无法发送出去,那仓库系统就无法接受到订单消息,进而也无法发货!

针对这个问题,业界主流的解决办法是采用集群部署,一主多从模式,从而实现服务的高可用,即使一台机器突然宕机了,也依然能保证服务可用,在服务器故障期间,通过运维手段,将服务重新启动,之后服务依然能正常运行!

但是还有另一个问题,假如仓库系统已经收到订单消息了,但是业务处理异常,或者服务器异常,导致当前商品库存并没有扣减,也没有发货!

这个时候又改如何处理呢?

今天我们所要介绍的正是这种场景,假如消息消费失败,我们应该如何处理?

二、解决方案

针对消息消费失败的场景,我们一般会通过如下方式进行处理:

  • 当消息消费失败时,会对消息进行重新推送
  • 如果重试次数超过最大值,会将异常消息存储到数据库,然后人工介入排查问题,进行手工重试

 

当消息在客户端消费失败时,我们会将异常的消息加入到一个消息重试对象中,同时设置最大重试次数,并将消息重新推送到 MQ 消息中间件里,当重试次数超过最大值时,会将异常的消息存储到 MongoDB数据库中,方便后续查询异常的信息。

基于以上系统模型,我们可以编写一个公共重试组件,话不多说,直接干!

三、代码实践

本次补偿服务采用 rabbitmq 消息中间件进行处理,其他消息中间件处理思路也类似!

3.1、创建一个消息重试实体类

  1. @Data 
  2. @EqualsAndHashCode(callSuper = false
  3. @Accessors(chain = true
  4. public class MessageRetryDTO implements Serializable { 
  5.  
  6.     private static final long serialVersionUID = 1L; 
  7.  
  8.     /** 
  9.      * 原始消息body 
  10.      */ 
  11.     private String bodyMsg; 
  12.  
  13.     /** 
  14.      * 消息来源ID 
  15.      */ 
  16.     private String sourceId; 
  17.  
  18.     /** 
  19.      * 消息来源描述 
  20.      */ 
  21.     private String sourceDesc; 
  22.  
  23.     /** 
  24.      * 交换器 
  25.      */ 
  26.     private String exchangeName; 
  27.  
  28.     /** 
  29.      * 路由键 
  30.      */ 
  31.     private String routingKey; 
  32.  
  33.     /** 
  34.      * 队列 
  35.      */ 
  36.     private String queueName; 
  37.  
  38.     /** 
  39.      * 状态,1:初始化,2:成功,3:失败 
  40.      */ 
  41.     private Integer status = 1; 
  42.  
  43.     /** 
  44.      * 最大重试次数 
  45.      */ 
  46.     private Integer maxTryCount = 3; 
  47.  
  48.     /** 
  49.      * 当前重试次数 
  50.      */ 
  51.     private Integer currentRetryCount = 0; 
  52.  
  53.     /** 
  54.      * 重试时间间隔(毫秒) 
  55.      */ 
  56.     private Long retryIntervalTime = 0L; 
  57.  
  58.     /** 
  59.      * 任务失败信息 
  60.      */ 
  61.     private String errorMsg; 
  62.  
  63.     /** 
  64.      * 创建时间 
  65.      */ 
  66.     private Date createTime; 
  67.  
  68.     @Override 
  69.     public String toString() { 
  70.         return "MessageRetryDTO{" + 
  71.                 "bodyMsg='" + bodyMsg + '\'' + 
  72.                 ", sourceId='" + sourceId + '\'' + 
  73.                 ", sourceDesc='" + sourceDesc + '\'' + 
  74.                 ", exchangeName='" + exchangeName + '\'' + 
  75.                 ", routingKey='" + routingKey + '\'' + 
  76.                 ", queueName='" + queueName + '\'' + 
  77.                 ", status=" + status + 
  78.                 ", maxTryCount=" + maxTryCount + 
  79.                 ", currentRetryCount=" + currentRetryCount + 
  80.                 ", retryIntervalTime=" + retryIntervalTime + 
  81.                 ", errorMsg='" + errorMsg + '\'' + 
  82.                 ", createTime=" + createTime + 
  83.                 '}'
  84.     } 
  85.  
  86.     /** 
  87.      * 检查重试次数是否超过最大值 
  88.      * 
  89.      * @return 
  90.      */ 
  91.     public boolean checkRetryCount() { 
  92.         retryCountCalculate(); 
  93.         //检查重试次数是否超过最大值 
  94.         if (this.currentRetryCount < this.maxTryCount) { 
  95.             return true
  96.         } 
  97.         return false
  98.     } 
  99.  
  100.     /** 
  101.      * 重新计算重试次数 
  102.      */ 
  103.     private void retryCountCalculate() { 
  104.         this.currentRetryCount = this.currentRetryCount + 1; 
  105.     } 
  106.  

3.2、编写服务重试抽象类

  1. public abstract class CommonMessageRetryService { 
  2.  
  3.     private static final Logger log = LoggerFactory.getLogger(CommonMessageRetryService.class); 
  4.  
  5.     @Autowired 
  6.     private RabbitTemplate rabbitTemplate; 
  7.  
  8.     @Autowired 
  9.     private MongoTemplate mongoTemplate; 
  10.  
  11.  
  12.     /** 
  13.      * 初始化消息 
  14.      * 
  15.      * @param message 
  16.      */ 
  17.     public void initMessage(Message message) { 
  18.         log.info("{} 收到消息: {},业务数据:{}", this.getClass().getName(), message.toString(), new String(message.getBody())); 
  19.         try { 
  20.             //封装消息 
  21.             MessageRetryDTO messageRetryDto = buildMessageRetryInfo(message); 
  22.             if (log.isInfoEnabled()) { 
  23.                 log.info("反序列化消息:{}", messageRetryDto.toString()); 
  24.             } 
  25.             prepareAction(messageRetryDto); 
  26.         } catch (Exception e) { 
  27.             log.warn("处理消息异常,错误信息:", e); 
  28.         } 
  29.     } 
  30.  
  31.     /** 
  32.      * 准备执行 
  33.      * 
  34.      * @param retryDto 
  35.      */ 
  36.     protected void prepareAction(MessageRetryDTO retryDto) { 
  37.         try { 
  38.             execute(retryDto); 
  39.             doSuccessCallBack(retryDto); 
  40.         } catch (Exception e) { 
  41.             log.error("当前任务执行异常,业务数据:" + retryDto.toString(), e); 
  42.             //执行失败,计算是否还需要继续重试 
  43.             if (retryDto.checkRetryCount()) { 
  44.                 if (log.isInfoEnabled()) { 
  45.                     log.info("重试消息:{}", retryDto.toString()); 
  46.                 } 
  47.                 retrySend(retryDto); 
  48.             } else { 
  49.                 if (log.isWarnEnabled()) { 
  50.                     log.warn("当前任务重试次数已经到达最大次数,业务数据:" + retryDto.toString(), e); 
  51.                 } 
  52.                 doFailCallBack(retryDto.setErrorMsg(e.getMessage())); 
  53.             } 
  54.         } 
  55.     } 
  56.  
  57.     /** 
  58.      * 任务执行成功,回调服务(根据需要进行重写) 
  59.      * 
  60.      * @param messageRetryDto 
  61.      */ 
  62.     private void doSuccessCallBack(MessageRetryDTO messageRetryDto) { 
  63.         try { 
  64.             successCallback(messageRetryDto); 
  65.         } catch (Exception e) { 
  66.             log.warn("执行成功回调异常,队列描述:{},错误原因:{}", messageRetryDto.getSourceDesc(), e.getMessage()); 
  67.         } 
  68.     } 
  69.  
  70.     /** 
  71.      * 任务执行失败,回调服务(根据需要进行重写) 
  72.      * 
  73.      * @param messageRetryDto 
  74.      */ 
  75.     private void doFailCallBack(MessageRetryDTO messageRetryDto) { 
  76.         try { 
  77.             saveMessageRetryInfo(messageRetryDto.setErrorMsg(messageRetryDto.getErrorMsg())); 
  78.             failCallback(messageRetryDto); 
  79.         } catch (Exception e) { 
  80.             log.warn("执行失败回调异常,队列描述:{},错误原因:{}", messageRetryDto.getSourceDesc(), e.getMessage()); 
  81.         } 
  82.     } 
  83.  
  84.     /** 
  85.      * 执行任务 
  86.      * 
  87.      * @param messageRetryDto 
  88.      */ 
  89.     protected abstract void execute(MessageRetryDTO messageRetryDto); 
  90.  
  91.     /** 
  92.      * 成功回调 
  93.      * 
  94.      * @param messageRetryDto 
  95.      */ 
  96.     protected abstract void successCallback(MessageRetryDTO messageRetryDto); 
  97.  
  98.     /** 
  99.      * 失败回调 
  100.      * 
  101.      * @param messageRetryDto 
  102.      */ 
  103.     protected abstract void failCallback(MessageRetryDTO messageRetryDto); 
  104.  
  105.     /** 
  106.      * 构建消息补偿实体 
  107.      * @param message 
  108.      * @return 
  109.      */ 
  110.     private MessageRetryDTO buildMessageRetryInfo(Message message){ 
  111.         //如果头部包含补偿消息实体,直接返回 
  112.         Map<String, Object> messageHeaders = message.getMessageProperties().getHeaders(); 
  113.         if(messageHeaders.containsKey("message_retry_info")){ 
  114.             Object retryMsg = messageHeaders.get("message_retry_info"); 
  115.             if(Objects.nonNull(retryMsg)){ 
  116.                 return JSONObject.parseObject(String.valueOf(retryMsg), MessageRetryDTO.class); 
  117.             } 
  118.         } 
  119.         //自动将业务消息加入补偿实体 
  120.         MessageRetryDTO messageRetryDto = new MessageRetryDTO(); 
  121.         messageRetryDto.setBodyMsg(new String(message.getBody(), StandardCharsets.UTF_8)); 
  122.         messageRetryDto.setExchangeName(message.getMessageProperties().getReceivedExchange()); 
  123.         messageRetryDto.setRoutingKey(message.getMessageProperties().getReceivedRoutingKey()); 
  124.         messageRetryDto.setQueueName(message.getMessageProperties().getConsumerQueue()); 
  125.         messageRetryDto.setCreateTime(new Date()); 
  126.         return messageRetryDto; 
  127.     } 
  128.  
  129.     /** 
  130.      * 异常消息重新入库 
  131.      * @param retryDto 
  132.      */ 
  133.     private void retrySend(MessageRetryDTO retryDto){ 
  134.         //将补偿消息实体放入头部,原始消息内容保持不变 
  135.         MessageProperties messageProperties = new MessageProperties(); 
  136.         messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); 
  137.         messageProperties.setHeader("message_retry_info", JSONObject.toJSON(retryDto)); 
  138.         Message message = new Message(retryDto.getBodyMsg().getBytes(), messageProperties); 
  139.         rabbitTemplate.convertAndSend(retryDto.getExchangeName(), retryDto.getRoutingKey(), message); 
  140.     } 
  141.  
  142.  
  143.  
  144.     /** 
  145.      * 将异常消息存储到mongodb中 
  146.      * @param retryDto 
  147.      */ 
  148.     private void saveMessageRetryInfo(MessageRetryDTO retryDto){ 
  149.         try { 
  150.             mongoTemplate.save(retryDto, "message_retry_info"); 
  151.         } catch (Exception e){ 
  152.             log.error("将异常消息存储到mongodb失败,消息数据:" + retryDto.toString(), e); 
  153.         } 
  154.     } 

3.3、编写监听服务类

在消费端应用的时候,也非常简单,例如,针对扣减库存操作,我们可以通过如下方式进行处理!

  1. @Component 
  2. public class OrderServiceListener extends CommonMessageRetryService { 
  3.  
  4.     private static final Logger log = LoggerFactory.getLogger(OrderServiceListener.class); 
  5.  
  6.     /** 
  7.      * 监听订单系统下单成功消息 
  8.      * @param message 
  9.      */ 
  10.     @RabbitListener(queues = "mq.order.add"
  11.     public void consume(Message message) { 
  12.         log.info("收到订单下单成功消息: {}", message.toString()); 
  13.         super.initMessage(message); 
  14.     } 
  15.  
  16.  
  17.     @Override 
  18.     protected void execute(MessageRetryDTO messageRetryDto) { 
  19.         //调用扣减库存服务,将业务异常抛出来 
  20.     } 
  21.  
  22.     @Override 
  23.     protected void successCallback(MessageRetryDTO messageRetryDto) { 
  24.         //业务处理成功,回调 
  25.     } 
  26.  
  27.     @Override 
  28.     protected void failCallback(MessageRetryDTO messageRetryDto) { 
  29.         //业务处理失败,回调 
  30.     } 

当消息消费失败,并超过最大次数时,会将消息存储到 mongodb 中,然后像常规数据库操作一样,可以通过 web 接口查询异常消息,并针对具体场景进行重试!

四、小结

可能有的同学会问,为啥不将异常消息存在数据库?

起初的确是存储在 MYSQL 中,但是随着业务的快速发展,订单消息数据结构越来越复杂,数据量也非常的大,甚至大到 MYSQL 中的 text 类型都无法存储,同时这种数据结构也不太适合在 MYSQL 中存储,因此将其迁移到 mongodb!

本文主要围绕消息消费失败这种场景,进行基础的方案和代码实践讲解,可能有理解不到位的地方,欢迎批评指出!

五、参考

 

1、石杉的架构笔记 - 如何处理消息消费失败问题

 

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2012-07-03 11:18:20

运维disable tab

2018-12-25 09:44:42

2019-08-15 10:20:19

云计算技术安全

2012-12-12 09:49:41

2017-03-13 13:21:34

Git处理大仓库

2020-12-29 09:11:33

LinuxLinux内核

2019-12-23 10:20:12

Web图片优化前端

2017-10-26 08:43:18

JavaScript内存处理

2022-06-02 10:54:16

BrokerRocketMQ

2010-05-17 10:04:45

2023-07-03 13:50:13

ReactonResize事件

2021-05-31 10:47:17

SpringSecuritySession

2022-04-19 09:00:52

ReactTypeScript

2011-02-28 14:08:31

网速变慢局域网网速

2021-03-24 10:40:26

Python垃圾语言

2023-01-04 10:01:21

ReactTypeScript元素

2024-04-16 13:32:57

2010-09-01 15:40:10

DHCP服务器

2010-06-22 15:06:23

2011-12-15 12:32:19

JavaNIO
点赞
收藏

51CTO技术栈公众号