关于rocketmq:RocketMQ使用指南

RocketMQ 的介绍

RocketMQ 作为目前支流的消息中间件之一, 而音讯队列次要利用于以下场景:

  • 异步(不须要同步期待)
  • 解耦(利用之间不相互依赖)
  • 削峰(防止流量激增导致系统性能问题)

RocketMQ 具备以下个性:

  • 音讯的订阅与公布(音讯队列的基本功能)
  • 程序音讯(生产的程序与发送的程序统一,包含全局有序和分区有序,全局有序的topic只有一个音讯队列,实用场景:性能要求不高,所有音讯须要严格有序。分区有序的所有音讯依据 sharding key进行分区。同一个分区内的音讯依照 FIFO 程序进行公布和生产。Sharding key 是音讯中用来辨别不同分区的关键字段,和一般音讯的 Key 是齐全不同的概念。 实用场景:性能要求高,对于某一类音讯须要有序,同时有一个sharding key 作为分区字段。)
  • 定时音讯(音讯发送到broker后,不会立刻被生产,期待特定工夫投递给真正的topic。)
  • 事务音讯(利用本地事务和发送音讯操作能够被定义到全局事务中,要么同时胜利,要么同时失败。通过事务音讯能达到分布式事务的最终统一。)
  • 音讯重试(消费者生产失败时,RocketMQ 提供重试机制使得音讯能够被再次生产。)
  • 流量管制(当Broker解决音讯的能力达到瓶颈时,通过回绝生产者的发送申请进行流量管制,当消费者的音讯能力达到瓶颈时,通过升高音讯的拉取频率进行流量管制)
  • 死信队列(当一条音讯达到最大重试次数后,若生产仍然失败,则表明消费者在失常状况下无奈正确地生产该音讯,此时,音讯队列不会立即将音讯抛弃,而是将其发送到该消费者对应的非凡队列中。)

RocketMQ 的概念

  • 音讯(MESSAGE)
    零碎之间相互传递的音讯,每个音讯都属于某一个主题,每个音讯应用惟一的Message ID进行标识,同时音讯能够带有标签(TAG)和 键(KEY)。
  • 标签(TAG)
    每条音讯能够携带标签,用于同一主题下辨别不同类型的音讯。
  • 键(KEY)
    除了标签,RocketMQ的音讯还能够带上KEY,能够有多个KEY。
  • 主题(TOPIC)
    音讯对应的主题,用于示意音讯的类别,一个主题能够对应多条音讯,生产者生产音讯时须要制订主题,消费者生产音讯时也须要制订主题
  • 生产者(PRODUCER)
    发送音讯的利用称为生产者。
  • 生产者组(PRODUCER GROUP)
    同一类的生产者的汇合,每个生存者组蕴含多个生产者。
  • 消费者(CONSUMER)
    生产音讯的利用称为消费者。
  • 消费者组(CONSUMER GROUP)
    同一类的消费者的汇合,每个消费者组蕴含多个消费者。
  • 代理服务器(BROKER)
    音讯直达角色,负责存储音讯、转发音讯。接管从生产者发送来的音讯并存储、同时为消费者的拉取申请作筹备。也存储音讯相干的元数据,包含消费者组、生产进度偏移和主题和队列音讯等。
  • 名字服务器(NAME SERVER)
    相似于注册核心,消费者和生产者的注册和发现都依赖于名字服务器,同时会保留broker的信息, 生产者或消费者可能通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但互相独立,没有信息替换。
  • 消费者获取音讯的形式:

    • 拉取式生产
      利用通常被动调用Consumer的拉音讯办法从Broker服务器拉音讯、主动权由利用管制。一旦获取了批量音讯,利用就会启动生产过程。
    • 推动式生产
      该模式下Broker收到数据后会被动推送给生产端,该生产模式个别实时性较高。
  • 生产模式:

    • 集群生产
      集群生产模式下,雷同Consumer Group的每个Consumer实例均匀摊派音讯。
    • 播送生产
      播送生产模式下,雷同Consumer Group的每个Consumer实例都接管全量的音讯。

RocketMQ 的架构设计

整体架构

整个RocketMQ 的架构如下:

能够整个架构由四局部组成,别离是Producer, Conusmer, Name Server, Broker。
整个RocketMQ 的工作流程如下:

  1. 启动NameServer,NameServer起来后监听端口,期待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
  2. Broker启动,跟所有的NameServer放弃长连贯,定时发送心跳包。心跳包中蕴含以后Broker信息(IP+端口等)以及存储所有Topic信息。注册胜利后,NameServer集群中就有Topic跟Broker的映射关系。
  3. 收发音讯前,先创立Topic,创立Topic时须要指定该Topic要存储在哪些Broker上,也能够在发送音讯时主动创立Topic。
  4. Producer发送音讯,启动时先跟NameServer集群中的其中一台建设长连贯,并从NameServer中获取以后发送的Topic存在哪些Broker上,轮询从队列列表中抉择一个队列,而后与队列所在的Broker建设长连贯从而向Broker发消息。
  5. Consumer跟Producer相似,跟其中一台NameServer建设长连贯,获取以后订阅Topic存在哪些Broker上,而后间接跟Broker建设连贯通道,开始生产音讯。

音讯存储

  • CommitLog:音讯主体以及元数据的存储主体,存储Producer端写入的音讯主体内容,音讯内容不是定长的。单个文件大小默认1G, 文件名长度为20位,右边补零,残余为起始偏移量,比方00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。音讯次要是程序写入日志文件,当文件满了,写入下一个文件;
  • ConsumeQueue:音讯生产队列,引入的目标次要是进步音讯生产的性能,因为RocketMQ是基于主题topic的订阅模式,音讯生产是针对主题进行的,如果要遍历commitlog文件中依据topic检索音讯是十分低效的。Consumer即可依据ConsumeQueue来查找待生产的音讯。其中,ConsumeQueue(逻辑生产队列)作为生产音讯的索引,保留了指定Topic下的队列音讯在CommitLog中的起始物理偏移量offset,音讯大小size和音讯Tag的HashCode值。consumequeue文件能够看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织形式如下:topic/queue/file三层组织构造,具体存储门路为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,别离为8字节的commitlog物理偏移量、4字节的音讯长度、8字节tag hashcode,单个文件由30W个条目组成,能够像数组一样随机拜访每一个条目,每个ConsumeQueue文件大小约5.72M;
  • IndexFile:IndexFile(索引文件)提供了一种能够通过key或工夫区间来查问音讯的办法。Index文件的存储地位是:$HOME \store\index${fileName},文件名fileName是以创立时的工夫戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile能够保留 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap构造,故rocketmq的索引文件其底层实现为hash索引。

RocketMQ采纳的是混合型的存储构造,Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。RocketMQ的混合型存储构造(多个Topic的音讯实体内容都存储于一个CommitLog中)针对Producer和Consumer别离采纳了数据和索引局部相拆散的存储构造,Producer发送音讯至Broker端,而后Broker端应用同步或者异步的形式对音讯刷盘长久化,保留至CommitLog中。RocketMQ应用Broker端的后盾服务线程—ReputMessageService不停地散发申请并异步构建ConsumeQueue(逻辑生产队列)和IndexFile(索引文件)数据。

音讯过滤

后面有提到Consumer端订阅音讯是通过ConsumeQueue拿到音讯的索引,而后再从CommitLog外面读取真正的音讯实体内容,ConsumeQueue的存储构造如下,能够看到其中有8个字节存储的Message Tag的哈希值,基于Tag的音讯过滤正是基于这个字段值的。

RocketMQ 反对两种音讯过滤形式:

  • Tag过滤形式:Consumer端在订阅音讯时除了指定Topic还能够指定TAG,如果一个音讯有多个TAG,能够用||分隔。其中,Consumer端会将这个订阅申请构建成一个 SubscriptionData,发送一个Pull音讯的申请给Broker端。Broker端从RocketMQ的文件存储层—Store读取数据之前,会用这些数据先构建一个MessageFilter,而后传给Store。Store从 ConsumeQueue读取到一条记录后,会用它记录的音讯tag hash值去做过滤,因为在服务端只是依据hashcode进行判断,无奈准确对tag原始字符串进行过滤,故在音讯生产端拉取到音讯后,还须要对音讯的原始tag字符串进行比对,如果不同,则抛弃该音讯,不进行音讯生产 (如果存在tag的hashcode统一,则可能导致失落音讯)。
  • SQL92的过滤形式:这种形式的大抵做法和下面的Tag过滤形式一样,只是在Store层的具体过滤过程不太一样,真正的 SQL expression 的构建和执行由rocketmq-filter模块负责的。每次过滤都去执行SQL表达式会影响效率,所以RocketMQ应用了BloomFilter防止了每次都去执行。SQL92的表达式上下文为音讯的属性。

音讯查问

RocketMQ反对两种形式查问:

  • 依照Message Id查问音讯。
    RocketMQ中的MessageId的长度总共有16字节,其中蕴含了音讯存储主机地址(IP地址和端口),音讯Commit Log offset。“依照MessageId查问音讯”在RocketMQ中具体做法是:Client端从MessageId中解析出Broker的地址(IP地址和端口)和Commit Log的偏移地址后封装成一个RPC申请后通过Remoting通信层发送(业务申请码:VIEW_MESSAGE_BY_ID)。Broker端走的是QueryMessageProcessor,读取音讯的过程用其中的 commitLog offset 和 size 去 commitLog 中找到真正的记录并解析成一个残缺的音讯返回。
  • 依照Message Key查问音讯。
    “依照Message Key查问音讯”,次要是基于RocketMQ的IndexFile索引文件来实现的。RocketMQ的索引文件逻辑构造,相似JDK中HashMap的实现。索引文件的具体构造如下:

    IndexFile索引文件为用户提供通过“依照Message Key查问音讯”的音讯索引查问服务。如果音讯的properties中设置了UNIQ_KEY这个属性,就用 topic + “#” + UNIQ_KEY的value作为 key 来做写入操作。如果音讯设置了KEYS属性(多个KEY以空格分隔),也会用 topic + “#” + KEY 来做索引。其中的索引数据蕴含了Key Hash/CommitLog Offset/Timestamp/Next Index offset 这四个字段,一共20 Byte。SlotTable中保留每个Slot对应的链表的头指针,NextIndexOffset 保留的是下一个节点的地址。
    依照Message Key查问音讯”的形式,RocketMQ的具体做法是,次要通过Broker端的QueryMessageProcessor业务处理器来查问,读取音讯的过程就是用topic和key找到IndexFile索引文件中的一条记录,依据其中的commitLog offset从CommitLog文件中读取音讯的实体内容。

事务音讯

RocketMQ采纳了2PC的思维来实现了提交事务音讯,同时减少一个弥补逻辑来解决二阶段超时或者失败的音讯,如下图所示。

上图阐明了事务音讯的大抵计划,其中分为两个流程:失常事务音讯的发送及提交、事务音讯的弥补流程。

  1. 事务音讯发送及提交:
    (1) 发送音讯(half音讯)。
    (2) 服务端响应音讯写入后果。
    (3) 依据发送后果执行本地事务(如果写入失败,此时half音讯对业务不可见,本地逻辑不执行)。
    (4) 依据本地事务状态执行Commit或者Rollback(Commit操作生成音讯索引,音讯对消费者可见)
  2. 弥补流程:
    (1) 对没有Commit/Rollback的事务音讯(pending状态的音讯),从服务端发动一次“回查”
    (2) Producer收到回查音讯,查看回查音讯对应的本地事务的状态
    (3) 依据本地事务状态,从新Commit或者Rollback
    其中,弥补阶段用于解决音讯Commit或者Rollback产生超时或者失败的状况。

事务音讯的实现

  1. 第一阶段的音讯写入
    写入的如果事务音讯,将音讯的Topic替换为RMQ_SYS_TRANS_HALF_TOPIC,同时将原来的Topic和Queue信息存储到音讯的属性中,正因为音讯主题被替换,故音讯并不会转发到该原主题的音讯生产队列,消费者无奈感知音讯的存在,不会生产。同时RocketMQ会开启一个定时工作,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取音讯进行生产,依据生产者组获取一个服务提供者发送回查事务状态申请,依据事务状态来决定是提交或回滚音讯。须要留神的是此时半音讯是Pending状态的。
  2. 第二阶段的音讯写入
    因为音讯是程序写入ConmmitLog的,无奈做到删除一个音讯,为了标记半音讯的状态,引入了Op音讯来标记是Commit状态还是Rollback状态。Op音讯外面保留了音讯的一些根本信息,根本信息里最次要的就是音讯的Offset。通过Offset能够索引到具体的音讯。
    RocketMQ将Op音讯写入到全局一个特定的Topic中通过源码中的办法—TransactionalMessageUtil.buildOpTopic();这个Topic是一个外部的Topic(像Half音讯的Topic一样),不会被用户生产。Op音讯的内容为对应的Half音讯的存储的Offset,这样通过Op音讯能索引到Half音讯进行后续的回查操作。

    第二阶段如果是Rollback操作,则通过发送一条Op音讯标记该音讯的状态为Rollback,同时表明该音讯曾经解决过了。如果第二阶段为Commit的操作,则须要发送一条Op音讯标记该音讯的状态为Commit,同时须要将半音讯读出,并复原出一条残缺的一般音讯进行发送。
  3. 回查音讯
    如果在RocketMQ事务音讯的二阶段过程中失败了,例如在做Commit操作时,呈现网络问题导致Commit失败,那么须要通过肯定的策略使这条音讯最终被Commit。RocketMQ采纳了一种弥补机制,称为“回查”。Broker端对未确定状态的音讯发动回查,将音讯发送到对应的Producer端(同一个Group的Producer),由Producer依据音讯来查看本地事务的状态,进而执行Commit或者Rollback。Broker端通过比照Half音讯和Op音讯进行事务音讯的回查并且推动CheckPoint(记录那些事务音讯的状态是确定的)。
    值得注意的是,rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无奈得悉事务状态,rocketmq默认回滚该音讯。

负载平衡

RocketMQ 的负载平衡都是在Client端实现的,分为生产者的负载平衡和消费者的负载平衡。

  • 生产者的负载平衡
    Producer端在发送音讯的时候,会先依据Topic找到指定的TopicPublishInfo,在获取了TopicPublishInfo路由信息后,RocketMQ的客户端在默认形式下selectOneMessageQueue()办法会从TopicPublishInfo中的messageQueueList中抉择一个队列(MessageQueue)进行发送音讯。具体的容错策略均在MQFaultStrategy这个类中定义。
  • 消费者的负载平衡
    在RocketMQ中,Consumer端的两种生产模式(Push/Pull)都是基于拉模式来获取音讯的,而在Push模式只是对pull模式的一种封装,其本质实现为音讯拉取线程在从服务器拉取到一批音讯后,而后提交到音讯生产线程池后,又“快马加鞭”的持续向服务器再次尝试拉取音讯。如果未拉取到音讯,则提早一下又持续拉取。在两种基于拉模式的生产形式(Push/Pull)中,均须要Consumer端在晓得从Broker端的哪一个音讯队列—队列中去获取音讯。
    集群模式下的具体的步骤如下:

    1. Conumser 向broker发送心跳包(蕴含了音讯生产分组名称、订阅关系汇合、音讯通信模式和客户端id的值等信息),Broker端在收到Consumer的心跳音讯后,会将它保护在ConsumerManager的本地缓存变量—consumerTable,同时并将封装后的客户端网络通道信息保留在本地缓存变量—channelInfoTable中。
    2. 启动负载平衡服务线程RebalanceService, 首先从rebalanceImpl实例的本地缓存变量—topicSubscribeInfoTable中,获取该Topic主题下的音讯生产队列汇合(mqSet),而后依据topic和consumerGroup为参数调用mQClientFactory.findConsumerIdList()办法向Broker端发送获取该生产组下消费者Id列表的RPC通信申请。对Topic下的音讯生产队列、消费者Id排序,而后用音讯队列调配策略算法(默认为:音讯队列的平均分配算法),计算出待拉取的音讯队列,最初依据调配队列的后果更新ProccessQueueTable,尝试删除不在待拉去的音讯队列中的队列,或者在调配中的队列中然而曾经过期的队列。为过滤后的音讯队列汇合(mqSet)中的每个MessageQueue创立一个ProcessQueue对象并存入RebalanceImpl的processQueueTable队列中,依据processQueueTable构建长轮询对象PullRequest对象,会从broker获取生产的进度。

RocketMQ 的最佳实际

程序音讯

FIFOProducer.java

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class FIFOProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("FIFOProducerGroup");

        producer.setNamesrvAddr("127.0.0.1:9876");

        producer.start();

        String[] tags = new String[]{"TagA", "TagC", "TagD"};

        // 订单列表
        List<OrderStep> orderList = new FIFOProducer().buildOrders();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(date);
        for (int i = 0; i < 10; i++) {
            // 加个工夫前缀
            String body = dateStr + " Hello RocketMQ " + orderList.get(i);
            Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());

            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Long id = (Long) arg;  //依据订单id抉择发送queue
                    long index = id % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//订单id

            System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
                    sendResult.getSendStatus(),
                    sendResult.getMessageQueue().getQueueId(),
                    body));
        }

        producer.shutdown();
    }

    /**
     * 订单的步骤
     */
    private static class OrderStep {
        private long orderId;
        private String desc;

        public long getOrderId() {
            return orderId;
        }

        public void setOrderId(long orderId) {
            this.orderId = orderId;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        @Override
        public String toString() {
            return "OrderStep{" +
                    "orderId=" + orderId +
                    ", desc='" + desc + '\'' +
                    '}';
        }
    }

    /**
     * 生成模仿订单数据
     */
    private List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("创立");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("创立");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("创立");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("实现");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("实现");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("实现");
        orderList.add(orderDemo);

        return orderList;
    }
}

FIFOConsumer.java

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class FIFOConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("FIFOConsumerGroup");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        /**
         * 设置Consumer第一次启动是从队列头部开始生产还是队列尾部开始生产<br>
         * 如果非第一次启动,那么依照上次生产的地位持续生产
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        consumer.subscribe("TopicTest", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerOrderly() {

            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 能够看到每个queue有惟一的consume线程来生产, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }

                try {
                    //模仿业务逻辑解决中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();

        System.out.println("Consumer Started.");
    }
}

事务音讯

TransactionProducer.java

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.io.UnsupportedEncodingException;
import java.util.concurrent.*;


public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("TransactionProducerGroup");
        producer.setNamesrvAddr("127.0.0.1:9876");
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();
        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg =
                        new Message("TopicTest", tags[i % tags.length], "KEY" + i,
                                ("Hello Luo " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);
                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}

TransactionListenerImpl.java

import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

RocketMQ 的应用倡议

  • producer应用倡议

    1. 请管制producer实例数,过多的连贯会造成server端的资源节约;
    2. 请给 producer 设置优雅敞开(producer.shutdown()),服务敞开能够立刻开释资源
    3. 接入多个rocketmq集群,为避免nameserver笼罩问题,须要设置InstanceName
  • consumer应用倡议

    1. 尽量应用push consumer
    2. 接入多个rocketmq集群,为避免nameserver笼罩问题,须要设置InstanceName
    3. 当应用TAG的时候,防止同一个consumerGroup 的Consumer 应用的TAG 的类型不同。

参考

RocketMQ 官网文档

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理