RocketMQ集群部署结构
1、Name Server
NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。
Name Server 主要包括两个功能:
- Broker管理:NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
- 路由信息管理:每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。
Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。任意一个 Name Server 包含所有集群的信息。
2、Broker
集群最核心模块,主要负责Topic消息存储、消费者的消费位点管理(消费进度);
Broker会注册到 Name Server上去,无论是否是主从, 每个 Broker 都会注册到 Name Server 上;
Broker部署相对复杂,Broker分为Master和Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
每个Broker 与Name Server集群中的所有节点建立长连接,定时(每隔30秒)注册Topic信息到所有Name Server。Name Server定时(每隔10秒)扫描所有存活的Broker连接,如果Name Server超过两分钟没有收到心跳。则Name Server断开与Broker 的连接。
每个 Broker 都会创建一个 consumerOffset.json 的文件,记录当前消费的节点指向了哪条消息,即消费的偏移量;偏移量是由 Consumer 上报的,Consumer 会定时或者 Kill 阶段提交各自对应 queue 的 offset 位置,为了避免消息的重复推送;consumerOffset.json 的文件格式:
3、producer
Producer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从Name Server中取Topic路由信息,并向提供Topic服务的Master建立长连接(基于 Netty),且定时向Master发送心跳。Producer 完全无状态,可集群部署。
Producer 每隔30秒(由ClientConfig的pollNameServerInterval)从Name Server 获取所有的Topic 队列的最新情况,这意味着如果Broker 不可用,Producer 最多30秒感知到。在此期间内发往Broker的所有消息都会失败。
Producer 每隔30秒 (由ClientConfig中heartbeatBrokerInterval决定) 向所有关联的 Broker 发送心跳,Broker 每隔10秒扫描所有存活的的连接,如果Borker 在2分钟内没有收到心跳数据,则关闭与Producer的连接。
4、Consumer:
Consumer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从Name Server 取 Topic 路由信息,并向提供Topic 服务的Master、 Slave 建立长连接(基于 Netty)且定时向 Master、Slave 发送心跳。Consumer 既可以从Master 订阅消息,也可以从Slave 订阅消息,订阅规则由 Broker 配置决定。
Consumer 每隔30秒 从Name Server 获取Topic 的最新队列情况,这意味着Broker 不可用时,Consumer 最多需要30秒 即可感知。
Consumer 每隔30秒(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的Broker 发送心跳,Broker 每隔 10 秒扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group 的所有Consumer发出通知,Group内的所有Consumer 重新分配队列,然后继续消费。
当Consumer得到 Master 宕机通知后,转向Slave 消费,Slave 的消息不对保证100%都同步过来了,因此会有少量的消息丢失。但是一旦Master 恢复,未同步过去的消息会被最终消费掉。
消费者队列是消费者连接之后(或之前连接过)才创建的。我们将原生的消费者标识由{IP}@{消费者group}扩展为 {IP}@{消费者group}{topic}{tag},(例如xxx.xxx.xxx.xxx@mqtest_producer-group_2m2sTest_tag-zyk)。任何一个元素不同都认为是不同的消费端,每个消费端会拥有一份自己的消费队列(默认是Borker队列数量*Broker数量)。新挂载的消费都队列中拥有CommitLog 的所有数据。
Topic
每个 Broker 上都会创建 Topic;每个 Topic 都会对应一个 CommitLog,真实的消息都存储在 CommitLog 里面;
每个 Topic 在创建之初都会默认创建 4 个队列(queue-0,queue-1,queue-2,queue-3),每个队列都会对应一个持久化的文件;Producer 向 Broker 上的 Topic 发送消息,若发现队列没有创建持久化的文件,则会创建相应的持久化文件 queueLog,queueLog 记录的每条消息在 CommitLog 中的位置等信息。
Producer Group
同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事物消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生 产者实例以提交或回溯消费。
Consumer Group
同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。
RocketMQ 支持两种消息模式:集群消费 (Clustering)和广播消费(Broadcasting)。
RocketMQ的特性
1、Producer 端
org.apache.rocketmq.client.impl.CommunicationMode
public enum CommunicationMode {
SYNC,
ASYNC,
ONEWAY
}
RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。
同步发送(SYNC):消息发送给 Broker之后,Broker给了反馈信息之后,生产者的代码才会继续向下执行;
异步发送(ASYNC):消息发送给 Broker 之后,不用等待Broker的响应,发送完成后会有回调函数去处理;异步的发送方式,发送完后,立刻返回。Client 在拿到 Broker 的响应结果后,会回调指定的 callback. 这个 API 也可以指定 Timeout,不指定也是默认的 3000ms;
单向发送(ONEWAY):发出去后,什么都不管直接返回。
2、Consumer 端
Consumer 消费消息有两种方式:拉取消费 和 推送消费;
拉取式消费(Pull Consumer):应用通常主动调用Consumer的拉消息方法从Broker服务 器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
推动式消费(Push Consumer):该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。
3、消息订阅模式
广播模式:广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。
org.apache.rocketmq.common.protocol.heartbeat.MessageModel#BROADCASTING
集群模式:集群消费模式下,相同Consumer Group 的每个 Consumer 实例平均分摊消息。
org.apache.rocketmq.common.protocol.heartbeat.MessageModel#CLUSTERING
4、消费点位
当建立一个新的消费者组时,需要决定是否需要消费已经存在于 Broker 中的历史消息:org.apache.rocketmq.common.consumer.ConsumeFromWhere
- CONSUME_FROM_LAST_OFFSET 将会忽略历史消息,并消费之后生成的任何消息。
- CONSUME_FROM_FIRST_OFFSET 将会消费每个存在于 Broker 中的信息。
- CONSUME_FROM_TIMESTAMP 消费在指定时间戳后产生的消息。
5、消息重复幂等
RocketMQ无法避免消息重复,所以如果业务对消费重复非常敏感,务必要在业务层面去重
幂等令牌是生产者和消费者两者中的既定协议,在业务中通常是具备唯一业务标识的字符串,如:订单号、流水号等。且一般由生产者端生成并传递给消费者端。
6、批量消息
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应 超过4MB。rocketmq建议每次批量消息大小大概在1MB。 当消息大小超过4MB时,需要将消息进行分割。
7、过滤消息
大多数情况下,可以通过TAG来选择您想要的消息。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
使用Filter功能,需要在启动配置文件当中配置以下选项:
enablePropertyFilter=true
消费者将接收包含 TAGA 或 TAGB 或 TAGC 的消息。但是限制是一个消息只能有一个标 签,这对于复杂的场景可能不起作用。在这种情况下,可以使用 SQL 表达式筛选消息。SQL 特性可以通过发送消息时的属性来进行计算。在 RocketMQ 定义的语法下,可以实现一些 简单的逻辑。
生产者:
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("filter_sample_group");
producer.setNamesrvAddr("192.168.241.198:9876");
producer.start();
for (int i = 0; i < 3; i++) {
Message msg = new Message("TopicFilter",
"TAG-FILTER",
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
msg.putUserProperty("a",String.valueOf(i));
if(i % 2 == 0){
msg.putUserProperty("b","yangguo");
}else{
msg.putUserProperty("b","xiaolong girl");
}
producer.send(msg);
}
producer.shutdown();
}
消费者:
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("filter_sample_group");
/**
* 注册中心
*/
consumer.setNamesrvAddr("192.168.241.198:9876");
/**
* 订阅主题
* 一种资源去换取另外一种资源
*/
consumer.subscribe("TopicFilter", MessageSelector.bySql("a between 0 and 3 and b = 'yangguo'"));
/**
* 注册监听器,监听主题消息
*/
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs){
try {
System.out.println("consumeThread=" + Thread.currentThread().getName()
+ ", queueId=" + msg.getQueueId() + ", content:"
+ new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Filter Consumer Started.%n");
}
8、延时消息
定时消息是指消息发到 Broker 后,不能立刻被 Consumer 消费,要到特定的时间点 或者等待特定的时间后才能被消费。
使用场景:如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的 状态,如果还是未付款就取消订单释放库存。
延时机制
延迟级别(18种)
* 当前支持的延迟时间
* 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
* 分别对应级别
* 1 2 3....................
RocketMQ的配置类
org.apache.rocketmq.store.config.MessageStoreConfig#messageDelayLevel
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
设置消息时延
Message message = new Message;
message.setDelayTimeLevel(3);
现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h 分别对应着等级1到18 消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级 和重试次数有关。
生产者: message.setDelayTimeLevel(6);
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("ExampleConsumer");
//;192.168.241.199:9876
producer.setNamesrvAddr("192.168.241.198:9876;192.168.241.199:9876");
producer.start();
int totalMessagesToSend = 3;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
//延时消费
message.setDelayTimeLevel(6);
// Send the message
producer.send(message);
}
System.out.printf("message send is completed .%n");
producer.shutdown();
}
消费者:
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
//;192.168.241.199:9876
consumer.setNamesrvAddr("192.168.241.198:9876");
consumer.subscribe("TestTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
for (MessageExt message : messages) {
// Print approximate delay time period
System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
+ "message content is :" + new String(message.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
//System.out.printf("Consumer Started.%n");
}
延时消息的实现原理
所有的延迟消息由 Producer 发出之后,都会存放到同一个 Topic(SCHEDULE_TOPIC_XXXX)下,不同的延迟级别会对应不同的队列序号。当延迟时间到之后,由定时线程读取转换为普通的消息存的真实指定的 Topic 下,此时对于 Consumer 端此消息才是可见的,从而被 Consumer 消费。
可以看到,总共有6个步骤:
- 修改消息Topic名称和队列信息
- 转发消息到延迟主题的CosumeQueue中
- 延迟服务消费SCHEDULE_TOPIC_XXXX消息
- 将信息重新存储到CommitLog中
- 将消息投递到目标Topic中
- 消费者消费目标topic中的数据
9、事务消息
RabbitMQ、Kafka都不支持事务消息,RocketMQ 可以支持事务消息的最大特性是 Producer 和 Broker 是双向通信的;
事务消息详解:
概念
- 事务消息:消息队列 MQ 提供类似 X/Open XA 的分布式事务功能,通过消息队列 MQ 事务消息能达到分布式事务的最终一致。
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了消息队列 MQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列 MQ 服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。
场景
通过购物车进行下单的流程中,用户入口在购物车系统,交易下单入口在交易系统,两个系统之间的数据需要保持最终一致,这时可以通过事务消息进行处理。交易系统下单之后,发送一条交易下单的消息到消息队列 MQ,购物车系统订阅消息队列 MQ 的交易下单消息,做相应的业务处理,更新购物车数据。
消息状态
org.apache.rocketmq.client.producer.LocalTransactionState
提交事务,它允许消费者消费此消息。
LocalTransactionState.CommitTransaction
回滚事务,它代表该消息将被删除,不允许被消费
LocalTransactionState.RollbackTransaction
中间状态,它代表需要检查消息队列来确定状态
LocalTransactionState.Unknown
交互流程
事务消息发送步骤如下:
- 发送方将半事务消息发送至消息队列 MQ 服务端。
- 消息队列 MQ 服务端将消息持久化成功之后,向发送方返回 Ack 确认消息 已经发送成功,此时消息为半事务消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务 端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。
事务消息回查步骤如下:
- 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端, 经过固定时间后服务端将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行操作。
事务消息限制
- 事务消息不支持延时消息和批量消息。
- 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限 制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限 制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将 丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
- 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度 之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
- 事务性消息可能不止一次被检查或消费。
- 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性 得到保证,建议使用同步的双重写入机制。
- 事务消息的3生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不 同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
事务消息使用案例
(1)定义消息监听器
消息监听器主要是实现TransactionListener接口,然后需要重写下面两个方法:
- executeLocalTransaction:执行本地事务;
- checkLocalTransaction:回查本地事务状态,根据这次回查的结果来决定此次事务是提交还是回滚;
@RocketMQTransactionListener(txProducerGroup = "myTxProducerGroup")
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<String, Integer>();
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
System.out.printf("#### executeLocalTransaction is executed, msgTransactionId=%s %n",
transId);
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(transId, status);
if (status == 0) {
// 事务提交
System.out.printf(" # COMMIT # Simulating msg %s related local transaction exec succeeded! ### %n", msg.getPayload());
return RocketMQLocalTransactionState.COMMIT;
}
if (status == 1) {
// 本地事务回滚
System.out.printf(" # ROLLBACK # Simulating %s related local transaction exec failed! %n", msg.getPayload());
return RocketMQLocalTransactionState.ROLLBACK;
}
// 事务状态不确定,待Broker发起 ASK 回查本地事务状态
System.out.printf(" # UNKNOW # Simulating %s related local transaction exec UNKNOWN! \n");
return RocketMQLocalTransactionState.UNKNOWN;
}
/**
* 在{@link TransactionListenerImpl#executeLocalTransaction(org.springframework.messaging.Message, java.lang.Object)}
* 中执行本地事务时可能失败,或者异步提交,导致事务状态暂时不能确定,broker在一定时间后
* 将会发起重试,broker会向producer-group发起ask回查,
* 这里producer->相当于server端,broker相当于client端,所以由此可以看出broker&producer-group是
* 双向通信的。
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
RocketMQLocalTransactionState retState = RocketMQLocalTransactionState.COMMIT;
Integer status = localTrans.get(transId);
if (null != status) {
switch (status) {
case 0:
retState = RocketMQLocalTransactionState.UNKNOWN;
break;
case 1:
retState = RocketMQLocalTransactionState.COMMIT;
break;
case 2:
retState = RocketMQLocalTransactionState.ROLLBACK;
break;
}
}
System.out.printf("------ !!! checkLocalTransaction is executed once," +
" msgTransactionId=%s, TransactionState=%s status=%s %n",
transId, retState, status);
return retState;
}
}
(2)定义消息生产者
private static final String TX_PGROUP_NAME = "myTxProducerGroup";
@Resource
private RocketMQTemplate rocketMQTemplate;
@Value("${tl.rocketmq.transTopic}")
private String springTransTopic;
@Value("${tl.rocketmq.topic}")
private String springTopic;
@Value("${tl.rocketmq.orderTopic}")
private String orderPaymentTopic;
@Value("${tl.rocketmq.msgExtTopic}")
private String msgExtTopic;
/**
* 发送事务消息
* @throws MessagingException
*/
private void testTransaction() throws MessagingException {
String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message msg = MessageBuilder.withPayload("Hello RocketMQ " + i).
setHeader(RocketMQHeaders.KEYS, "KEY_" + i).build();
/**
* TX_PGROUP_NAME 必须同 {@link TransactionListenerImpl} 类的注解 txProducerGroup
* @RocketMQTransactionListener(txProducerGroup = "myTxProducerGroup")
*/
SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(TX_PGROUP_NAME,
springTransTopic + ":" + tags[i % tags.length], msg, null);
System.out.printf("------ send Transactional msg body = %s , sendResult=%s %n",
msg.getPayload(), sendResult.getSendStatus());
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}