RocketMQ 的详解
https://www.cnblogs.com/happydreamzjl/articles/11951245.html
https://cdn.modb.pro/db/72488
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 端
1 2 3 4 5 6 | 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 实例平均分摊消息。
4、消费点位
- CONSUME_FROM_LAST_OFFSET 将会忽略历史消息,并消费之后生成的任何消息。
- CONSUME_FROM_FIRST_OFFSET 将会消费每个存在于 Broker 中的信息。
- CONSUME_FROM_TIMESTAMP 消费在指定时间戳后产生的消息。
5、消息重复幂等
6、批量消息
7、过滤消息
大多数情况下,可以通过TAG来选择您想要的消息。
1 2 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer( "CID_EXAMPLE" ); consumer.subscribe( "TOPIC" , "TAGA || TAGB || TAGC" ); |
使用Filter功能,需要在启动配置文件当中配置以下选项:
1 | enablePropertyFilter= true |
消费者将接收包含 TAGA 或 TAGB 或 TAGC 的消息。但是限制是一个消息只能有一个标 签,这对于复杂的场景可能不起作用。在这种情况下,可以使用 SQL 表达式筛选消息。SQL 特性可以通过发送消息时的属性来进行计算。在 RocketMQ 定义的语法下,可以实现一些 简单的逻辑。
生产者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 (); } |
消费者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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后去检查这个订单的 状态,如果还是未付款就取消订单释放库存。
1 2 3 4 5 6 7 8 9 10 11 | 延迟级别(18种) * 当前支持的延迟时间 * 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h * 分别对应级别 * 1 2 3.................... <br><br>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);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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(); } |
消费者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 是双向通信的;
事务消息详解:https://blog.csdn.net/Weixiaohuai/article/details/123733518
概念
- 事务消息:消息队列 MQ 提供类似 X/Open XA 的分布式事务功能,通过消息队列 MQ 事务消息能达到分布式事务的最终一致。
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了消息队列 MQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列 MQ 服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。
场景
消息状态
1 2 3 4 5 6 7 | org.apache.rocketmq.client.producer.LocalTransactionState<br> 提交事务,它允许消费者消费此消息。 LocalTransactionState.CommitTransaction<br> 回滚事务,它代表该消息将被删除,不允许被消费 LocalTransactionState.RollbackTransaction<br> 中间状态,它代表需要检查消息队列来确定状态 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 查询到消费者。
事务消息详解:https://blog.csdn.net/Weixiaohuai/article/details/123733518
事务消息使用案例
(1)定义消息监听器
消息监听器主要是实现TransactionListener接口,然后需要重写下面两个方法:
- executeLocalTransaction:执行本地事务;
- checkLocalTransaction:回查本地事务状态,根据这次回查的结果来决定此次事务是提交还是回滚;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | @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)定义消息生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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(); } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!