风止雨歇

RocketMQ 的详解

https://www.cnblogs.com/happydreamzjl/articles/11951245.html

https://cdn.modb.pro/db/72488

 

RocketMQ集群部署结构

 

 

1、Name Server

  NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。

  Name Server 主要包括两个功能:

  •   Broker管理:NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
  •   路由信息管理:每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。

  Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。任意一个 Name Server 包含所有集群的信息

 

2、Broker

集群最核心模块,主要负责Topic消息存储、消费者的消费位点管理(消费进度);

  Broker会注册到 Name Server上去,无论是否是主从, 每个 Broker 都会注册到 Name Server 上

  Broker部署相对复杂,Broker分为Master和Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。

  每个Broker 与Name Server集群中的所有节点建立长连接,定时(每隔30秒)注册Topic信息到所有Name Server。Name Server定时(每隔10秒)扫描所有存活的Broker连接,如果Name Server超过两分钟没有收到心跳。则Name Server断开与Broker 的连接。

  每个 Broker 都会创建一个 consumerOffset.json 的文件,记录当前消费的节点指向了哪条消息,即消费的偏移量;偏移量是由 Consumer 上报的,Consumer 会定时或者 Kill 阶段提交各自对应 queue 的 offset 位置,为了避免消息的重复推送;consumerOffset.json 的文件格式:

 

3、producer

  Producer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从Name Server中取Topic路由信息,并向提供Topic服务的Master建立长连接(基于 Netty),且定时向Master发送心跳。Producer 完全无状态,可集群部署。

  Producer 每隔30秒(由ClientConfig的pollNameServerInterval)从Name Server 获取所有的Topic 队列的最新情况,这意味着如果Broker 不可用,Producer 最多30秒感知到。在此期间内发往Broker的所有消息都会失败。

  Producer 每隔30秒 (由ClientConfig中heartbeatBrokerInterval决定) 向所有关联的 Broker 发送心跳,Broker 每隔10秒扫描所有存活的的连接,如果Borker 在2分钟内没有收到心跳数据,则关闭与Producer的连接。

   

4、Consumer

  Consumer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从Name Server 取 Topic 路由信息,并向提供Topic 服务的Master、 Slave 建立长连接(基于 Netty)且定时向 Master、Slave 发送心跳。Consumer 既可以从Master 订阅消息,也可以从Slave 订阅消息,订阅规则由 Broker 配置决定。

  Consumer 每隔30秒 从Name Server 获取Topic 的最新队列情况,这意味着Broker 不可用时,Consumer 最多需要30秒 即可感知。

  Consumer 每隔30秒(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的Broker 发送心跳,Broker 每隔 10 秒扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group 的所有Consumer发出通知,Group内的所有Consumer 重新分配队列,然后继续消费。

  当Consumer得到 Master 宕机通知后,转向Slave 消费,Slave 的消息不对保证100%都同步过来了,因此会有少量的消息丢失。但是一旦Master 恢复,未同步过去的消息会被最终消费掉。

  消费者队列是消费者连接之后(或之前连接过)才创建的。我们将原生的消费者标识由{IP}@{消费者group}扩展为 {IP}@{消费者group}{topic}{tag},(例如xxx.xxx.xxx.xxx@mqtest_producer-group_2m2sTest_tag-zyk)。任何一个元素不同都认为是不同的消费端,每个消费端会拥有一份自己的消费队列(默认是Borker队列数量*Broker数量)。新挂载的消费都队列中拥有CommitLog  的所有数据。

 

Topic

  每个 Broker 上都会创建 Topic;每个 Topic 都会对应一个 CommitLog,真实的消息都存储在 CommitLog 里面;

  每个 Topic 在创建之初都会默认创建 4 个队列(queue-0,queue-1,queue-2,queue-3),每个队列都会对应一个持久化的文件;Producer 向 Broker 上的 Topic 发送消息,若发现队列没有创建持久化的文件,则会创建相应的持久化文件 queueLog,queueLog 记录的每条消息在 CommitLog 中的位置等信息。

 

Producer Group

  同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事物消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生 产者实例以提交或回溯消费。

 

Consumer Group

  同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。

RocketMQ 支持两种消息模式:集群消费 (Clustering)和广播消费(Broadcasting)。

 

RocketMQ的特性

1、Producer 端

org.apache.rocketmq.client.impl.CommunicationMode 
public enum CommunicationMode {
  SYNC, 
  ASYNC, 
  ONEWAY
}

RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。

同步发送(SYNC):消息发送给 Broker之后,Broker给了反馈信息之后,生产者的代码才会继续向下执行;

异步发送(ASYNC):消息发送给 Broker 之后,不用等待Broker的响应,发送完成后会有回调函数去处理;异步的发送方式,发送完后,立刻返回。Client 在拿到 Broker 的响应结果后,会回调指定的 callback. 这个 API 也可以指定 Timeout,不指定也是默认的 3000ms;

单向发送(ONEWAY):发出去后,什么都不管直接返回。

2、Consumer 端

Consumer 消费消息有两种方式:拉取消费 和 推送消费;

拉取式消费(Pull Consumer):应用通常主动调用Consumer的拉消息方法从Broker服务 器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
推动式消费(Push Consumer):该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。

3、消息订阅模式

广播模式广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

 org.apache.rocketmq.common.protocol.heartbeat.MessageModel#BROADCASTING 

集群模式集群消费模式下,相同Consumer Group 的每个 Consumer 实例平均分摊消息。

 org.apache.rocketmq.common.protocol.heartbeat.MessageModel#CLUSTERING 
 

4、消费点位

当建立一个新的消费者组时,需要决定是否需要消费已经存在于 Broker 中的历史消息:org.apache.rocketmq.common.consumer.ConsumeFromWhere
  • CONSUME_FROM_LAST_OFFSET 将会忽略历史消息,并消费之后生成的任何消息。
  • CONSUME_FROM_FIRST_OFFSET 将会消费每个存在于 Broker 中的信息。
  • CONSUME_FROM_TIMESTAMP 消费在指定时间戳后产生的消息。

5、消息重复幂等

  RocketMQ无法避免消息重复,所以如果业务对消费重复非常敏感,务必要在业务层面去重
幂等令牌是生产者和消费者两者中的既定协议,在业务中通常是具备唯一业务标识的字符串,如:订单号、流水号等。且一般由生产者端生成并传递给消费者端。
 

6、批量消息

  批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应 超过4MB。rocketmq建议每次批量消息大小大概在1MB。 当消息大小超过4MB时,需要将消息进行分割。

7、过滤消息

  大多数情况下,可以通过TAG来选择您想要的消息。

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); 
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");

  使用Filter功能,需要在启动配置文件当中配置以下选项:

enablePropertyFilter=true

  消费者将接收包含 TAGA 或 TAGB 或 TAGC 的消息。但是限制是一个消息只能有一个标 签,这对于复杂的场景可能不起作用。在这种情况下,可以使用 SQL 表达式筛选消息。SQL 特性可以通过发送消息时的属性来进行计算。在 RocketMQ 定义的语法下,可以实现一些 简单的逻辑。

生产者:

public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("filter_sample_group");
        producer.setNamesrvAddr("192.168.241.198:9876");
        producer.start();

        for (int i = 0; i < 3; i++) {
            Message msg = new Message("TopicFilter",
                    "TAG-FILTER",
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            msg.putUserProperty("a",String.valueOf(i));
            if(i % 2 == 0){
                msg.putUserProperty("b","yangguo");
            }else{
                msg.putUserProperty("b","xiaolong girl");
            }
            producer.send(msg);
        }

        producer.shutdown();
    }

消费者:

public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("filter_sample_group");
        /**
         * 注册中心
         */
        consumer.setNamesrvAddr("192.168.241.198:9876");
        /**
         * 订阅主题
         * 一种资源去换取另外一种资源
         */
        consumer.subscribe("TopicFilter", MessageSelector.bySql("a between 0 and 3 and b = 'yangguo'"));
        /**
         * 注册监听器,监听主题消息
         */
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs){
                    try {
                        System.out.println("consumeThread=" + Thread.currentThread().getName()
                                + ", queueId=" + msg.getQueueId() + ", content:"
                                + new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();

        System.out.printf("Filter Consumer Started.%n");
    }

  

8、延时消息

  定时消息是指消息发到 Broker 后,不能立刻被 Consumer 消费,要到特定的时间点 或者等待特定的时间后才能被消费。

  使用场景:如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的 状态,如果还是未付款就取消订单释放库存。

延时机制
延迟级别(18种) 
* 当前支持的延迟时间 
* 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 
* 分别对应级别 
* 1 2 3.................... 

RocketMQ的配置类 org.apache.rocketmq.store.config.MessageStoreConfig#messageDelayLevel private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; 设置消息时延 Message message = new Message; message.setDelayTimeLevel(3);

  现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h 分别对应着等级1到18 消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级 和重试次数有关。

 

生产者:  message.setDelayTimeLevel(6); 

public static void main(String[] args) throws Exception {

        DefaultMQProducer producer = new DefaultMQProducer("ExampleConsumer");

        //;192.168.241.199:9876
        producer.setNamesrvAddr("192.168.241.198:9876;192.168.241.199:9876");

        producer.start();
        int totalMessagesToSend = 3;
        for (int i = 0; i < totalMessagesToSend; i++) {
            Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
            //延时消费
            message.setDelayTimeLevel(6);
            // Send the message
            producer.send(message);
        }

        System.out.printf("message send is completed .%n");
        producer.shutdown();
    }

消费者:

public static void main(String[] args) throws Exception {

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
        //;192.168.241.199:9876
        consumer.setNamesrvAddr("192.168.241.198:9876");

        consumer.subscribe("TestTopic", "*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
                for (MessageExt message : messages) {
                    // Print approximate delay time period
                    System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
                            + "message content is :" + new String(message.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();

        //System.out.printf("Consumer Started.%n");
    }

 

延时消息的实现原理

 所有的延迟消息由 Producer 发出之后,都会存放到同一个 Topic(SCHEDULE_TOPIC_XXXX)下,不同的延迟级别会对应不同的队列序号。当延迟时间到之后,由定时线程读取转换为普通的消息存的真实指定的 Topic 下,此时对于 Consumer 端此消息才是可见的,从而被 Consumer 消费。

 可以看到,总共有6个步骤:

  1. 修改消息Topic名称和队列信息
  2. 转发消息到延迟主题的CosumeQueue中
  3. 延迟服务消费SCHEDULE_TOPIC_XXXX消息
  4. 将信息重新存储到CommitLog中
  5. 将消息投递到目标Topic中
  6. 消费者消费目标topic中的数据

 

9、事务消息

RabbitMQ、Kafka都不支持事务消息,RocketMQ 可以支持事务消息的最大特性是 Producer 和 Broker 是双向通信的;

事务消息详解:https://blog.csdn.net/Weixiaohuai/article/details/123733518

概念

  • 事务消息:消息队列 MQ 提供类似 X/Open XA 的分布式事务功能,通过消息队列 MQ 事务消息能达到分布式事务的最终一致。
  • 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了消息队列 MQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
  • 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列 MQ 服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。

场景

  通过购物车进行下单的流程中,用户入口在购物车系统,交易下单入口在交易系统,两个系统之间的数据需要保持最终一致,这时可以通过事务消息进行处理。交易系统下单之后,发送一条交易下单的消息到消息队列 MQ,购物车系统订阅消息队列 MQ 的交易下单消息,做相应的业务处理,更新购物车数据。

消息状态

org.apache.rocketmq.client.producer.LocalTransactionState
提交事务,它允许消费者消费此消息。 LocalTransactionState.CommitTransaction
回滚事务,它代表该消息将被删除,不允许被消费 LocalTransactionState.RollbackTransaction
中间状态,它代表需要检查消息队列来确定状态 LocalTransactionState.Unknown
 

交互流程

 事务消息发送步骤如下:

  • 发送方将半事务消息发送至消息队列 MQ 服务端。
  • 消息队列 MQ 服务端将消息持久化成功之后,向发送方返回 Ack 确认消息 已经发送成功,此时消息为半事务消息。
  • 发送方开始执行本地事务逻辑。
  • 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务 端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。

 

事务消息回查步骤如下:
  • 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端, 经过固定时间后服务端将对该消息发起消息回查。
  • 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行操作。

 

事务消息限制

  • 事务消息不支持延时消息和批量消息。
  • 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限 制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限 制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将 丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  • 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度 之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
  • 事务性消息可能不止一次被检查或消费。
  • 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性 得到保证,建议使用同步的双重写入机制。
  • 事务消息的3生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不 同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

事务消息详解:https://blog.csdn.net/Weixiaohuai/article/details/123733518

 

事务消息使用案例

(1)定义消息监听器

消息监听器主要是实现TransactionListener接口,然后需要重写下面两个方法:

  • executeLocalTransaction:执行本地事务;
  • checkLocalTransaction:回查本地事务状态,根据这次回查的结果来决定此次事务是提交还是回滚;
@RocketMQTransactionListener(txProducerGroup = "myTxProducerGroup")
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<String, Integer>();

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
        System.out.printf("#### executeLocalTransaction is executed, msgTransactionId=%s %n",
                transId);
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(transId, status);
        if (status == 0) {
            // 事务提交
            System.out.printf("    # COMMIT # Simulating msg %s related local transaction exec succeeded! ### %n", msg.getPayload());
            return RocketMQLocalTransactionState.COMMIT;
        }

        if (status == 1) {
            // 本地事务回滚
            System.out.printf("    # ROLLBACK # Simulating %s related local transaction exec failed! %n", msg.getPayload());
            return RocketMQLocalTransactionState.ROLLBACK;
        }

        // 事务状态不确定,待Broker发起 ASK 回查本地事务状态
        System.out.printf("    # UNKNOW # Simulating %s related local transaction exec UNKNOWN! \n");
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    /**
     * 在{@link TransactionListenerImpl#executeLocalTransaction(org.springframework.messaging.Message, java.lang.Object)}
     * 中执行本地事务时可能失败,或者异步提交,导致事务状态暂时不能确定,broker在一定时间后
     * 将会发起重试,broker会向producer-group发起ask回查,
     * 这里producer->相当于server端,broker相当于client端,所以由此可以看出broker&producer-group是
     * 双向通信的。
     * @param msg
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
        RocketMQLocalTransactionState retState = RocketMQLocalTransactionState.COMMIT;
        Integer status = localTrans.get(transId);
        if (null != status) {
            switch (status) {
                case 0:
                    retState = RocketMQLocalTransactionState.UNKNOWN;
                    break;
                case 1:
                    retState = RocketMQLocalTransactionState.COMMIT;
                    break;
                case 2:
                    retState = RocketMQLocalTransactionState.ROLLBACK;
                break;
            }
        }
        System.out.printf("------ !!! checkLocalTransaction is executed once," +
                        " msgTransactionId=%s, TransactionState=%s status=%s %n",
                transId, retState, status);
        return retState;
    }
}

 

(2)定义消息生产者

    private static final String TX_PGROUP_NAME = "myTxProducerGroup";

    @Resource
    private RocketMQTemplate rocketMQTemplate;
    @Value("${tl.rocketmq.transTopic}")
    private String springTransTopic;
    @Value("${tl.rocketmq.topic}")
    private String springTopic;
    @Value("${tl.rocketmq.orderTopic}")
    private String orderPaymentTopic;
    @Value("${tl.rocketmq.msgExtTopic}")
    private String msgExtTopic;

    /**
     * 发送事务消息
     * @throws MessagingException
     */
    private void testTransaction() throws MessagingException {
        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {

                Message msg = MessageBuilder.withPayload("Hello RocketMQ " + i).
                        setHeader(RocketMQHeaders.KEYS, "KEY_" + i).build();
                /**
                 * TX_PGROUP_NAME 必须同 {@link TransactionListenerImpl} 类的注解 txProducerGroup
                 * @RocketMQTransactionListener(txProducerGroup = "myTxProducerGroup")
                 */
                SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(TX_PGROUP_NAME,
                        springTransTopic + ":" + tags[i % tags.length], msg, null);
                System.out.printf("------ send Transactional msg body = %s , sendResult=%s %n",
                        msg.getPayload(), sendResult.getSendStatus());

                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

 

posted on 2022-08-29 23:14  风止雨歇  阅读(494)  评论(0编辑  收藏  举报

导航