大家好,我是苍何。
上一篇留了一个小问题,如果消费者出现异常,消费某一条消息失败,这时候 RocketMQ 会怎么处理呢?
你可能会用你聪明绝顶的脑袋瓜子想,苍何你是不是傻,失败了肯定重试啊,RocketMQ 一定有什么重试机制。
没错,RocketMQ 就是用重试机制来解决消息消费失败问题,那如果我问你重试都有哪些策略呢?生产端和消费端如何进行消息重试呢?
这个时候你肯定一脸懵逼,啥玩意?
别急,我们这一篇就专门来叨叨 RocketMQ 的消息重试机制。
在正式开始学(chui)习(bi)之前,有必要给大家推荐下 RocketMQ 官方的中文社区,阿里官方出品,收藏起来吧:https://rocketmq-learning.com/
什么情况下需要消息重试
消息重试的目的是为了保证消息的完整性,防止消息丢失,是业务兜底策略。什么情况下需要消息重试呢?
其实也就是什么情况下会发生消息丢失(毕竟不丢失就不需要重试嘛)
- 消息自身原因:
例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。
这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功。
所以最好提供一种定时重试机制,即过10秒后再重试。(而不是立马就重试)
- 业务处理失败:
依赖的下游应用服务不可用,例如 db 连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。
这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻Broker重试消息的压力。
消息重试能解决哪些问题
我们都知道消息队列一大核心作用就是异步解耦,也就是让上下游服务不耦合依赖,可以通过 mq 来进行业务逻辑关联处理。
但是如果下游服务消费消息出错了,这个时候,如何保证整个调用链路的完整性呢?不可能失败就失败了吧,比如订单消息给到 mq,库存系统消费来扣减库存。
库存系统消费消息的时候如果发生异常,那么对应的这一条订单就没有扣减库存,这是很严重的问题,明明已经卖出去了,库存里面居然还有,颇有一种用不完的感觉😂
这种情况已经严重影响到业务整个链路数据完整性了,也是我们最不希望看到的,所以就需要消息重试。
消息重试主要可以解决以下问题:
- 临时性故障处理: 当消费者因为网络波动、服务器暂时不可用等临时性问题无法正常消费消息时,重试机制可以在稍后再次尝试投递,提高消息处理的成功率。
- 业务处理异常: 如果消费者在处理消息时遇到业务逻辑异常,重试机制可以让消费者有机会在稍后再次处理该消息。
- 提高系统可靠性: 通过多次重试,可以降低因单次失败导致的消息丢失风险,提高整个消息系统的可靠性和稳定性。
- 应对突发流量: 在消费者遇到突发高流量无法及时处理所有消息时,重试机制可以帮助削峰填谷,让消息在稍后的低峰期被重新处理。
- 分布式事务处理: 在实现最终一致性的分布式事务中,消息重试可以作为一种补偿机制,确保事务的最终完成。
重试又分生产端的消息重试和消费端的消息重试,我们一起看看吧。
生产端的消息重试
生产者向 RocketMQ 的 broker 发送消息时,因自身原因如网络波动等导致没有发送成功,这时发送端没收到 RocketMQ 的 ACK 心跳包。
消息都没发出去,消费者自然收不到。
在生产端,针对不同的发送方式,会有不同的重试策略。
那好奇的你肯定会问,发送方式又有哪些呢?其实 RocketMQ 提供了 3 种发送方式,分别是:
- 同步发送
- 异步发送
- 单向发送
下面针对不同发送方式简单看下 RocketMQ 提供的重试策略吧:
同步发送
同步发送是 RocketMQ 默认的发送方式,默认调用 producer.Send () 方法不指定回调使用的都是同步发送,每次发送都需要等待响应,配合重试机制这种发送方式的可靠性是极高的,缺点当然是吞吐量低了。
RocketMQ 的同步发送默认 2 次重试(加上第一次发送,共3次尝试),重试间隔是 1 秒。
当然了,我们也可以修改默认重试次数和时间间隔:
// 同步发送消息,如果5秒内没有发送成功,则重试3次
DefaultMQProducer producer = new DefaultMQProducer("DefaultProducer");
producer.setRetryTimesWhenSendFailed(3);
producer.send(msg, 5000L);
异步发送
异步发送指的是生产者发送消息完后,不用等待 RocketMQ 服务端返回 ack,而是通过回调函数处理结果,所以需要指定回调函数,下面我写个发送的简单 demo:
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.start();Message msg = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET));// 异步发送
producer.send(msg, new SendCallback() {@Overridepublic void onSuccess(SendResult sendResult) {System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());}@Overridepublic void onException(Throwable e) {System.out.printf("%-10d Exception %s %n", index, e);e.printStackTrace();}
});// 等待一段时间以确保异步发送完成
producer.shutdown();
异步发送默认是不会进行消息重试的,因其本身无法立即确定发送状态,要想开启重试,也可以设置 setRetryTimesWhenSendFailed。
对于高吞吐量场景或非关键消息,可以考虑使用异步发送来提高性能。
单向发送
单向发送是指不关心发送结果的发送方式,因其不关心发送结果故而也不支持重试,很少有业务会使用单向发送。
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.start();Message msg = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET));// 单向发送
producer.sendOneway(msg);producer.shutdown();
PmHub 生产端消息如何重试
在 PmHub 中抽象了消息组件,所有消息发送都在 pmhub-base-notice 中。
因 PmHub 消息发送业务场景目前主要是基于企业微信的消息推送及流程状态通知,所以我们使用的是默认的发送方式,也即同步发送。
因无自定义重试需求,默认的 1 秒重试 1 次,一共重试 2 次也足够,所以我们并没做过多改动,所以 PmHub 用的是默认的同步发送以及默认的重试策略。
下面是具体的发送代码,大家也可以直接去 GitHub 中下载源码观看更佳:
/** * 推送到微信topic * */public static void push2Wx(com.laigeoffer.pmhub.base.notice.domain.entity.Message ob){ try { String key = IdUtil.simpleUUID(); ObjectMapper objectMapper = new ObjectMapper(); // 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。 String endpoint = addr; // 消息发送的目标Topic名称,需要提前创建。 String topic = WX_TOPIC; ClientServiceProvider provider = ClientServiceProvider.loadService(); ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(endpoint); ClientConfiguration configuration = builder.build(); // 初始化Producer时需要设置通信配置以及预绑定的Topic。 Producer producer = provider.newProducerBuilder() .setTopics(topic) .setClientConfiguration(configuration) .build(); // 普通消息发送。 Message message = provider.newMessageBuilder() .setTopic(topic) // 设置消息索引键,可根据关键字精确查找某条消息。 .setKeys(key) // 设置消息Tag,用于消费端根据指定Tag过滤消息。 .setTag(mqTag) // 消息体。 .setBody(objectMapper.writeValueAsString(ob).getBytes()) .build(); // 发送消息,需要关注发送结果,并捕获失败等异常。 SendReceipt sendReceipt = producer.send(message); LogFactory.get().info("Send message successfully, messageId={}", sendReceipt.getMessageId()); producer.close(); } catch (ClientException | IOException e) { LogFactory.get().error("推送微信消息时发生错误:", e); } }
生产端的消息是重试 hold 住了,但如果消费者出现异常,消费某一条消息失败,这时候 RocketMQ 会怎么处理呢?
我们下一篇会专门针对消费端的消息重试做更深入的了解,也是面试重灾区。
好啦,今天的分享结束。
我是苍何,这是图解 RocketMQ 教程的第 6 篇,我们下篇见~