使用RocketMQ的小细节

消息过滤

说到消息过滤,就不得不说到 tag。没错,就是我们之前在专业术语中提到过的 tag。也称为消息标签,用来标记 Topic 下的不同用途的消息。

在 RocketMQ 中消费者是可以按照 Tag 对消息进行过滤。举个电商交易场景的例子,用户下完订单之后,在后台会产生一系列的消息,比如说订单消息、支付消息和物流消息。假设这些消息都发送到 Topic 为 Trade 中,同时用 tag 为 order 来标记订单消息,用 tag 为 pay 来标记支付消息,用 tag 为 logistics 来标记物流消息。需要支付消息的支付系统(相当于一个 consumer)订阅 Trade 中 tag 为 pay 的消息,此时,broker 则只会把 tag 为 pay 的消息投递给支付系统。而如果是一个实时计算系统,它可能需要接收所有和交易相关的消息,那么只要它订阅 Trade 中 tag 为 order、pay、logistics 的消息,broker 就会把带有这些 tag 的消息投递给实时计算系统。

对于消息分类,我们可以选择创建多个 Topic 来区分,也可以选择在同一个 Topic 下创建多个 tag 来区分。这两种方式都是可行的,但是一般情况下,不同的 Topic 之间的消息是没有什么必然联系的,使用 tag 来区分同一个 Topic 下相互关联的消息则更加合适一些。

订阅关系一致性

讲完了消息过滤,我们接着讲讲什么是订阅关系一致性呢?其实在讲 RocketMQ 消费模式的时候提到过,除了使用同一个 group name,订阅的 tag 也必须是一样的,只有符合这两个条件的 consumer 实例才能组成 consumer 集群。这里所说的其实就是订阅关系一致性。在 RocketMQ 中,订阅关系由 Topic和 Tag 组成,因此要保证订阅关系一致性,就必须同时保证这两点:

  1. 订阅的 Topic 必须一致

  2. 订阅的 Topic 中的 tag 必须一致

保证订阅关系一致性是非常重要的,一旦订阅关系不一致,消息消费的逻辑就会混乱,甚至导致消息丢失,这对于大部分业务场景来说都是不允许的,甚至是致命的。在实际使用中,切记同一个消费者集群内的所有消费者实例务必要保证订阅关系的一致性。

图 1

备注:图中 “*” 代表订阅该Topic下所有的 tag。

我们用具体的例子来解释一下,如图 1 所示,消费者集群中有 3 个 consumer 实例,分别为 C1、C2、C3,各自订阅的 topic 和 tag 各不相同。首先 C1 和 C2 都订阅 TopicA,满足了订阅关系一致性的第一点,但是 C1 订阅的是 TopicA 的 Tag1,而 C2 订阅的是 TopicA 的 Tag2,不满足订阅关系一致性的第二点,所以 C1、C2 不满足订阅关系一致性。而 C3 订阅的 Topic 和 Tag 都与 C1 和 C2不一样,同样也不满足订阅关系一致性。

图 2

备注:图中 “||” 用来连接不用的 tag,表示与的意思。

在图 2 中,消费者集群中有 3 个 consumer 实例,分别为 C1、C2、C3,都是订阅 TopicA 下的 Tag1 和 Tag2,满足了订阅关系一致性的两点要求,所以满足了订阅关系一致性。

图 3

如图 3 所示,一个 consumer 也可以订阅多个 Topic,同时也必须保证该 consumer 集群里的多个消费者实例的订阅关系一致性,才不会造成不必要的麻烦。

总结

在实际使用中,消息过滤可以帮助我们只消费我们所需要的消息,这是在broker端就帮我们处理好的,大大减少了在 consumer 端的消息过滤处理,一方面减少了代码量,另一方面更减少了不必要消息的网络传输消耗。

订阅消息一致性则保证了同一个消费者集群中 consumer 实例的正常运行,避免消息逻辑的混乱和消息的丢失。所以在实际使用中,在 producer 端要做好消息的分类,便于 consumer 可以使用 tag 进行消息的准确订阅,而在 consumer 端,则要保证订阅关系一致性。

消息重试

首先明确之前说过的,消息重试只针对集群消费模式,广播消费没有消息重试的特性,消费失败之后,只会继续消费下一条消息。这也是为什么我们一再强调,推荐大家使用集群消费模式,其消息重试的特性能给开发者带来极大的方便。

那么什么是消息重试呢?简单来说,就是当消费者消费消息失败后,broker 会重新投递该消息,直到消费成功。在 RocketMQ 中,当消费者使用集群消费模式时,消费者接收到消息并进行相应的逻辑处理之后,最后都要返回一个状态值给 broker。这样 broker 才知道是否消费成功,需不需要重新投递消息。也就是说,我们可以通过设置返回的状态值来告诉 broker 是否重新投递消息。

到这里,可能大家会有一个疑问,那如果这条消息本身就是一条脏数据,就算你消费 100 次也不会消费成功,难道还是一直去重试嘛?其实 RocketMQ 并不会无限制地重试下去,默认每条消息最多重试 16 次,而每次重试的间隔时间如下表所示:

第几次重试 每次重试间隔时间
1 10 秒
2 30 秒
3 1 分钟
4 2 分钟
5 3 分钟
6 4 分钟
7 5 分钟
8 6 分钟
9 7 分钟
10 8 分钟
11 9 分钟
12 10 分钟
13 20 分钟
14 30 分钟
15 1 小时
16 2 小时

那么如果消息重试 16 次之后还是消费失败怎么办呢?那么消息就不会再投递给消费者,而是将消息放到相对应的死信队列中。这时候我们就需要对死信队列的消息做一些人工补偿处理,因为这些消息可能本身就有问题,也有可能和消费逻辑调用的服务有关等,所以需要人工判断之后再进行处理。

到这里不知道大家有没有一个疑问,那就是什么样的情况才叫消费失败呢?可以分为 3 种情况:

  1. 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER

  2. 返回 null

  3. 抛出异常

前两种情况都比较好理解,就是前面说过的设置状态值,也就是说,只需要消费者返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 或者 null,就相当于告诉 broker 说,这条消息我消费失败了,你给我重新投递一次。而对于抛出异常这种情况,只要在你处理消费逻辑的地方抛出了异常,那么 broker 也重新投递这条消息。注意一点,如果是被捕获的异常,则不会进行消息重试。

消息幂等

首先什么是消费幂等呢?简单来说就是对于一条消息的处理结果,不管这条消息被处理多少次,最终的结果都一样。比如说,你收到一条消息是要更新一个商品的价格为 6.8 元,那么当这条消息执行 1 次,还是执行 100 次,最终在数据库里的该商品价格就是 6.8 元,这就是所谓的幂等。 那么为什么消费需要幂等呢?因为在实际使用中,尤其在网络不稳定的情况下,RocketMQ 的消息有可能会出现重复,包括两种情况:

  1. 发送时消息重复;

  2. 投递时消息重复;

第一种情况是生产者发送消息的场景,消息已成功发送到 broker ,但是此时可能发生网络闪断或者生产者宕机了,导致 broker 发回的响应失败。这时候生产者由于没有收到响应,认为消息发送失败,于是尝试再次发送消息给 broker。这样一来,broker 就会再收到一条一摸一样内容的消息,最终造成了消费者也收到两条内容一摸一样的消息。

第二种情况是消费者消费消息的场景,消息已投递到消费者并完成消费逻辑处理,当消费者给 broker 反馈消费状态时可能发生网络闪断。broker 收不到消费者的消费状态,为了保证至少消费一次的语义,broker 将在网络恢复后再次尝试投递之前已经被处理过的消息,最终造成消费者收到两条内容一摸一样的消息。

当然对于一些允许消息重复的场景,大可以不必关心消费幂等。但是对于那些不允许消息重复的业务场景来说,处理建议就是通过业务上的唯一标识来作为幂等处理的依据。

总结

消息重试,保证了消费消息的容错性,即使消费失败,也不需要开发者自己去编写代码来做补偿,大大提高了开发效率,同时也是 RocketMQ 相较于其他 MQ 的一个非常好的特性。而消费幂等主要是针对那些不允许消息重复的场景,应该说大部分 MQ 都需要幂等处理,这属于代码逻辑或者说业务上的需要,最好的处理方式就是前面所说的根据业务上唯一标识来作为幂等处理的依据。

猜你喜欢

转载自blog.csdn.net/l18848956739/article/details/83111963