文章目录
1. rocketMQ的消息类型
RocketMQ的消息发送和接收都是有个比较固定的步骤的,大致如下:
下面来看一下RocketMQ都支持哪些类型的消息:
1.1 消息的发送和接收方式
生产者消息发送时有三种形式
- 单向发送:关键点就是使用producer.sendOneWay方式来发送消息,这个方法没有返回值,也没有回调。就是只管把消息发出去就行了。吞吐量非常高,但容易丢消息
- 同步发送:producer发送时会同步等待broker返回一个发送状态。如果失败会重试。吞吐量最低,但安全
- 异步发送:producer在发送后去做自己的事情,异步接受broker的回调结果,比较有趣的地方就是引入了一个countDownLatch来保证所有消息回调方法都执行完了再关闭Producer。 所以从这里可以看出,RocketMQ的Producer也是一个服务端,在往Broker发送消息的时候也要作为服务端提供服务。异步代码如下:
消费者在接收消息时,有两种模式
- . 拉模式:消费者主动去Broker上拉取消息。拉那里的消息要根据主题和tag过滤,在拉消息时有两种方式:
- 自己管理offset:由于每次拉取消息个数有限,需要多次拉取。offset偏移量,记录着上次拉取消息的位置。目的是防止消息被重复消费。使用时可以把offset存在redis中,这样就可以灵活控制消息的消费位置。
- 不需管理offset的方式,默认一次拉取32条
// 不指定offset,直接拉取
DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("lite_pull_consumer_test");
litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
litePullConsumer.subscribe("TopicTest", "*");
litePullConsumer.start();
- 推模式:Broker收到消息后,主动推送到消费者上。实现形式是:消费端注册一个监听器MessageListenerConcurrently,监听着broker上的消息,如果broker上有新消息,则触发监听器MessageListenerConcurrently。broker会自动往监听器中的ConsumeMessage()方法中推送消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
// 在consumeMessage方法中处理消息
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
注意:消费者只负责接收Broker中的消息,或者通过offset偏移量去拉取broker中的消息。但消息消费后,消费者不会擅自删除broker中的消息。broekr中的消息删除由配置文件配置,可配置项:
- 消息存储时长
- 消息删除时间
1.2 顺序消息
顺序消息(FIFO 消息)是 MQ 提供的一种严格按照顺序进行发布和消费的消息类型。顺序消息包含两种类型:
- 局部有序:即一个有序流程内,消息是有序的,但可以允许其他流程的消息插入。这也是RocketMQ提供的顺序类型。
- 全局有序:严格要求所有消息有序。全局顺序将面临性能的问题,即Topic只有一个分区,而且绝大多数场景都不需要全局顺序
全局有序和局部有序哪个更重要?
其实在大部分的MQ业务场景中,我们只需要能够保证局部有序就可以了。例如我们用QQ聊天,只需要保证一个聊天窗口里的消息有序就可以了,其他的聊天窗口中的内容与当前聊天窗口中的内容无关。而对于电商订单场景,也只要保证一个订单的所有消息是有序的就可以了!
通常情况下,发送者发送消息时,会通过MessageQueue轮询的方式保证消息尽量均匀的分布到所有的MessageQueue上。MessageQueue是RocketMQ存储消息的最小单元,分布在不同的Broker上,他们之间的消息都是互相隔离的。消费者可以任意的从不同机器上的MessageQueue获取消息,由于不能保证消费者每一次都从相同的MessageQueue上获取消息,所以在这种情况下,是无法保证消息全局有序的。
问题:那么rocketMQ是如何保证消息局部有序的呢?
只需要将有序的一组消息都存入同一个MessageQueue里,这样MessageQueue的FIFO设计天生就可以保证这一组消息的有序。RocketMQ中,可以在发送者发送消息时
指定一个MessageSelector对象,让这个对象来决定消息发入哪一个MessageQueue。这样就可以保证一组有序的消息能够发到同一个MessageQueue里。消费消息时
使用分布式锁机制,保证多个消费者就有一个进入队列获取消息,防止并发消费导致消息的消费顺序被打乱
在MQ的模型中,顺序需要由3个阶段去保障:
- 有序发送:消息被发送时保持顺序。对于有顺序要求的消息,用户应该在同一个线程中采用同步的方式发送。多线程发送的消息,不同线程间的消息不是顺序发布的,同一线程的消息是顺序发布的,这是需要用户自己去保障的。注意这里是有顺序要求的消息,对于多组有序消息组,可以使用多线程。
- 有序存储:消息被存储在broker时保持和发送的顺序一致,要求在同一线程中被发送出来的消息A和B,存储时在空间上A一定在B之前
- 有序消费:消息被消费时保持和存储的顺序一致:要求消息A、B到达Consumer之后必须按照先A后B的顺序被处理。
如下图所示:
对于两个订单的消息的原始数据:a1、b1、b2、a2、a3、b3(用户点击时机产生的无序数据)
- 在发送时,a订单的消息需要保持a1、a2、a3的顺序,b订单的消息也相同,但是a、b订单之间的消息没有顺序关系,这意味着a、b订单的消息可以在不同的线程中被发送出去
- 在存储时,保持和发送的顺序一致,但是a、b订单之间的消息的顺序可以不保证,比如:
a1、b1、b2、a2、a3、b3是可以接受的
a1、a2、b1、b2、a3、b3也是可以接受的
a1、a3、b1、b2、a2、b3是不能接受的 - 消费时保证顺序需要使用分布式锁对MessageQueue加锁,防止并发消费导致消息乱序!
Producer端
roducer端确保消息顺序唯一要做的事情就是将消息路由到特定的messagequeue,在RocketMQ中,通过MessageQueueSelector来实现messagequeue的选择。
// 每一个订单处理的消息有多步,但前缀都是orderId
Message msg =
new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// arg = orderId
Integer id = (Integer) arg;
// 通过orderId 和 队列数取模运算,得到要放到哪个队列
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
Consumer端
消费者端要保证消息有序,就需要按队列一个一个来取消息,即取完一个队列的消息后,再去取下一个队列的消息。而给consumer注入的MessageListenerOrderly对象,在RocketMQ内部就会通过锁队列的方式阻止并发消费,保证消息顺序。源码如下图
而普通的MessageListenerConcurrently这个消息监听器则不会锁队列,每次都是从多个Message中取一批数据(默认不超过32条)。因此也无法保证消息有序。源码如下图
Consumer端代码如下:
// MessageListenerOrderly 加锁,锁住队列保证消费顺序
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for(MessageExt msg:msgs){
System.out.println("收到消息内容 "+new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
1.3 广播消息
Rocketmq 消费者的消费方式分两种
- 集群模式:集群模式下,当一个消费者消费了某个消息,其他消费者就不再消费这个消息了。rocketmq默认是集群模式
- 广播模式:所有订阅同一个主题的消费者都会收到消息
广播模式代码实现上其实很简单,就是在消费端开启广播模式即可,如下
// 开启广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 订阅主题
consumer.subscribe("TopicTest", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
1.4 延时消息
延迟消息是RocketMQ特有的一个功能,像rabbitMQ只能结合死信队列迂回的实现。延迟消息的效果就是在调用producer.send方法后,消息并不会立即发送出去,而是会等一段时间再发送出去。那会延迟多久呢?延迟时间的设置就是在Message消息对象上设置一个延迟级别message.setDelayTimeLevel(3);
开源版本的RocketMQ中,对延迟消息并不支持任意时间的延迟设定(商业版本中支持),而是只支持18个固定的延迟级别,1到18分别对应:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。
Message msg = new Message("TopicTest" ,
"TagA" ,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
// 设置延时级别为第三级别10s后发送
msg.setDelayTimeLevel(3);
// 发消息
SendResult sendResult = producer.send(msg);
在broker存储消息时,源码中会判断是否是延时消息,如果是的话,则会修改这个消息的Topic和QueueId等信息,消息发过来之后,会先把消息存入Schedule_Topic主题中对应的队列。然后等延迟时间到了,再转发到目标队列,推送给消费者进行消费。
整个延迟消息的实现方式是这样的:
这个转储到Schedule_Topic主题中的核心服务是scheduleMessageService,这个其中有个需要注意的点就是在ScheduleMessageService的start方法中。有一个很关键的CAS操作:
这个CAS操作保证了同一时间只会有一个延时器执行。保证了消息安全的同时也限制了消息进行回传的效率。所以,这也是很多互联网公司在使用RocketMQ时,对源码进行定制的一个重点(虽然安全但效率慢)。
1.5 批量消息
批量消息是指将多条消息合并成一个批量消息,一次发送出去。这样的好处是可以减少网络IO,提升吞吐量。批量消息的使用是有一定限制的,这些消息应该有相同的Topic,相同的waitStoreMsgOK。而且不能是延迟消息、事务消息等。
官网也提示:如果批量消息大于1MB就不要用一个批次发送,而要拆分成多个批次消息发送。也就是说,一个批次消息的大小不要超过1MB,实际使用时,这个1MB的限制可以稍微扩大点,实际最大的限制是4194304字节,大概4MB。
String topic = "BatchTest";
// list集合存储批量消息
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
// 发送批量消息
producer.send(messages);
1.6 过滤消息
过滤消息目的是把消息发送到Topic后,让消费端根据不同的过滤条件,消费对应的消息。RocketMQ的最佳实践中就建议,使用RocketMQ时,一个应用可以就用一个Topic,而应用中的不同业务就用Tag来区分。过滤消息分为两种:
- Tag过滤
- Sql过滤
过滤消息的精髓在于:在broker提前进行消息过滤。如果要在消费端进行过滤的话,borker要把消息都发给消费者,然后消费者过滤后再通知broker,这样会产生多次不必要的消息传输。所以rokcetmq为了避免上述情况,选择了在broker端进行消息过滤,过滤完成后直接推送给消费者。
按Tag过滤:
Tag是RocketMQ中特有的一个消息属性,用以区分应用中的不同业务。下面代码中生产者把消息轮询分发给Topic下不同的Tag,消费者根据Tag过滤只消费对应的消息。代码如下:
生产者:
// Tags数组
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 15; i++) {
// 消息发送到TagFilterTest主题下
Message msg = new Message("TagFilterTest",
// 取模,轮询发送到不同的tag中
tags[i % tags.length],
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
消费者:
// 过滤条件,只消费TagFilterTest主题下的TagA 和 TagB下的消息
consumer.subscribe("TagFilterTest", "TagA || TagC");
// 使用MessageListenerConcurrently 接收borker推过来的消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
按Sql过滤:
这个模式的关键是在消费者端使用MessageSelector.bySql(String sql)返回的一个MessageSelector。这里面的sql语句是按照SQL92标准来执行的。sql中可以使用的参数有默认的TAGS和一个在生产者中加入的a属性。
SQL92语法:
- 数值比较,比如:>,>=,<,<=,BETWEEN,=
- 字符比较,比如:=,<>,IN
- IS NULL 或者 IS NOT NULL
- 逻辑符号 AND,OR,NOT
使用注意:只有推模式的消费者可以使用SQL过滤。拉模式是用不了的。
生产者:
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 15; i++) {
Message msg = new Message("SqlFilterTest",
tags[i % tags.length],
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 这里给Message增加了a属性,后续消费者还要按照a属性去过滤
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
消费者:
// 过滤条件:tags不为空,且只消费TagFilterTest主题下的TagA 和 TagB下的消息
// 同时让Message的属性a也不为空,而且a的值介于 0 - 3 之间
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"and (a is not null and a between 0 and 3)"));
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
可以看到,按照sql过滤的方式,比按Tag过滤更加灵活,且支持更加多元化的操作
1.7 事务消息
如何保证生产者能把消息发送到RocketMQ?
- 同步发送+多次重试,最通用的方案
- RocketMQ的事务消息(回查就类似于重试),下面讲
事务消息是RcoketMQ非常重要的一个功能,官网的介绍是:事务消息是在分布式系统中保证最终一致性的两阶段提交的消息实现。他可以保证本地事务执行与消息发送两个操作的原子性,也就是这两个操作一起成功或者一起失败。因此,事务消息只涉及到消息发送者,对于消息消费者来说,并没有什么特别的。
事务消息的消息发送者与上面的顺序消息、广播消息、延时消息都不一样,不同之处如下:
- 消息发送者实例为 TransactionMQProducer
- 消息发送时启动事务监听器(需要实现TransactionListener接口),根据监听器中的逻辑发送消息!
// 其他消息类型的消息发送者 DefaultMQProducer
DefaultMQProducer producer = new DefaultMQProducer("topicTest");
// 事务消息类型的消息发送者 TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("topicTest");
接下里看一下代码示例:
生产者
//实例化事务监听器
TransactionListener transactionListener = new TransactionListenerImpl();
//消息发送者producer
TransactionMQProducer producer = new TransactionMQProducer("TopicTest");
//建立nameServer连接
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设置线程池 和 事务监听器
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
//启动producer
producer.start();
//tag数组
String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
//轮询的向5个Tag中发送消息,每个tag发送2个消息
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();
}
}
生产者代码如上,在基本消息类型中,5个Tag每个会收到两条消息,但是事务消息类型加入了事务监听器,影响了消息的发送逻辑,具体怎么影响的呢?看一下监听器的实现案例
事务监听器
//事务监听器需要实现 TransactionListener 接口
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
// 发送消息时走 executeLocalTransaction 方法逻辑
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String tags = msg.getTags();
if(StringUtils.contains(tags,"TagA")){
//返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
return LocalTransactionState.COMMIT_MESSAGE;
}else if(StringUtils.contains(tags,"TagB")){
//返回ROLLBACK_MESSAGE状态的消息会被丢弃
return LocalTransactionState.ROLLBACK_MESSAGE;
}else{
//返回UNKNOW状态的消息会等待Broker进行事务状态回查
return LocalTransactionState.UNKNOW;
}
}
// 事务状态回查时,执行 checkLocalTransaction 方法,回查触发需要需要一定时间
// 在 Broker 配置文件中的参数 transactionMsgTimeout 可以配置多久之后被检查
//当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String tags = msg.getTags();
if(StringUtils.contains(tags,"TagC")){
//过一段时间后,TagC的消息被消费者消费到
return LocalTransactionState.COMMIT_MESSAGE;
}else if(StringUtils.contains(tags,"TagD")){
//TagD的消息也会在状态回查时被丢弃掉
return LocalTransactionState.ROLLBACK_MESSAGE;
}else{
//剩下TagE的消息会继续回查,回查15次依旧没有commit,则最终被丢弃
//可以通过 Broker 配置文件的 transactionCheckMax参数来修改15次限制
return LocalTransactionState.UNKNOW;
}
}
}
有了事务监听器对消息发送逻辑的影响,真实的接受结果其实是:TagA的消息立马被消费,过了一会TagC的消息被消费,其他消息都未被消费,都被丢弃!
造成这种现象的原因都写在了事务监听器中:
- TagA的消息立马被消费者消费,TagB的消息直接被丢弃,TagC、TagD、TagE的消息由于是UNKNOW状态,会在过一段时间后执行回查方法。
- 在回查方法中,TagC的消息会被消费,TagD的消息被丢弃。
- TagE的消息由于是UNKNOW状态继续回查,直到15次默认回查执行完毕,TagE依旧是UNKNOW,此时TagE的消息被直接丢弃!并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
通过事务监听器的executeLocalTransaction 和 checkLocalTransaction方法,以及不同的事务状态,使rokcetMQ在发送消息时的每一步都可以反悔,可以灵活地根据业务场景、本地事务执行结果来决定是否提交或丢弃这条消息!
事务消息的实现机制如下:
以订单系统为例,上图的事务消息生产者即为订单生产者
- 订单生产者先发送一个 half半消息 给rocketMQ,并存入RocketMQ内部的一个 RMQ_SYS_TRANS_HALF_TOPIC 这个Topic,这样对消费者是不可见的。这个half半消息的作用在执行本地事务之前(比如下单),先确定rocketMQ的状态正常,如果broker挂掉,订单系统是接收不到broker反馈的!
- 订单系统接收到Broker对half半消息的反馈,证明了broker状态正常
- 执行本地事务,比如下单入库
- 如果本地事务执行正常,发消息时,在事务监听器中设置事务状态为提交COMMIT_MESSAGE,此时消息提交到Topic由消费者正常消费。
如果本地事务执行异常,发消息时,在事务监听器中设置事务状态为提交ROLLBACK_MESSAGE,此时该条消息被直接丢弃
如果本地事务执行时有多个环节关联,比如下单后在5分钟内需要完成支付操作,之后才会发消息给会员等系统消费。在这种场景下,可以在事务监听器中设置事务状态为UNKNOW,通过回查方法中制定的逻辑,也就是检查该订单是否被支付。如果已支付,在回查方法中设置事务状态为提交COMMIT_MESSAGE,此时才会进行消息提交,否则继续回查,直到默认15次被丢弃!被丢弃意味着:这条消息已死,将不会被消费,不会增加会员积分
事务消息的使用限制
明白了事务消息的原理,也要了解下事务消息的使用限制:
- 事务消息仅仅保证了分布式事务的一半,因为消费者的消费成功与否RocketMQ管不着,要保证 生产者-broker-消费者 事务一致的话,需要用到分布式事务,而RocketMQ的事务消息仅仅保证 生产者-broker 之间的事务一致
- 事务消息不支持延迟消息和批量消息。
- 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
更多事务消息相关 请看 https://www.bilibili.com/read/cv3318498/
2. ACL权限控制
权限控制(ACL)主要为RocketMQ提供Topic资源级别的用户访问控制,用户在使用RocketMQ权限控制时,可以在Client客户端 通过 RPCHook注入AccessKey和SecretKey签名。Broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常
配置步骤
- 在broker.conf中打开acl的标志:aclEnable=true
- 在plan_acl.yml中配置ACL权限,这个配置文件是热加载的,也就是说要修改配置时,只要修改配置文件就可以了,不用重启Broker服务。配置如下:
#全局白名单,不受ACL控制
#通常需要将主从架构中的所有节点加进来
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*
accounts:
#第一个账户
- accessKey: RocketMQ
secretKey: 12345678
whiteRemoteAddress:
admin: false
defaultTopicPerm: DENY #默认Topic访问策略是拒绝
defaultGroupPerm: SUB #默认Group访问策略是只允许订阅
topicPerms:
- topicA=DENY #topicA拒绝
- topicB=PUB|SUB #topicB允许发布和订阅消息
- topicC=SUB #topicC只允许订阅
groupPerms:
# the group should convert to retry topic
- groupA=DENY
- groupB=PUB|SUB
- groupC=SUB
#第二个账户,只要是来自192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2
secretKey: 12345678
whiteRemoteAddress: 192.168.1.*
# if it is admin, it could access all resources
admin: true
- 如果要在自己的客户端中使用RocketMQ的ACL功能,还需要引入一个单独的依赖包
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
<version>4.7.1</version>
</dependency>
更多配置信息请参考rocketMQ源码包下的rocketmq-all-4.7.1-source-release\docs\cn\acl\user_guide.md文件,里面有更详细的解释!
3. RocketMQ消息轨迹
打开消息轨迹功能,需要在broker.conf中打开一个关键配置:traceTopicEnable=true
这个配置的默认值是false。也就是说默认是关闭的。
- 生产者开启消息轨迹(true)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true);
- 消费者开启消息轨迹(true)
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true);
消息轨迹数据的关键属性:
默认情况下,消息轨迹数据是存于一个系统级别的Topic :RMQ_SYS_TRACE_TOPIC
。这个Topic在Broker节点启动时,会自动创建出来。
另外,也支持客户端自定义轨迹数据存储的Topic。在客户端的两个核心对象 DefaultMQProducer和DefaultMQPushConsumer,他们的构造函数中,都有两个可选的参数来打开消息轨迹存储
- enableMsgTrace:是否打开消息轨迹。默认是false。
- customizedTraceTopic:配置将消息轨迹数据存储到用户指定的Topic 。
4. springboot整合RocketMQ
在使用SpringBoot的starter集成包时,要特别注意版本。因为SpringBoot集成RocketMQ的starter依赖是由Spring社区提供的,目前正在快速迭代的过程当中,不同版本之间的差距非常大,甚至基础的底层对象都会经常有改动。例如如果使用rocketmq-spring-boot-starter:2.0.4版本开发的代码,升级到目前最新的rocketmq-spring-boot-starter:2.1.1后,基本就用不了了。
引入依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.1</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
配置文件
# nameServer地址
rocketmq.name-server=192.168.232.128:9876
# 生产者组
rocketmq.producer.group=springBootGroup
生产者:
@Component
public class SpringProducer {
@Resource
private RocketMQTemplate rocketMQTemplate;
// 发送普通消息
public void sendMessage(String topic,String msg){
this.rocketMQTemplate.convertAndSend(topic,msg);
}
//发送事务消息
public void sendMessageInTransaction(String topic,String msg) throws InterruptedException {
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
//尝试在Header中加入一些自定义的属性。
Message<String> message = MessageBuilder.withPayload(msg)
.setHeader(RocketMQHeaders.TRANSACTION_ID,"TransID_"+i)
//发到事务监听器里后,这个自己设定的TAGS属性会丢失。但是上面那个属性不会丢失。
.setHeader(RocketMQHeaders.TAGS,tags[i % tags.length])
//MyProp在事务监听器里也能拿到,为什么就单单这个RocketMQHeaders.TAGS拿不到?这只能去调源码了。
.setHeader("MyProp","MyProp_"+i)
.build();
String destination =topic+":"+tags[i % tags.length];
//这里发送事务消息时,还是会转换成RocketMQ的Message对象,再调用RocketMQ的API完成事务消息机制。
SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message,destination);
System.out.printf("%s%n", sendResult);
Thread.sleep(10);
}
}
}
消费者:
// @RocketMQMessageListener 中接收消息,所有消息配置都在注解中
@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode= ConsumeMode.CONCURRENTLY)
public class SpringConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("Received message : "+ message);
}
}
SpringBoot集成RocketMQ,消费者部分的核心就在这个@RocketMQMessageListener注解上。所有消费者的核心功能也都会集成到这个注解中。所以我们还要注意下这个注解里面的属性:
例如:
- 消息过滤可以由里面的selectorType属性和selectorExpression来定制
- 消息有序消费还是并发消费则由consumeMode属性定制。
- 消费者是集群部署还是广播部署由messageModel属性定制。
然后关于事务消息,还需要配置一个事务消息监听器:
/**
* @description: 事务消息监听器
* 关于@RocketMQTransactionListener 这个注解,有点奇怪。2.0.4版本中,是需要指定txProducerGroup指向一个消息发送者组。不同的组可以有不同的事务消息逻辑。
* 但是到了2.1.1版本,只能指定rocketMQTemplateBeanMame,也就是说如果你有多个发送者组需要有不同的事务消息逻辑,那就需要定义多个RocketMQTemplate。
* 而且这个版本中,虽然重现了我们在原生API中的事务消息逻辑,但是测试过程中还是发现一些奇怪的特性,用的时候要注意点。
**/
//@RocketMQTransactionListener(txProducerGroup = "springBootGroup2")
@RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate")
public class MyTransactionImpl implements RocketMQLocalTransactionListener {
private ConcurrentHashMap<Object, Message> localTrans = new ConcurrentHashMap<>();
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
Object transId = msg.getHeaders().get(RocketMQHeaders.PREFIX+RocketMQHeaders.TRANSACTION_ID);
String destination = arg.toString();
localTrans.put(transId,msg);
//这个msg的实现类是GenericMessage,里面实现了toString方法
//在Header中自定义的RocketMQHeaders.TAGS属性,到这里就没了。但是RocketMQHeaders.TRANSACTION_ID这个属性就还在。
//而message的Header里面会默认保存RocketMQHeaders里的属性,但是都会加上一个RocketMQHeaders.PREFIX前缀
System.out.println("executeLocalTransaction msg = "+msg);
//转成RocketMQ的Message对象
org.apache.rocketmq.common.message.Message message = RocketMQUtil.convertToRocketMessage(new StringMessageConverter(),"UTF-8",destination, msg);
String tags = message.getTags();
if(StringUtils.contains(tags,"TagA")){
return RocketMQLocalTransactionState.COMMIT;
}else if(StringUtils.contains(tags,"TagB")){
return RocketMQLocalTransactionState.ROLLBACK;
}else{
return RocketMQLocalTransactionState.UNKNOWN;
}
}
//延迟检查
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String transId = msg.getHeaders().get(RocketMQHeaders.PREFIX+RocketMQHeaders.TRANSACTION_ID).toString();
Message originalMessage = localTrans.get(transId);
//这里能够获取到自定义的transaction_id属性
System.out.println("checkLocalTransaction msg = "+originalMessage);
//获取标签时,自定义的RocketMQHeaders.TAGS拿不到,但是框架会封装成一个带RocketMQHeaders.PREFIX的属性
// String tags = msg.getHeaders().get(RocketMQHeaders.TAGS).toString();
String tags = msg.getHeaders().get(RocketMQHeaders.PREFIX+RocketMQHeaders.TAGS).toString();
if(StringUtils.contains(tags,"TagC")){
return RocketMQLocalTransactionState.COMMIT;
}else if(StringUtils.contains(tags,"TagD")){
return RocketMQLocalTransactionState.ROLLBACK;
}else{
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
总结:
- SpringBoot 引入org.apache.rocketmq:rocketmq-spring-boot-starter依赖后,就可以通过内置的RocketMQTemplate来与RocketMQ交互。相关属性都以rockemq.开头。具体所有的配置信息可以参见org.apache.rocketmq.spring.autoconfigure.RocketMQProperties这个类。
- SpringBoot依赖中的Message对象和RocketMQ-client中的Message对象是两个不同的对象,这在使用的时候要非常容易弄错。例如RocketMQ-client中的Message里的TAG属性,在SpringBoot依赖中的Message中就没有。Tag属性被移到了发送目标中,与Topic一起,以Topic:Tag的方式指定。