RocketMQ消息机制--重点笔记
MQ作用
异步
作⽤:异步能提⾼系统的响应速度、吞吐量
解耦
作⽤:
1、服务之间进⾏解耦,才可以减少服务之间的影响。提⾼系统整体的稳定性以及可扩展性。
2、另外,解耦后可以实现数据分发。⽣产者发送⼀个消息后,可以由⼀个或者多个消费者进⾏消费,并且消费者的增加或者减少对⽣产者没有影响。
削峰
作⽤:以稳定的系统资源应对突发的流量冲击。
=================================================================================================================
Dledger集群
在Dledger集群中,就不再单独指定各个broker的服务,⽽是由这些broker服务⾃⾏进⾏选举,产⽣⼀个
Leader⻆⾊的服务,响应客户端的各种请求。⽽其他的broker服务,就作为Follower⻆⾊,负责对Leader上的
数据进⾏备份。当然,Follower所要负责的事情,⽐主从架构中的SLAVE⻆⾊会要复杂⼀点,因为这种节点选
举是在后端不断进⾏的,他们需要随时做好升级成Leader的准备。

Dledger集群的选举是通过Raft协议进⾏的,Raft协议是⼀种多数同意机制。也就是每次选举需要有集群中超
过半数的节点确认,才能形成整个集群的共同决定。同时,这也意味着在Dledger集群中,只要有超过半数的
节点能够正常⼯作,那么整个集群就能正常⼯作。因此,在部署Dledger集群时,通常都是部署奇数台服务,
这样可以让集群的容错性达到最⼤。
脑裂问题
脑裂问题是指在分布式系统中,由于⽹络分区或其他原因导致集群被分割成两个或多个⼦集群,各⾃独⽴运⾏且⽆法感知到其他⼦集群的存
在。这可能导致数据不⼀致和错误决策。
解决:⽽Raft协议对于脑裂问题,会采⽤随机休眠的机制,彻底解决脑裂问题。RocketMQ是Raft协议的⼀个重要的成功示例。
1. 选举机制:Raft协议的基础是选举出⼀个领导者(Leader),其他节点(Follower)都从领导者获取 数据。选举过程要求候选⼈必须获得集群中⼤多数节点的⽀持才能成为领导者。这确保了集群中只能 有⼀个领导者,从⽽避免了脑裂问题。 2. 任期(Term):Raft协议为每个选举周期设置了⼀个递增的任期编号。任期编号⽤于标识当前的领 导者,确保旧的领导者不会再次被选为领导者。如果⼀个节点发现⾃⼰的任期⼩于其他节点,那么它 会停⽌当前的⼯作并更新⾃⼰的任期。 3. ⼼跳机制:领导者会定期向其他节点发送⼼跳消息,以保持与Follower节点的连接。当⼀个节点⻓时 间未收到领导者的⼼跳时,它会认为当前领导者失效,并启动新⼀轮选举。这确保了当领导者出现故 障时,系统能够快速地选出新的领导者。 4. ⽇志复制:领导者负责将数据更新(⽇志条⽬)复制到其他节点。Follower节点只有在收到领导者的 ⽇志条⽬并将其写⼊本地⽇志后,才会响应客户端的请求。这确保了在发⽣脑裂情况下,不会出现多 个节点试图同时修改同⼀份数据的情况。
=================================================================================================================
RocketMQ模型

⽣产者和消费者都可以指定⼀个Topic发送消息或者拉取消息。
⽽Topic是⼀个逻辑概念。Topic中的消息会分布在后⾯多个MessageQueue当中。
这些MessageQueue会分布到⼀个或者多个broker中。
在RocketMQ的这个消息模型当中,最为核⼼的就是Topic。对于客户端,Topic代表了⼀类有相同业务规则的消息。对于Broker,Topic则代表了系统中⼀系列存储消息的资源。
⽽对于业务来说,最为重要的就是消息Message了。⽣产者发送到某⼀个Topic下的消息,最终会保存在Topic下的某⼀个MessageQueue中。
⽽消费者来消费消息时,RocketMQ会在Broker端给每个消费者组记录⼀个消息的消费位点Offset。通过Offset控制每个消费者组的消息处理进度。
这样,每⼀条消息,在⼀个消费者组当中只被处理⼀次。
=================================================================================================================
发送方式
第⼀种称为单向发送
producer.sendOneway(message);
单向发送⽅式下,消息⽣产者只管往Broker发送消息,⽽全然不关⼼Broker端有没有成功接收到消息。
单向发送有⼀个好处,就是发送消息的效率更⾼。适⽤于⼀些追求消息发送效率,⽽允许消息丢失的业务场景。⽐如⽇志。
第⼆种称为同步发送
SendResult sendResult = producer.send(msg);
同步发送⽅式下,消息⽣产者在往Broker端发送消息后,会阻塞当前线程,等待Broker端的相应结果。
在SendResult中有⼀个SendStatus枚举类型属性。
public enum SendStatus { SEND_OK, FLUSH_DISK_TIMEOUT, FLUSH_SLAVE_TIMEOUT, SLAVE_NOT_AVAILABLE, }
在这⼏种枚举值中,SEND_OK表示消息已经成功发送到Broker上。⾄于其他⼏种枚举值,都是表示消息在Broker端处理失败了。使⽤同步发送
的机制,我们就可以在消息⽣产者发送完消息后,对发送失败的消息进⾏补救。例如重新发送。
但是此时要注意,如果Broker端返回的SendStatus不是SEND_OK,也并不表示消息就⼀定不会推送给下游的消费者。仅仅只是表示Broker端并
没有完全正确的处理这些消息。因此,如果要重新发送消息,最好要带上唯⼀的系统标识,这样在消费者端,才能⾃⾏做幂等判断。也就是⽤具
有业务含义的OrderID这样的字段来判断消息有没有被重复处理。
第三种称为异步发送
异步发送机制下,⽣产者在向Broker发送消息时,会同时注册⼀个回调函数。接下来⽣产者并不等待Broker的响应。当Broker端有响应数据过
来时,⾃动触发回调函数进⾏对应的处理。
producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { countDownLatch.countDown(); System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } @Override public void onException(Throwable e) { countDownLatch.countDown(); System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } });
在SendCallback接⼝中有两个⽅法,onSuccess和onException。
onSuccess
当Broker端返回消息处理成功的响应信息SendResult时,就会调⽤onSuccess⽅法。
onException
当Broker端处理消息超时或者失败时,就会调⽤onExcetion⽅法,⽣产者就可以在onException⽅法中进⾏补救措施。
如果Broker端返回响应信息太慢,超过了超时时间,也会触发onException⽅法。
注:在SendCallback的对应⽅法被触发之前,⽣产者不能调⽤shutdown()⽅法。如果消息处理完之前,⽣产者线程就关闭了,⽣产者的
SendCallback对应⽅法就不会触发。这是因为使⽤异步发送机制后,⽣产者虽然不⽤阻塞下来等待Broker端响应,但是SendCallback还是需要
附属于⽣产者的主线程才能执⾏。如果Broker端还没有返回SendResult,⽽⽣产者主线程已经停⽌了,那么SendCallback的执⾏线程也就会随
主线程⼀起停⽌,对应的⽅法⾃然也就⽆法执⾏了。
=================================================================================================================
过滤消息
简单过滤
⽣产者端需要在发送消息时,增加Tag属性。
String[] tags = new String[] {"TagA", "TagB", "TagC"}; for (int i = 0; i < 15; i++) { Message msg = new Message("TagFilterTest", tags[i % tags.length], "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); }
消费者端就可以通过这个Tag属性订阅⾃⼰感兴趣的内容。核⼼代码
consumer.subscribe("TagFilterTest", "TagA");
SQL过滤
如果要进⾏更复杂的消息过滤,⽐如数字⽐较,模糊匹配等,就需要使⽤SQL过滤⽅式。
式可以通过Tag属性以及⽤户⾃定义的属性⼀起,以标准SQL的⽅式进⾏消息过滤。
⽣产者端在发送消息时,出了Tag属性外,还可以增加⾃定义属性。核⼼代码:
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) ); msg.putUserProperty("a", String.valueOf(i)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); }
消费者端在进⾏过滤时,可以指定⼀个标准的SQL语句,定制复杂的过滤规则。核⼼代码
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)"));
注意:如果需要使⽤⾃定义参数进⾏过滤,需要在Broker端,将参数enablePropertyFilter设置成true。这个参数默认是false。
注意点: 1、使⽤Tag过滤时,如果希望匹配多个Tag,可以使⽤两个竖线(||)连接多个Tag值。另外,也可以使⽤星号(*)匹配所有。 2、使⽤SQL顾虑时,SQL语句是按照SQL92标准来执⾏的。SQL语句中⽀持⼀些常⻅的基本操作: 数值⽐较,⽐如:>,>=,<,<=,BETWEEN,=; 字符⽐较,⽐如:=,<>,IN; IS NULL 或者 IS NOT NULL; 逻辑符号 AND,OR,NOT;
=================================================================================================================
一 顺序消息
应⽤场景:
每⼀个订单有从下单、锁库存、⽀付、下物流等⼏个业务步骤。每个业务步骤都由⼀个消息⽣产者通知给下游服务。如何保证对每个订单的业务处理顺序不乱?
示例代码:
⽣产者核⼼代码: for (int i = 0; i < 10; i++) { int orderId = i; for(int j = 0 ; j <= 5 ; j ++){ 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) { Integer id = (Integer) arg; int index = id % mqs.size(); return mqs.get(index); } }, orderId); System.out.printf("%s%n", sendResult); } }
通过MessageSelector,将orderId相同的消息,都转发到同⼀个MessageQueue中。
消费者核⼼代码: 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; } });
注⼊⼀个MessageListenerOrderly实现
实现思路:
RocketMQ实现消息顺序消费,是需要⽣产者和消费者配合才能实现的。

1、⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,通过MessageQueue的FIFO特性保证这⼀批消息的顺序。
如果不指定MessageSelector对象,那么⽣产者会采⽤轮询的⽅式将多条消息依次发送到不同的MessageQueue上。
2、消费者需要实现MessageListenerOrderly接⼝,实际上在服务端,处理MessageListenerOrderly时,会给⼀个MessageQueue加锁,拿到
MessageQueue上所有的消息,然后再去读取下⼀个MessageQueue的消息。
注意点:
1、理解局部有序与全局有序。⼤部分业务场景下,我们需要的其实是局部有序。如果要保持全局有序,那就只保留⼀个MessageQueue。性能
显然⾮常低。
2、⽣产者端尽可能将有序消息打散到不同的MessageQueue上,避免过于集中导致数据热点竞争。
3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,如果消费者⼀直处
理失败,超过最⼤重试次数,那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。
4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT
作为替代。
二 延迟消息
应⽤场景:
延迟消息发送是指消息发送到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,如果太⼤就需要⾃⾏分割。
四 事务消息
具体见文档。

1. ⽣产者将消息发送⾄Apache RocketMQ服务端。
2. Apache RocketMQ服务端将消息持久化成功之后,向⽣产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
3. ⽣产者开始执⾏本地事务逻辑。
4. ⽣产者根据本地事务执⾏结果向服务端提交⼆次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
⼆次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
⼆次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
5. 在断⽹或者是⽣产者应⽤重启的特殊情况下,若服务端未收到发送者提交的⼆次确认结果,或服务端收到的⼆次确认结果为Unknown未知
状态,经过固定时间后,服务端将对消息⽣产者即⽣产者集群中任⼀⽣产者实例发起消息回查。
6. ⽣产者收到消息回查后,需要检查对应消息的本地事务执⾏的最终结果。
7. ⽣产者根据检查到的本地事务的最终状态再次提交⼆次确认,服务端仍按照步骤4对半事务消息进⾏处理。
注意点: 1、半消息是对消费者不可⻅的⼀种消息。实际上,RocketMQ的做法是将消息转到了⼀个系统Topic,RMQ_SYS_TRANS_HALF_TOPIC。 2、事务消息中,本地事务回查次数通过参数transactionCheckMax设定,默认15次。本地事务回查的间隔通过参数transactionCheckInterval 设定,默认60秒。超过回查次数后,消息将会被丢弃。 3、其实,了解了事务消息的机制后,在具体执⾏时,可以对事务流程进⾏适当的调整。


TransactionMQProducer 事务消息发送者
sendMessageInTransaction 发生事务消息

TransactionListener 事务消息监听
方法:excuteLocalTrancation执行本地事务 checkLocalTransaction事务状态回查
MQ的源码详解(消息的提交和消费)
https://javap.blog.csdn.net/article/details/120229506

浙公网安备 33010602011771号