用了这么久RocketMQ,这个问题都不知道?

文章目录

    • 前言
    • 错误代码
      • 生产者:
      • 消费者
    • 问题说明
      • 问题分析
    • 解决方法:
    • 后续
    • 结束语

前言

最近接到一个需求,业务场景是:数据下发,从crm下发到对应的erp,因为要实现异步以及一个高可用,就选择了用RokcetMQ;

想法:

使用同一个消费者组订阅相同的Topic不同的tag,所以自然而然的消费者就肯定要选择广播模式

想象中的好处是,一条消息被多个消费者接收,而消费者自身会进行过滤(因为选择了Tag)

用了这么久RocketMQ,这个问题都不知道?_第1张图片

错误代码

采用的是集成springboot那套,不是原生的,主要是图方便

生产者:

    @Resource
    private RocketMQTemplate rocketMQTemplate;  

	@Value("${rocketmq.topic.send-message-topic}")
    private String sendMessageTopic;

    /**
     * 客户资料下发到各大ERP
     * @param customerInfoRespVO 客户资料
     */
    public void sendMessageToERP(CustomerInfoRespVO customerInfoRespVO,String tag){
        SendResult sendResult = rocketMQTemplate.syncSend(sendMessageTopic+":"+tag, customerInfoRespVO);
    }

这里遇到的小问题:

一开始虽然知道RocketMQ可以选择tag发送,但集成springboot的方式就不知道怎么发了,后面点进看源码才知道只要Topic后面加“:”,再跟tag就可以了;

我们跟进syncSend方法看下:

用了这么久RocketMQ,这个问题都不知道?_第2张图片

再继续跟进,看后续怎么处理的:syncSend —> syncSend —> createRocketMqMessage —> convertToRocketMessage —> getAndWrapMessage

用了这么久RocketMQ,这个问题都不知道?_第3张图片

很明显底层源码就是根据:分割的;格式就是xxxxtopic:tag

消费者

@Component
@RocketMQMessageListener(topic = "TOPIC", consumerGroup = "CONSUMER_GROUP",selectorExpression = "tagA")
@RefreshScope
@Slf4j
public class DataReceptionConsumer implements RocketMQListener<CustomerInfoIssueRespVO> {
    
    @Override
    public void onMessage(CustomerInfoIssueRespVO message) {
        log.info("erp接收成功");
        System.out.println(message);
    }
}

还有一个消费者,其余都一样,就选择的Tag不一样,一个TagA,一个TagB

selectorExpression选择tag,多个为 “tag1 || tag2 || tag3”,默认是"*",意思为所有;

需配合selectorType使用,因为默认是SelectorType.TAG,所以不用动

用了这么久RocketMQ,这个问题都不知道?_第4张图片

问题说明

两个consumer接收相同的topic,不同tag的消息,

当处方系统发送tagB的消息的时候,consumer2正常消费了,

但是当处方系统发送tagA的消息的时候本来consumer1应该消费的,但是却没反应,就是没接收到消息,造成了一个消息的丢失

主要问题就是配置了相同的Group。

问题分析

  1. 首先这是broker决定的,不是consumer端决定的
  2. Consumer端发心跳给Broker,Broker收到后存到consumerTable里(就是个Map),key是GroupName,value是ConsumerGroupInfo
  3. ConsumerGroupInfo包含topic、tag等信息,但是问题就出在上一步骤,key是groupName,相同GroupName的话Broker心跳最后收到的Consumer会覆盖前者的。

所以,先后启动consumer1和consumer2,当前Broker存到的consumerTable里的最终结果就是:

key:CONSUMER_GROUP —> value:TOPIC,tagB.....

我们可以想象一下TagA消息的走向:

  1. 生产者发送消息TagA
  2. 两个消费者因为是广播模式,按道理来说是会发送给所有消费者的
  3. 但是因为RocketMQ会强制使同一个消费者组的订阅关系一致,所以在它的信息中,consumer1订阅的也是TagB,但事实是TagA
  4. 所以Broker根本就不会推送给这个消费者组,或者说这个消费者组不会拉取,会过滤掉这条消息,因为它的订阅是TagB,而我的消息是TagA

解决方法:

针对不同的tag配置不同的consumerGroup即可。

后续

站在RocketMQ的角度来说,为什么订阅关系不一致会导致消息丢失呢?

我们可以先看下RocketMQ的存储架构,如下图:

用了这么久RocketMQ,这个问题都不知道?_第5张图片

这个 tag 的作用是过滤消息:

  1. 假如一个 Consumer 订阅了 Topic1 中的 Tag1,那这个 Consumer 拉取消息时,首先从 Name Server 获取订阅关系,
  2. 得到当前 Consumer 订阅的所有 tag 的 hashcode 集合 codeSet
  3. 每次从 ConsumerQueue 获取一条记录,就要判断最后 8 个字节 tag hashcode 是否在 codeSet 中,

比如 Tag2 不在 codeSet 中,就会被过滤掉。如下图:

用了这么久RocketMQ,这个问题都不知道?_第6张图片

消费组 1 消费 Topic1 中的消息时:

  1. Consumer1 通过 ConsumeQueue1 和 ConsumeQueue2 进行消费,
  2. Consumer2 通过 ConsumeQueue3 和 ConsumeQueue4 进行消费,
  3. 如果 Consumer1 订阅了 Tag1, Consumer2 订阅了 Tag2,
  4. 那 Consumer1 从 ConsumeQueue1 和 ConsumeQueue2 消费消息时,就会把 Tag2 中的消息过滤掉,
  5. 这样即使 Consumer2 订阅了 Tag2,也不能消费到 ConsumeQueue1 和 ConsumeQueue2 里 Tag2 中的消息了。

注: 这只是假设,RocketMQ是不会出现同一个消费者组订阅不一样的情况,因为在上面也说了,同一个消费者组如果订阅不一致,后在NameServier注册的就会覆盖前面那个消费者的订阅条件

结束语

大家觉得有什么问题可以评论提出来,一起学习:

时间的跨度不过是一次相遇与告别

你可能感兴趣的:(项目问题,java-rocketmq,rocketmq,java,分布式,中间件)