目录
消息示例-简单消息的实现
1. 前言
上一篇文章我们介绍了RocketMQ集群的搭建,这篇文章将主要使用RocketMQ测试下简单消息。
2. 同步消息(生产者)
同步消息的话,消费者发布消息之后必须等集群返回成功之后才会发布下一条消息,消息的发布是同步进行的。
2.1. 测试代码
-
创建生产者
// 1.创建生产者对象 DefaultMQProducer defaultMQProducer = new DefaultMQProducer("feige-producer-group");
-
指定nameserver
// 2.指定nameServer defaultMQProducer.setNamesrvAddr("172.31.184.89:9876");
因为每个nameserver都有所有broker的路由信息,所以只需要指定一个nameserver。
-
启动生产者发布消息
// 3.启动生产者 defaultMQProducer.start(); //4.创建消息 for (int i = 0; i < 100; i++) { // 创建消息,指定topic,以及消息体 Message message = new Message("base_topic", ("飞哥测试消息" + i).getBytes()); //5.发送消息 SendResult send = defaultMQProducer.send(message); System.out.println(send); } // 6.关闭生产者 defaultMQProducer.shutdown();
创建一个名为 base_topic的topic,虽然集群中还没有这个topic,但是由于前面我们搭建集群的时候指定的可以自动创建topic autoCreateTopicEnable=true
。 然后消息体是:飞哥测试消息xxx。这里打印了集群的响应结果SendResult。
运行结果(部分结果):
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E2250000, offsetMsgId=AC1FB85900002A9F00000000001F70B6, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=0], queueOffset=125]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E2410001, offsetMsgId=AC1FB85900002A9F00000000001F71A1, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=1], queueOffset=125]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E24D0002, offsetMsgId=AC1FB85900002A9F00000000001F728C, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=2], queueOffset=125]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E2570003, offsetMsgId=AC1FB85900002A9F00000000001F7377, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=3], queueOffset=125]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E2600004, offsetMsgId=AC1FB85900002A9F00000000001F7462, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=0], queueOffset=126]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E2830005, offsetMsgId=AC1FB85900002A9F00000000001F754D, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=1], queueOffset=126]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E28C0006, offsetMsgId=AC1FB85900002A9F00000000001F7638, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=2], queueOffset=126]
SendResult [sendStatus=SEND_OK, msgId=7F0000018D8014DAD5DC0C03E2970007, offsetMsgId=AC1FB85900002A9F00000000001F7723, messageQueue=MessageQueue [topic=base_topic, brokerName=broker-b, queueId=3], queueOffset=126]
这里SendResult 返回结果有几个属性需要说明下:
- sendStatus: 发送状态
- msgId:消息ID,每个消息都是唯一的
- offsetMsgId:偏移消息ID,在队列里的消息唯一ID
- messageQueue:用于指定当前这条消息落到哪个队列中,在搭建集群的时候指定一个broker有4个messageQueue。
- topic:当前队列所属的主题
- brokerName:当前队列所属的broker
- queueId:当前队列在broker中序号
- queueOffset:当前消息在队列里的偏移量。
从打印的结果可以看出,目前这100条消息是轮流的发送到broker-b中的4个队列中的。关系如下图所示:
3. 消费者
-
创建消费者&指定nameserver
// 1.创建消费者 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group"); // 2.指定连接nameServer consumer.setNamesrvAddr("172.31.184.89:9876");
-
订阅一个或者多个topic,这里指定消费base_topic,不做过滤。
// 3.订阅一个或者多个topic,这里指定消费base_topic,不做过滤 consumer.subscribe("base_topic", "*");
-
创建一个回调函数&处理消息
// 4.创建一个回调函数 consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { // 5.处理消息 for (MessageExt msg : msgs) { System.out.println(msg); System.out.println("收到的消息内容:" + new String(msg.getBody())); } // 返回消费成功的对象 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; });
创建一个回调监听函数,它是一个长轮询,当有消息产生时,它会监听到并进行消费(ps: broker会把消息推送给消费者)。
-
启动消费者
// 6.启动消费者 consumer.start(); System.out.println("消费者已经启动");
运行结果(部分截图):
4. 异步消息
异步消息与同步消息的区别就是异步消息不需要等待集群返回发送成功的标识,即可发送下一条消息。主要是发送消息阶段有区别。其他的与同步消息相同。
// 异步消息发送失败重试次数
defaultMQProducer.setRetryTimesWhenSendAsyncFailed(0);
CountDownLatch2 countDownLatch2 = new CountDownLatch2(100);
// 4.创建消息
for (int i = 0; i < 100; i++) {
// 创建消息,指定topic,以及消息体
Message message = new Message("base_topic", "TagA", "feige", ("飞哥异步消息测试" + i).getBytes());
// 5.发送消息
int index = i;
defaultMQProducer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch2.countDown();
System.out.printf("%-10d ok,%s,%n", index,sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
countDownLatch2.countDown();
System.out.printf("%-10d fail,%s,%n", index, e);
e.printStackTrace();
}
});
}
System.out.println("=====================");
countDownLatch2.await(10, TimeUnit.SECONDS);
异步消息在调用send方法的时候,需要实现SendCallback 接口。此函数有 onSuccess 方法和onException 方法。onSuccess 方法在消息发送成功的时候会被集群调用,而onException方法则是在消息发送失败的时候被调用。
5. 单向消息
单向消息只管发送不管接收。
//4.创建消息
for (int i = 0; i < 100; i++) {
// 创建消息,指定topic,以及消息体
Message message = new Message("base_topic", ("飞哥同步消息测试:" + i).getBytes());
//5.发送消息
defaultMQProducer.sendOneway(message);
}
6. 总结
本文详细介绍了简单消息里的同步消息,异步消息和单向消息。他们的区别主要是生产者发布消息时的区别。另外,简单消息的消费是没有顺序的。
顺序消息&延迟消息&广播消息的实现
1. 前言
顺序消息指的是消费者在消费消息时,按照生产者发送消息的顺序进行消费。即先发送的先消费【FIFO】。
顺序消息分为 全局顺序消息和局部顺序消息。
全局顺序消息就是全局使用一个queue。
局部顺序消息就是 有顺序依赖的消息放在同一个queue中,多个queue并行消费。
2. 局部顺序消息
默认情况下RocketMQ会根据轮询的方式将消息发送到某个broker中的某个队列中,这样的话就不能保证消息是有序的。
比如在购物网站下单场景下:有 1. 创建订单---->2. 订单支付---->3. 订单发货---->4. 订单完成 四条消息。这四条消息逻辑上肯定是有序的。但是如果采用RocketMQ默认的消息投递方式,那么同一个订单,有可能创建订单被投递到了 MessageQueue1,订单支付的话被投递到了MessageQueue2。 由于消息在不同的MessageQueue中,消费者在消费的时候就可能会出现订单支付的消息先于创建订单的消息。
局部顺序消息就是要保证同一笔订单4条消息都放在同一个queue中,这样的话就不会出现订单支付的消息先于创建订单的消息被消费。就像下图所示:
局部顺序消息消费者在消费某个topic的某个队列中的消息的时候是顺序的。消费者使用MessageListenerOrderly类来进行消息监听。
2.1. 定义生产者
-
这里定义了名为part_order_topic_test的topic。运行程序之后该topic可以路由到broker-a 以及broker-b 两个broker。
public class OrderProducer {
// 局部顺序消费,核心就是自己选择Queue,保证需要顺序保障的消息落到同一个队列中
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("order_producer_group");
defaultMQProducer.setNamesrvAddr("172.31.184.89:9876");
defaultMQProducer.start();
for (int i = 0; i < 10; i++) {
int orderId = i;
for (int j = 0; j < 5; j++) {
// 构建消息体,tags和key 只是做一个简单区分
Message partOrderMsg = new Message("part_order_topic_test", "order_" + orderId, "KEY_" + orderId, ("局部顺序消息处理_" + orderId + ";step_" + j).getBytes());
SendResult send = defaultMQProducer.send(partOrderMsg, new MessageQueueSelector() {
@Override
//这里的arg参数就是外面的orderId
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer orderId = (Integer) arg;
int index = orderId % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.printf("%s%n", send);
}
}
defaultMQProducer.shutdown();
}
}
-
在发送消息的时候实现MessageQueueSelector接口用于在发送消息的时候指定队列。其中,
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg)
方法有三个参数:其中,mqs表示当前topic所路由的全部队列数,这里就是8个队列,broker-a有4个队列,broker-b有4个队列。msg就是传入的消息体,arg 就是传入的orderId。 -
这里根据orderId与队列数求模取余来获取消息应该发送到哪个队列中,这样就保证了相同的orderId的消息会落到同一个队列中
Integer orderId = (Integer) arg; int index = orderId % mqs.size(); return mqs.get(index);
生产者运行结果(部分截图)
从运行结果可以看出相同orderId的消息被投递到了同一个MessageQueue中,而相同MessageQueue队列天然是有顺序的。
2.2.定义消费者
说完了生产者,接着来说说消费者。消费者的逻辑主要是在消费的时候需要实现 MessageListenerOrderly 类来进行消息监听。核心代码是:
// 2.订阅消费消息
defaultMQPushConsumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("消费得到的消息是={}" + msg);
System.out.println("消息体内容是={}" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
这里启动了三个消费者,不管消费者消费的顺序如何,相同的orderId下的5条消息都是被顺序消费的。
3. 碰到的问题
在首次调试的时候出现了一个 broker is full 的错误。这是由于磁盘空间不足导致的,可以通过 df -h
命令查看当前磁盘空间的占用情况,当磁盘空间使用率超过90%的话则会报此错。
4. 全局顺序消息
全局顺序消息是指消费者消费全部消息都是顺序的,只能让所有的消息都发送到同一个MessageQueue中来实现,在高并发场景下会非常影响效率。
5. 广播消息
广播消息是向主题(topic)的所有订阅者发送消息,订阅同一个topic的多个消费者,都能全量收到生产者发送的所有消息。
广播消息的生产者与普通同步消息的生产者实现是一致的,不同的是消费者的消息模式不同。这里给出消费者实现的不同之处。
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("broadCastGroup");
defaultMQPushConsumer.setNamesrvAddr("172.31.184.89:9876");
// 设置消费者的模式是广播模式
defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
//从第一位开始消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
6. 延迟消息
延迟消息与普通消息的不同之处在于,它们要在指定的时间之后才会被传递。生产者并不会延迟发送消息,而是发送到topic里面,消费者延迟指定的时间进行消费。
6.1. 延迟消息生产者
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("scheduled_group");
defaultMQProducer.setNamesrvAddr("172.31.186.180:9876");
defaultMQProducer.start();
for (int i = 0; i < 100; i++) {
Message message = new Message("Schedule_topic", ("延迟消息测试" + i).getBytes());
//设置延迟级别,默认有18个延迟级别,这个消息将延迟10秒消费
message.setDelayTimeLevel(3);
defaultMQProducer.send(message);
}
System.out.println("所有延迟消息发送完成");
defaultMQProducer.shutdown();
延迟消息生产者与普通消息生产者主要的区别是延迟消息需要调用 setDelayTimeLevel
方法设置延迟级别,这里设置级别是3,则是延迟10秒。RocketMQ提供了18种延迟级别。可以在 RocketMQ的仪表板中的集群中的broker配置中找到。
延迟消息的消费者与普通消息的消费者相同的。RocketMQ内部通过名为SCHEDULE_TOPIC_XXXX 的topic来存放延迟消息。
7.批量消息
批量发送消息提高了传递消息的性能。官方建议批量消息的总大小不应超过1M,实际不应超过4M。如果超过4M的批量消息需要进行分批处理。同时设置broker的配置参数为4M(在broker的配置文件中修改:maxMessageSize=4194304)。核心代码如下:
//4.创建消息
List<Message> messageList = new ArrayList<>();
for (int i = 0; i < 100*100; i++) {
// 创建消息,指定topic,以及消息体
messageList.add(new Message("batch_topic", ("飞哥测试批量消息" + i).getBytes()));
}
//批量消息消息小于4M的处理
SendResult send = defaultMQProducer.send(messageList);
System.out.println(send);
8.过滤消息
使用tag过滤
在大多数情况下,标签是一种简单而有用的设计,可以用来选择你想要的消息。
首先是根据tag来过滤消息,生产者在发送消息的时候指定该消息的tag标签,消费者则可以根据tag来过滤消息。
8.1. 过滤消息生产者
这里定义了三个tag,分别是tagA,tagB以及tagC,生产者在生产消息的时候给每个消息指定不同的tag。
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("TagProducer_group");
defaultMQProducer.setNamesrvAddr("172.31.184.89:9876");
defaultMQProducer.start();
String[] tags = new String[]{"tagA", "tagB", "tagC"};
for (int i = 0; i < 15; i++) {
Message message = new Message("TagFilterTest", tags[i % tags.length], ("飞哥tag消息过滤" + tags[i % tags.length]).getBytes());
SendResult send = defaultMQProducer.send(message);
System.out.printf("%s%n", send);
}
defaultMQProducer.shutdown();
8.2. 过滤消息的消费者
消费者过滤出了标签带有tagA以及tagC的消息进行消费。这里其实是broker将consumer需要的消息推给消费者。
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("tagConsumer");
defaultMQPushConsumer.setNamesrvAddr("172.31.184.89:9876");
defaultMQPushConsumer.subscribe("TagFilterTest", "tagA||tagC");
defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.println("接收到的消息=" + msg);
System.out.println("接收到的消息体=" + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
defaultMQPushConsumer.start();
System.out.println("消费者已经启动");
使用SQL过滤
SQL 功能可以通过发送消息时输入的属性进行一些计算,在RocketMQ定义的语法下,可以实现一些有趣的逻辑。
语法
RocketMQ只定义了一些基本的语法类支持这个特性。
1. 数值比较:如 `>`,`>=`,`<=`,`BETWEEN`,`=`;
2. 字符比较:如 `=`,'<>',`IN`;
3. `IS NULL` 或 `IS NOT NULL` ;
4. 逻辑`AND`,`OR`,`NOT`;
常量类型有:
1. 数字,如 123,
2. 字符,如 'abc',必须用单引号;
3. `NULL`,特殊常数;
4. 布尔值,`TRUE` 或 `FALSE`;
SQL过滤生产者
生产者主要设置属性过滤 message.putUserProperty("a", String.valueOf(i));
表示第一条消息键值对是 a=0,第二条消息键值对是a=1。
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("TagProducer_group");
defaultMQProducer.setNamesrvAddr("172.31.184.89:9876");
defaultMQProducer.start();
String[] tags = new String[]{"tagA", "tagB", "tagC"};
for (int i = 0; i < 15; i++) {
Message message = new Message("SQLFilterTest", tags[i % tags.length], ("飞哥sql消息过滤" + tags[i % tags.length]).getBytes());
message.putUserProperty("a", String.valueOf(i));
SendResult send = defaultMQProducer.send(message);
System.out.printf("%s%n", send);
}
defaultMQProducer.shutdown();
SQL过滤消费者:
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("tagConsumer");
defaultMQPushConsumer.setNamesrvAddr("172.31.184.89:9876");
defaultMQPushConsumer.subscribe("SQLFilterTest", MessageSelector.bySql("(TAGS is not null and TAGS in ('tagA','tagC'))"+" and (a is null and a between 0 and 3)"));
defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.println("接收到的消息=" + msg);
System.out.println("接收到的消息体=" + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
defaultMQPushConsumer.start();
System.out.println("消费者已经启动");
如果运行报 The broker does not support consumer to filter message by SQL92
则需要修改 broker.conf 文件,增加如下配置:
# 开启对 propertyfilter的支持
enablePropertyFilter = true
filterSupportRetry = true
然后重启broker。
RocketMQ事务消息
1. 事务消息的定义
事务消息可以认为是一个两阶段的提交消息实现,以确保分布式事务的最终一致性。事务性消息确保本地事务的执行和消息的发送可以原子执行。
两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做,所谓的两个阶段是指:第一阶段:准备阶段;第二阶段:提交阶段。
事务消息有三种状态:
- TransactionStatus.CommitTransaction: 提交事务,表示允许消费者消费该消息。
- TransactionStatus.RollbackTransaction: 回滚事务,表示该消息将被删除,不允许消费。
- TransactionStatus.Unknow: 中间状态,表示需要MQ回查才能确定状态。
2.事务消息的实现流程
- 生产者发送half消息,broker接收到half消息并回复half消息。
- 生产者调用
TransactionListener.executeTransaction()
方法执行本地事务。 - 生产者获得本地事务执行状态,提交给broker。如果状态是COMMIT_MESSAGE状态的话则broker会将消息推送给消费者。如果状态是ROLLBACK_MESSAGE状态的话则broker会丢弃此消息。如果状态是中间状态UNKNOW状态则broker会回查本地事务状态。
- 生产者调用
TransactionListener.checkLocalTransaction()
方法回查本地事务执行状态,并再次执行5,6,7三步骤,若回查次数超过15次则丢弃。
使用限制:
- 事务性消息没有调度和批处理支持。
- 为避免单条消息被检查次数过多,导致半队列消息堆积,我们默认单条消息的检查次数限制为15次,但用户可以通过更改
transactionCheckMax
来更改此限制,如果一条消息的检查次数超过transactionCheckMax
次,broker默认会丢弃这条消息,同时打印错误日志。用户可以重写AbstractTransactionCheckListener
类来改变这种行为。 - 事务消息将一定时间后检查,该时间由代理配置中的参数
transactionTimeout
确定。并且用户也可以在发送事务消息时通过设置用户属性CHECK_IMMUNITY_TIME_IN_SECONDS
来改变这个限制,这个参数优先于transactionMsgTimeout
参数。 - 一个事务性消息会被检查或消费不止一次。
- 事务性消息的生产者ID不能与其他类型消息的生产者ID共享,与其他类型的消息不同,事务性消息允许向后查询。MQ服务器通过其生产者ID查询客户端。
- 提交给用户目标主题的消息reput可能会失败,目前它取决于日志记录,高可用是由RocketMQ本身的高可用机制来保证的。如果要保证事务消息不丢失,保证事务完整性,推荐使用同步双写机制。
3. 事务消息的实现示例
3.1. 事务消息的消费者
事务消息的消费者与普通消息的消费者基本相同,也就是说事务消息是控制生产者端和broker端。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my_transaction_consumer");
consumer.setNamesrvAddr("172.31.184.89:9876");
consumer.subscribe("TransactionTopic", "*");
// 4.创建一个回调函数
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
// 5.处理消息
for (MessageExt msg : msgs) {
System.out.println(msg);
System.out.println("收到的消息内容:" + new String(msg.getBody()));
}
// 返回消费成功的对象
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 6.启动消费者
consumer.start();
System.out.println("消费者已经启动");
3.2. 本地事务的实现
事务消息最关键的地方是生产者本地事务的实现,生产者本地事务实现 TransactionListener 接口,并实现该接口中的executeLocalTransaction方法和checkLocalTransaction方法。
其中,executeLocalTransaction 方法的作用是执行本地事务。它在生产者每次发送half消息的时候被调用,
- 如果调用此方法返回
LocalTransactionState.COMMIT_MESSAGE
状态,则此消息会被消费者消费到。 - 如果返回
LocalTransactionState.ROLLBACK_MESSAGE
状态,则此消息会被broker丢弃 - 如果返回
LocalTransactionState.UNKNOW
状态,即中间状态,则broker会调用checkLocalTransaction
方法进行回查,最多回查15次。
checkLocalTransaction方法的作用是检查本地事务, 它是生产者发送完所有消息的时候调用,主要是针对的是中间状态的消息进行调用。
同样的如果调用此方法返回前面提到的三种状态,broker也会做出相同的处理。
public class TransactionListenerImpl implements TransactionListener {
/**
* 执行本地事务
* 当事务half消息发送成功,这个方法将被执行
* 事务的half消息是发到 RMQ_SYS_TRANS_OP_HALF_TOPIC 的topic中
*
* @param msg 消息
* @param arg arg 自定义业务参数
* @return {@link LocalTransactionState}
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String tags = msg.getTags();
System.out.println("============执行executeLocalTransaction方法,;消息内容是="+new String(msg.getBody()));
if (StringUtils.contains(tags, "tagA")) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.contains(tags, "tagB")) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
/**
* 检查本地事务
* 回查本地事务状态,当half消息没响应时调用。
*
* @param msg 消息
* @return {@link LocalTransactionState}
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String tags = msg.getTags();
System.out.println("============执行checkLocalTransaction方法,;消息内容是="+new String(msg.getBody()));
if (StringUtils.contains(tags, "tagC")) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.contains(tags, "tagD")) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
}
3.3. 事务消息的生产者
事务消息的生产者与普通消息的生产者最核心的区别是事务消息的生产者需要事务监听器,并且是调用sendMessageInTransaction
方法发送 half 消息。
//1.定义事务监听器
TransactionListener transactionListener = new TransactionListenerImpl();
//2.定义生产者
TransactionMQProducer producer = new TransactionMQProducer("transaction_produce_group");
producer.setNamesrvAddr("172.31.184.89:9876");
//3.定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), (runnable, executor) -> {
BlockingQueue<Runnable> queue = executor.getQueue();
try {
queue.put(runnable);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
//4.设置线程池
producer.setExecutorService(threadPoolExecutor);
//5.设置事务监听器
producer.setTransactionListener(transactionListener);
// 启动生产者
producer.start();
String[] tags = {"tagA", "tagB", "tagC", "tagD","tagE"};
//发送10条half消息,消费者是收不到half消息的
for (int i = 0; i < 10; i++) {
Message message = new Message("TransactionTopic", tags[i % tags.length],
"key" + i, ("飞哥测试事务消息" + tags[i % tags.length]+"_"+i).getBytes(StandardCharsets.UTF_8));
TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
System.out.println("本次发送的消息是=" + new String(message.getBody()));
System.out.printf("%s%n", transactionSendResult);
Thread.sleep(10);
}
System.out.println("==========所有消息发送完成======");
运行结果:
生产者:
从运行结果可以看出中间状态的消息最多回查15次,就像图中的消息 执行checkLocalTransaction方法,;消息内容是=飞哥测试事务消息tagE_9
broker调用checkLocalTransaction 方法回查了15次。
消费者:
我们可以看到最终消费者消费到的是消费的tags是tagA以及tagC的四条消息。
那么,为啥生产者发送的half消息,消费者不会里面收到呢?这是因为half消息会被放到 RMQ_SYS_TRANS_OP_HALF_TOPIC 的topic中,直到本地事务返回 COMMIT_MESSAGE 状态时,消费者才能消费到此消息 。