延迟消息
应⽤场景:
延迟消息发送是指消息发送到Apache RocketMQ后,并不期望⽴⻢投递这条消息,⽽是延迟⼀定时间后才投递到Consumer进⾏消费。
核⼼⽅法:
当前版本RocketMQ提供了两种实现延迟消息的机制,⼀种是指定固定的延迟级别,⼀种是指定消息发送时间。
⽣产者端核⼼代码
// 指定固定的延迟级别
Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8));
message.setDelayTimeLevel(3); //10秒之后发送
// 指定消息发送时间
Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8));
message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L); //指定10秒之后的时间点
关于延迟级别,RocketMQ给消息定制了18个默认的延迟级别
应⽤只需要根据⾃⼰的业务要求,选择对应的延迟级别即可。
实现思路:
对于指定固定延迟级别的延迟消息,RocketMQ的实现⽅式是预设⼀个系统Topic,名字叫做SCHEDULE_TOPIC_XXXXX。在这个Topic下,预设了18个MessageQueue。这⾥每个对列就对应了⼀种延迟级别。然后每次扫描这18个队列⾥的消息,进⾏延迟操作就可以了。
另外指定时间点的延迟消息,RocketMQ是通过时间轮算法实现的。
批量消息
应⽤场景:
⽣产者要发送的消息⽐较多时,可以将多条消息合并成⼀个批量消息,⼀次性发送出去。这样可以减少⽹络IO,提升消息发送的吞吐量。
示例代码:
⽣产者核⼼代码:
List<Message> messages = new ArrayList<>(MESSAGE_COUNT);
for (int i = 0; i < MESSAGE_COUNT; i++) {
messages.add(new Message(TOPIC, TAG, "OrderID" + i, ("Hello world " +i).getBytes(StandardCharsets.UTF_8)));
}
//split the large batch into small ones:
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
List<Message> listItem = splitter.next();
SendResult sendResult = producer.send(listItem);
System.out.printf("%s", sendResult);
}
注意点:
批量消息的使⽤⾮常简单,但是要注意RocketMQ做了限制。同⼀批消息的Topic必须相同,另外,不支持延迟消息。
还有批量消息的⼤⼩不要超过1M,如果太⼤就需要⾃⾏分割。
事务消息
应⽤场景:
事务消息是RocketMQ⾮常有特色的⼀个⾼级功能。他的基础诉求是通过RocketMQ的事务机制,来保证上下游的数据一致性。
以电商为例,⽤户⽀付订单这⼀核⼼操作的同时会涉及到下游物流发货、积分变更、购物⻋状态清空等多个⼦系统的变更。这种场景,⾮常适合使⽤RocketMQ的解耦功能来进行串联。
考虑到事务的安全性,即要保证相关联的这⼏个业务⼀定是同时成功或者同时失败的。如果要将四个服务⼀起作为⼀个分布式事务来控制,可以做到,但是会非常麻烦。⽽使⽤RocketMQ在中间串联了之后,事情可以得到⼀定程度的简化。由于RocketMQ与消费者端有失败重试机制,所以,只要消息成功发送到RocketMQ了,那么可以认为Branch2.1,Branch2.2,Branch2.3这⼏个分⽀步骤,是可以保证最终的数据⼀致性的。这样,⼀个复杂的分布式事务问题,就变成了MinBranch1和Branch2两个步骤的分布式事务问题。
然后,在此基础上,RocketMQ提出了事务消息机制,采用两阶段提交的思路,保证Main Branch1和Branch2之间的事务一致性。
具体的实现思路是这样的:
- ⽣产者将消息发送⾄Apache RocketMQ服务端。
- Apache RocketMQ服务端将消息持久化成功之后,向⽣产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
- ⽣产者开始执⾏本地事务逻辑。
- ⽣产者根据本地事务执⾏结果向服务端提交⼆次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- ⼆次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- ⼆次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
- 在断⽹或者是⽣产者应⽤重启的特殊情况下,若服务端未收到发送者提交的⼆次确认结果,或服务端收到的⼆次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息⽣产者即⽣产者集群中任⼀⽣产者实例发起消息回查。
- ⽣产者收到消息回查后,需要检查对应消息的本地事务执⾏的最终结果。
- ⽣产者根据检查到的本地事务的最终状态再次提交⼆次确认,服务端仍按照步骤4对半事务消息进⾏处理。
实现时的重点是使⽤RocketMQ提供的TransactionMQProducer事务⽣产者,在TransactionMQProducer中注⼊⼀个TransactionListener事务监听器来执⾏本地事务,以及后续对本地事务的检查。
注意点:
- 半消息是对消费者不可⻅的⼀种消息。实际上,RocketMQ的做法是将消息转到了⼀个系统Topic,RMQ_SYS_TRANS_HALF_TOPIC。
- 事务消息中,本地事务回查次数通过参数transactionCheckMax设定,默认15次。本地事务回查的间隔通过参数transactionCheckInterval设定,默认60秒。超过回查次数后,消息将会被丢弃。
- 其实,了解了事务消息的机制后,在具体执⾏时,可以对事务流程进行适当的调整。
ACL权限控制机制
应⽤场景:
RocketMQ提供了针对队列、⽤户等不同维度的⾮常全⾯的权限管理机制。通常来说,RocketMQ作为⼀个内部服务,是不需要进⾏权限控制的,但是,如果要通过RocketMQ进⾏跨部门甚⾄跨公司的合作,权限控制的重要性就显现出来了。
权限控制体系:
1、RocketMQ针对每个Topic,就有完整的权限控制。⽐如,在控制平台中,就可以很⽅便的给每个Topic配置权限。
perm字段表示Topic的权限。有三个可选项。 2:禁写禁订阅,4:可订阅,不能写,6:可写可订阅
2、在Broker端还提供了更详细的权限控制机制。主要是在broker.conf中打开acl的标志:aclEnable=true。然后就可以⽤他提供的plain_acl.yml来进行权限配置了。并且这个配置⽂件是热加载的,也就是说要修改配置时,只要修改配置⽂件就可以了,不⽤重启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
接下来,在客户端就可以通过accessKey和secretKey提交身份信息了。客户端在使⽤时,需要先引⼊⼀个Maven依赖包。
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
<version>4.9.1</version>
</dependency>
然后在声明客户端时,传⼊⼀个RPCHook。
/声明时传⼊RPCHook
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook());
private static final String ACL_ACCESS_KEY = "RocketMQ";
private static final String ACL_SECRET_KEY = "1234567";
static RPCHook getAclRPCHook() {
return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY,ACL_SECRET_KEY));
}