02 ROMQ之生产者 + 第一个DEMO + 三种消息发送方式 + 四种消息类型

1. 基本概念

在生产者一章的基本概念包括消息,Tag,Keys,队列和生产者的介绍。

1.1 消息

RocketMQ 消息构成非常简单

  • topic,表示要发送的消息的主题。
  • body 表示消息的存储内容
  • properties 表示消息属性
  • transactionId 会在事务消息中使用。

提示

Tag: 不管是 RocketMQ 的 Tag 过滤还是延迟消息等都会利用 Properties 消息属性机制,这些特殊信息使用了系统保留的属性Key,设置自定义属性时需要避免和系统属性Key冲突。

Keys: 服务器会根据 keys 创建哈希索引,设置后,可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引,请尽可能保证 key 唯一,例如订单号,商品 Id 等。

1.2 Tag

Topic 与 Tag 都是业务上用来归类的标识,区别在于 Topic 是一级分类,而 Tag 可以理解为是二级分类。使用 Tag 可以实现对 Topic 中的消息进行过滤。
在这里插入图片描述
什么时候该用 Topic,什么时候该用 Tag?

  • 消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型必须使用不同的Topic进行区分
  • 业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分。
  • 消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分。
  • 消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个 Topic,则有可能会因为过长的等待时间而“饿死”,不同量级的消息用不同的Topic区分
  • 通常情况下,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息,例如全集和子集的关系、流程先后的关系。

1.3 Keys

ROMQ 的每个消息可以在业务层面的设置唯一标识码 keys 字段,方便将来定位消息丢失问题。
Broker 端会为每个消息创建索引(哈希索引),应用可以通过 topic、key 来查询这条消息内容,以及消息被谁消费。
由于是哈希索引,务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。

   //将订单id设为消息的key
   String orderId = "20034568923546";
   message.setKeys(orderId);

1.4 队列

一个 Topic 可能有多个队列,并且可能分布在不同的 Broker 上

队列可以提升消息发送和消费的并发度 , 并支持水平扩展
在这里插入图片描述

一般来说一条消息,如果没有重复发送(比如因为服务端没有响应而进行重试),则只会存在在 Topic 的其中一个队列中.

消息在队列中按照先进先出的原则存储,每条消息会有自己的位点,每个队列会统计当前消息的总条数,记为最大位点 MaxOffset;队列的起始位置对应的位置叫做起始位点 MinOffset。

1.5 生产者

生产者(Producer)就是消息的发送者

在不同的场景中,需要使用不同的消息进行发送。
比如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延迟消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。如支付未完成,则关闭订单。如已完成支付则忽略.;

电商场景中,业务上要求同一订单的消息保持严格顺序,此时就要用到顺序消息

在日志处理场景中,可以接受的比较大的发送延迟,但对吞吐量的要求很高,希望每秒能处理百万条日志,此时可以使用批量消息

在银行扣款的场景中,要保持上游的扣款操作和下游的短信通知保持一致,此时就要使用事务消息

再次强调 : 生产环境中不同消息类型需要使用不同的Topic主题,这样可以避免运维过程中的风险和错误

2 DEMO

2.1 生产和消费的流程

生产者的流程

  1. 创建消息生产者 producer,并制定生产者组名
  2. 指定 NameserEEver地址
  3. 启动 producer
  4. 创建消息对象,指定主题 Topic、Tag和消息体等
  5. 发送消息
  6. 关闭生产者 producer

消费者的流程

  1. 创建消费者 consumer,制定消费者组名
  2. 指定 Nameserver地址
  3. 创建监听订阅主题 Topic和 Tag等
  4. 处理消息
  5. 启动消费者 consumer

2.2 代码

需要先加入依赖

<dependency>
 <groupId>org.apache.rocketmq</groupId>
 <artifactId>rocketmq-client</artifactId>
 <version>4.9.2</version>
 </dependency>
/**
* 测试生产者
*
* @throws Exception
*/
@Test
public void testProducer() throws Exception {
		 // 创建默认的生产者 (组名)
 		DefaultMQProducer producer = new DefaultMQProducer("test-group");
		 // 设置 nameServer 地址
		 producer.setNamesrvAddr("localhost:9876");
		 // 启动实例
		 producer.start();
		 
		 for (int i = 0; i < 10; i++) {
				 // 创建消息
				 // 参数依次为 Topic名, Tag名, 消息体
				 Message msg = new Message("TopicTest" /* Topic */,
                        "TagA" /* Tag */,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                );   
                // 利用producer进行发送,并同步等待发送结果
 		SendResult sendResult = producer.send(msg);
		 System.out.println(send);
 		 }
		 // 关闭实例
		 producer.shutdown();
	 }

发送后在网页端可以看到:
点击主题, 会发现确实新建了一个名为testTopic的主题

一个Topic内默认有4个MessageQueue, 自动实现负载均衡
在这里插入图片描述可以看到broker的地址, nameServer找到Broker进行发送消息

在这里插入图片描述

 /**
消费者的流程
1. 创建消费者 consumer,制定消费者组名
2.	指定 Nameserver地址
3.	创建监听订阅主题 Topic和 Tag等
4.	处理消息
5.	启动消费者 consumer
 */
 @Test
 public void testConsumer() throws Exception {
 // 创建默认消费者组
 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
 // 设置 nameServer 地址
 consumer.setNamesrvAddr("localhost:9876");
 // 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
 consumer.subscribe("TopicTest", "*");
 
 // 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认 20 个线程,可以参看 consumer.setConsumeThreadMax()
 //监听方式为异步, 每次有消息时都要走一次业务处理流程
 consumer.registerMessageListener(new MessageListenerConcurrently() {
 			@Override
			public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
 ConsumeConcurrentlyContext context) {
					//这就是消费的方法(进行业务处理的地方) 
					//消费者可以接收, 也可以拒收消息
			 		System.out.println(Thread.currentThread().getName() + "----" + msgs);
			 		
 					// 返回消费的状态 如果是 CONSUME_SUCCESS 则成功,消息从MQ中出队
 					//若为 RECONSUME_LATER 则该条消息会被重回队列,重新被投递
 					// 重试的时间为 messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
 					// 如果重试了 18 次 那么这个消息就会被终止发送给消费者
					// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
 					return ConsumeConcurrentlyStatus.RECONSUME_LATER;
		 }
 });
 //必须先创建消费者, 注册消费者并确定业务流程, 最终才能启动消费
 consumer.start();

//挂起当前JVM, 原理是没有东西读就一直等待
 System.in.read();
 }

3. 三种消息发送方式

同步和异步属于可靠传输, 必须要等待响应后才能关闭进程

3.1 同步消息

所以针对重要的消息可以选择这种方式

同步发送是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发下一条消息的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。

MQ 集群中,主机要等到所有的从机都响应了才能继续

性能不高

在这里插入图片描述

DEMO中即为同步消息

注意的是
同步发送方式请务必捕获发送异常,并做业务侧失败兜底逻辑,如果忽略异常则可能会导致消息未成功发送的情况。

3.2 异步发送

异步发送是指发送方发出一条消息后,不必等服务端返回响应,就可以发送下一条消息 ,发送方通过回调接口接收服务端响应,并处理响应结果。

异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。

在这里插入图片描述

异步发送与同步发送代码唯一区别在于调用send接口的参数不同,异步发送不会等待发送返回,取而代之的是send方法需要传入 SendCallback 的实现,SendCallback 接口主要有onSuccess 和 onException 两个方法,表示消息发送成功和消息发送失败。

public class AsyncProducer {
  public static void main(String[] args) throws Exception {
    // 初始化一个producer并设置Producer group name
    DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    // 设置NameServer地址
    producer.setNamesrvAddr("localhost:9876");
    // 启动producer
    producer.start();
    producer.setRetryTimesWhenSendAsyncFailed(0);
    int messageCount = 100;
    final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
    for (int i = 0; i < messageCount; i++) {
      try {
          final int index = i;
          // 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤
          Message msg = new Message("TopicTest",
            "TagA",
            "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
          // 异步发送消息, 发送结果通过callback返回给客户端
          producer.send(msg, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
              System.out.printf("%-10d OK %s %n", index,
                sendResult.getMsgId());
              countDownLatch.countDown();
            }
            @Override
            public void onException(Throwable e) {
              System.out.printf("%-10d Exception %s %n", index, e);
              e.printStackTrace();
              countDownLatch.countDown();
            }
          });
        } catch (Exception e) {
            e.printStackTrace();
            countDownLatch.countDown();
        }
    }
    //异步发送属于可靠传输,必须要等回调接口返回明确结果后才能结束逻辑,否则立即关闭Producer可能导致部分消息尚未传输成功
    countDownLatch.await(5, TimeUnit.SECONDS);
    // 一旦producer不再使用,关闭producer
    producer.shutdown();
  }
}

3.3 单向发送

发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

适用于某些耗时非常短,但对可靠性要求并不高的场景例如日志收集。

在这里插入图片描述

public class OnewayProducer {
  public static void main(String[] args) throws Exception{
    // 初始化一个producer并设置Producer group name
    DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    // 设置NameServer地址
    producer.setNamesrvAddr("localhost:9876");
    // 启动producer
    producer.start();
    for (int i = 0; i < 100; i++) {
      // 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤
      Message msg = new Message("TopicTest" /* Topic */,
        "TagA" /* Tag */,
        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
      );
      // sendOneway方式发送消息时, 不会对返回结果有任何等待和处理,如果出现消息发送失败,则会因为没有重试而导致数据丢失。若数据不可丢,建议选用可靠同步或可靠异步发送方式.
      producer.sendOneway(msg);
    }
     // 一旦producer不再使用,关闭producer
     producer.shutdown();
  }
}

4. 四种消息类型

4.1 顺序消息

顺序消息是一种对消息发送消费顺序都有严格要求的消息。

顺序消费的原理 :

在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列);
而消费消息的时候从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。
但是如果控制发送的顺序消息依次发送到同一个 queue 中,并且消费的时候只从这个 queue 上依次拉取,则就保证了顺序。
例如在下图中, shardingKey A 消息只能放到Queue1 中

需要注意的是 RocketMQ 消息的顺序性分为两部分,生产顺序性和消费顺序性。只有同时满足了生产顺序性和消费顺序性才能达到上述的FIFO效果。

生产顺序性: RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化。如需保证消息生产的顺序性,则必须满足以下条件:

  • 单一生产者: 消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的分区键,不同生产者之间产生的消息也无法判定其先后顺序。
  • 串行发送:生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。

满足以上条件的生产者,将顺序消息发送至服务端后,会保证设置了同一分区键的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:

在这里插入图片描述

例如创建订单的场景,需要保证同一个订单的生成、付款和发货,这三个操作被顺序执行。如果是普通消息,订单A的消息可能会被轮询发送到不同的队列中,不同队列的消息将无法保持顺序,而顺序消息发送时将ShardingKey相同(同一订单号)的消息序路由到一个逻辑队列中。

4.1.1 实例

模拟一个订单的发送流程,创建两个订单,发送的消息分别是
订单号 111 消息流程 下订单->物流->签收
订单号 112 消息流程 下订单->物流->拒收

一个比较重要的问题是, 如何让相同订单号的消息进入同一个队列呢?

假设订单号是200010110, 并且有10个队列,
用订单号对队列数取模, 即20010110%10
只要订单号相同, 取模得到的数字必定相同, 这就是这个订单进入的队列

对于生产者来说, 生产的主要区别是
每一个list内存储的对象都调用了SendResult send(Message msg, MessageQueueSelector selector, Object arg)方法,

三个参数分别为:
MessageQueueSelector 是队列选择器,arg 是一个 Java Object 对象,可以传入作为消息发送分区的分类标准。

返回值 是该消息需要发送到的队列。

最终该message + 返回值 + 订单ID 作为 producer.send的三个参数, 执行发送.

@Test
public void testOrderlyProducer() throws Exception {
		 // 创建默认的生产者
		 DefaultMQProducer producer = new DefaultMQProducer("test-group");
		 // 设置 nameServer 地址
		 producer.setNamesrvAddr("localhost:9876");
		 // 启动实例
		 producer.start();
		 List<Order> orderList = Arrays.asList(
		 		 		new Order(1, 111, 59D, new Date(), "下订单"),
						new Order(2, 111, 59D, new Date(), "物流"),
						new Order(3, 111, 59D, new Date(), "签收"),
 						new Order(4, 112, 89D, new Date(), "下订单"),
 						new Order(5, 112, 89D, new Date(), "物流"),
 						new Order(6, 112, 89D, new Date(), "拒收")
	 );
	 // 循环集合开始发送
 	orderList.forEach(order -> {
			 Message message = new Message("TopicTest", order.toString().getBytes());
			 try {
					 // 发送的时候 相同的订单号选择同一个队列 
					 producer.send(message, new MessageQueueSelector() {
							 @Override
 							 public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
									 // 当前主题有多少个队列 
									 int queueNumber = mqs.size();
									 // 这个 arg 就是后面传入的 order.getOrderNumber()
									 Integer i = (Integer) arg;
									 // 用订单号取模队列个数 , 最终消息放在第index条队列上 
									 int index = i % queueNumber;
									 // 返回选择的这个队列即可 ,那么相同的订单号 就会被放在相同的队列里 实现 FIFO 了 
									 return mqs.get(index);
							 }
					 }, order.getOrderNumber());
			 } catch (Exception e) {
					 System.out.println("发送异常");
			 }
	 });
 		// 关闭实例
		producer.shutdown();
}

对于消费者来说, 与消费普通消息的主要区别是, 注册消费监听中, new的对象是 MessageListenerOrderly , 指定为顺序消费

    @Test
    public void testOrderlyConsumer() throws Exception {
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置 nameServer 地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("TopicTest", "*");
        
        // 注册一个消费监听 MessageListenerOrderly 是顺序消费 单线程消费
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                MessageExt messageExt = msgs.get(0);
                System.out.println(new String(messageExt.getBody()));
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }

4.1.2 顺序消息的一致性问题

考虑一个场景 : 一个Broker掉线,那么此时队列总数必定变化(减少) , 此时有两种代码方案

  1. 代码中用订单号取模当前队列数(可能为 0~9 ), 但得到的结果极大概率与原本对10取模不同. 此时, 消息会被送入错误的队列.
  2. 代码中用订单号取模写死队列数(固定为10), 得到的结果一致, 但问题是这样消息会发送到掉线的broker中, 根本没有机会挽救.

例如
20010110 % 10 = 0
20010110 % 9 = 5

如果要选用 1 的方案, 即牺牲可用性以保证严格顺序性

  1. 创建 Topic 时指定 -o 参数 , 指定顺序消息队列
  2. NameServer中的配置 orderMessageEnable 和 returnOrderTopicConfigToBroker 必须是 true。

如果上述任意一个条件不满足,则是保证可用性而不是严格顺序。

4.2 延迟消息

消息放入 mq 后,过一段时间,才会被监听到并消费

比如下订单业务,提交了一个订单就可以发送一个延时消息,30min 后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

  • 延时消息的实现逻辑需要先经过定时存储等待触发,延时时间到达后才会被投递给消费者。因此,如果将大量延时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。
  • ROMQ中并不能指定任意的延迟时间, 是有一定规范的, 且在代码中用序号指代时间
    在这里插入图片描述
    对于生产者来说, 生产的主要区别是
    在send之前, 用message对象调用setDelayTimeLevel方法, 指定延迟时间
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
        producer.start();
        int totalMessagesToSend = 100;
        
        for (int i = 0; i < totalMessagesToSend; i++) {
            Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());

					 //用3指代10秒
            message.setDelayTimeLevel(3);
            // Send the message
            producer.send(message);
        }

        // Shutdown producer after use.
        producer.shutdown();
    }

对消费者来说, 延迟时间对其是透明的, 因此消费者代码无需任何改动.

4.3 批量消息发送

在这里插入图片描述

在对吞吐率有一定要求的情况下 , ROMQ 可以将一些消息聚成一批(Batch)以后进行发送,可以增加吞吐率,并减少API和网络调用次数。

  • 一个Batch内的所有消息必须是相同的Topic
  • 批量发送的一组消息会被当成一条消息进行消费
  • 批量消息的大小不能超过 1MiB(否则需要自行分割)

对于生产来说, 生产的主要区别是
将多条message打包进存储对象为 Message 的List中, 然后send这个List

    @Test
    public void testBatchProducer() throws Exception {
    
        DefaultMQProducer producer = new DefaultMQProducer("test-group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        
        //Batch内的每一条Message的Topic必须相同
        List<Message> msgs = Arrays.asList(
                new Message("TopicTest", "我是一组消息的 A 消息".getBytes()),
                new Message("TopicTest", "我是一组消息的 B 消息".getBytes()),
                new Message("TopicTest", "我是一组消息的 C 消息".getBytes())
        );
        SendResult send = producer.send(msgs);
        System.out.println(send);

        producer.shutdown();
    }

4.4 事务消息发送

以电商交易场景为例,用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统的变更。当前业务的处理分支包括:

  • 主分支订单系统状态更新:由未支付变更为支付成功。
  • 物流系统状态新增:新增待发货物流记录,创建订单物流记录。
  • 积分系统状态变更:变更用户积分,更新用户积分表。
  • 购物车系统状态变更:清空购物车中已经支付商品,更新用户购物车记录表。

在这里插入图片描述
基于 ROMO 的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。
将二阶段提交和本地事务绑定,实现全局提交结果的一致性。

事务消息发送分为两个阶段。

正常事务发送与提交阶段

  1. 生产者发送一个半事务消息给Broker(半事务消息是指消费者暂时不能消费的消息)
  2. MQServer端若收到该半事务消息, 则向生产者响应ACK,半事务消息发送成功
  3. 生产者端侧开始执行本地事务
  4. 根据本地事务的执行状态( Commit或者Rollback ) 向Broker发送二次确认
  5. MQServer端如果收到Commit状态, 真正向下游投递消息

事务信息的补偿阶段

  1. 为防止长期收不到生产者端本地事务地执行状态反馈, 一段时间后 MQServer 向生产者 询问该消息的最终状态(Commit或是Rollback)。
  2. 补偿阶段主要是用于解决生产者在发送Commit或者Rollback操作时发生超时或失败的情况。

在这里插入图片描述

4.4.1 事务消息实例

事务消息发送步骤如下:

  1. 生产者将半事务消息发送至 ROMQ Broker。
  2. RocketMQ Broker 将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息暂不能投递,为半事务消息。
  3. 生产者开始执行本地事务逻辑。
  4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
    a. 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
    b. 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
  5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。

需要注意的是,服务端仅仅会按照参数尝试指定次数,超过次数后事务会强制回滚,因此未决事务的回查时效性非常关键,需要按照业务的实际风险来设置

事务消息回查步骤如下:

  1. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  2. 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

在这里插入图片描述

对于生产者来说, 生产的主要区别是

  1. 事务消息的发送不再使用 DefaultMQProducer,而是使用 TransactionMQProducer
  2. 在produce.start之前, 设置一个事务监听器setTransactionListener, 根据事务状态决定消息发送

事务监听器类, 实现TransactionListener接口

        static 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;
                        default:
                            return LocalTransactionState.COMMIT_MESSAGE;
                    }
                }
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
    }

生产者类

        public static void main(String[] args) throws MQClientException, InterruptedException {
            TransactionListener transactionListener = new TransactionListenerImpl();
            TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
            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 RocketMQ " + 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();
        }

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值