一、消息发送规则
生产者会选择一个message queue进行发送,类似于partition的分区器,具体代码如下:
public MessageQueue selectOneMessageQueue(TopicPublishInfo tpInfo, String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
int i = 0;
while(true) {
int writeQueueNums;
MessageQueue mq;
if (i >= tpInfo.getMessageQueueList().size()) {
String notBestBroker = (String)this.latencyFaultTolerance.pickOneAtLeast();
writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
}
this.latencyFaultTolerance.remove(notBestBroker);
break;
}
writeQueueNums = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (writeQueueNums < 0) {
writeQueueNums = 0;
}
mq = (MessageQueue)tpInfo.getMessageQueueList().get(writeQueueNums);
if (this.latencyFaultTolerance.isAvailable(mq.getBrokerName()) && (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))) {
return mq;
}
++i;
}
} catch (Exception var7) {
log.error("Error occurred when selecting message queue", var7);
}
return tpInfo.selectOneMessageQueue();
} else {
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
}
关键在于下面这段代码,自增随机数对messageQueue长度取模
writeQueueNums = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (writeQueueNums < 0) {
writeQueueNums = 0;
}
还有一些实现MessageQueueSelector接口的算法
- SelectMessageQueueByHash:哈希取值(默认)
- SelectMessageQueueByRandom:随机选取
- SelectMessageQueueByMachineRoom:返回空,没有实现
若是都不满意,也可以在send时自定义算法。
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
return null;
}
}, null);
二、消息有序
消息的有序性在前文中提到过,通常指的是局部有序而非全局有序,因此实现原理如下:
- 生产者发送消息时,到达Broker应该是有序的,所以对于生产者而言,不能使用多线程异步发送,只能使用单线程顺序发送。
- 写入Broker时,应该是顺序写入的,也就是相同主题的消息应该集中写入,选择同一个Message Queue,而不是分散写入。
- 消费者消费的时候只能有一个线程,否则由于消费速率的不同,有可能出现记录到数据库的时候无序。
1、生产者
在producer的send方法中,有一个参数,CommunicationMode.SYNC,其实它包含多组参数
public enum CommunicationMode {
//同步
SYNC,
//异步
ASYNC,
//单向
ONEWAY;
private CommunicationMode() {
}
}
在这里,它使用了同步,即必须要同步发送,等待响应。
private SendResult sendMessageSync(String addr, String brokerName, Message msg, long timeoutMillis, RemotingCommand request) throws RemotingException, MQBrokerException, InterruptedException {
RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
assert response != null;
return this.processSendResponse(brokerName, msg, response);
}
以上是生产者的实现。
2、Broker写入
在之前提到的方法中,sendOneWayOrderly是单向顺序发送,在其内部方法中,有一个关键的参数hashKey,hashKey可以保证对它取模时,最终路由到同一个Message Queue。
public void sendOneWayOrderly(String destination, Message<?> message, String hashKey) {
if (!Objects.isNull(message) && !Objects.isNull(message.getPayload())) {
try {
org.apache.rocketmq.common.message.Message rocketMsg = this.createRocketMqMessage(destination, message);
this.producer.sendOneway(rocketMsg, this.messageQueueSelector, hashKey);
} catch (Exception var5) {
log.error("sendOneWayOrderly failed. destination:{}, message:{}", destination, message);
throw new MessagingException(var5.getMessage(), var5);
}
} else {
log.error("sendOneWayOrderly failed. destination:{}, message is null ", destination);
throw new IllegalArgumentException("`message` and `message.payload` cannot be null");
}
}
3、消费者消费
可在消费者设置中选择顺序消费,这样就可以加锁进行单线程消费保证顺序性。
三、消息事务
和kafka类似,这里的事务也类似分布式事务。
实现思路
- 生产者先发送一条消息到Broker,把这个消息状态标记为未确认。
- Broker通知生产者消息接收成功,现在可以执行本地事务
- 生产者执行本地事务
- 确认或放弃
首先,Producer将发送半消息给Broker,Broker将回复消息接收成功,然后Producer执行本地事务,根据事务的成功或失败决定返回结果是提交还是回滚,Broker将根据此次事务结果决定这个消息是投递还是丢弃。如果一直没能得到这次的事务究竟成功还是失败,Broker将主动向Producer发起询问,Producer检查事务情况,将结果告知Broker。
示例图如下:
生产者将启动事务监听器,这个事务监听器可以自己重定义其中的方法。
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("transaction_test_group_name");
producer.setNamesrvAddr("192.168.44.1:9876;192.168.44.2: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.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 12; i++) {
try {
Message msg =
new Message("transaction-test-topic", 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();
}
}
for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
}
重写方法executeLocalTransaction,这个方法将执行本地事务,根据返回结果不同对应事务的成功,失败或未知。
重写方法checkLocalTransaction,这个方法将执行事务回查,在executeLocalTransaction方法超时响应或回复未知时,将主动提供询问事务执行结果,这个方法会执行15次,第一次会回查6秒,后续回查时间间隔1分钟。
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
/**
* 执行本地事务
* @param msg
* @param arg
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
System.out.println("收到ACK,假装执行本地事务,返回结果:UNKNOW……");
return LocalTransactionState.UNKNOW;
}
/**
* 事务回查
* @param msg
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("有人没有报告事务执行状态,主动检查!");
Integer status = localTrans.get(msg.getTransactionId());
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
在springboot中有类似的方法,sendMessageInTransaction()
和kafka不同的是,kafka的事务是原子性的,一个失败全部失败,需要协调者协调全部事务预提交成功,RocketMQ则采用半消息,可以同时控制消息事务和数据库事务,确保事务一致性。
四、延时投递
setDelayTimeLevel方法实现延时投递,在开源版本中,延时时间分为18个等级,但实际上能用的只有后面16个。
Message msg = new Message("delay-topic",
"TagA",
"OrderID188",
content.getBytes(RemotingHelper.DEFAULT_CHARSET));
msg.setDelayTimeLevel(3); // 10秒钟
SendResult sendResult = producer.send(msg);
它的原理可以阐述为,producer先把延时消息发送给一个临时Topic,之后由定时任务投递给目标Topic,再由消费者消费。
五、存储方式
1、集中存储
和kafka不同的是,RocketMQ没有partition的概念,它会将所有的消息集中存储到一个文件中。
但是这样做会给消息的查找带来很大的困难,因此RocketMQ使用ConsumerQueue来对消息和消费者的关系进行管理。ConsumerQueue会按照topic来划分目录,和写队列的个数相等,在写入消息时,不仅仅需要在文件中写入,也要在ConsumerQueue中写入。consumer消费消息时,会通过ConsumerQueue读取对应的消息。
各文件作用及大小
- index:索引,400M空间,存储约2000万条数据
- consumequeue:消费者队列,600B空间,30万条数据
- commitlog:消息数据,1G空间。
为了提升读取速度,所以RocketMQ使用了几种I/O技术。
2、Page CaChe
Page CaChe,页缓存。在操作系统中,内存从磁盘空间中读取数据的时候,会按照一页来读取,一般来说是4KB大小,为了提升磁盘的读取效率,操作系统会将这一页数据缓存起来,当有读取请求时,会先去缓存中进行查找,如果没有,才会去磁盘中再读取。除了正常读取,还会有预读取,也就是将读取页相邻的页面也加载到页缓存中,为将来可能的读取做准备。
3、Memory Map
Memory Map,内存映射,类似于Kafka的零拷贝提升读取速度,但有相当差距,它在用户区和内存区建立了一个共享缓冲区,避免再使用read之类的系统函数读取数据,进一步提高了读取效率。
六、文件清理
在RocketMQ中,消息的默认保存时间为3天,比Kafka的7天要短,同时会在每天的凌晨4点删除消息。如果磁盘已经快要写满了(超过75%),RocketMQ将立即清除数据,不再等待固定时间,直到低于75%,如果当时占用空间已经超过90%,将拒绝消息写入。
七、消费者负载
在广播模式无所谓负载,因为所有消费者都将得到一样的消息,只有集群模式才有消费者负载。
在集群模式中,类似kafka,RocketMQ也有类似的分配策略,使用方法consumer.setAllocateMessageQueueStrategy();即可完成策略选择
- AllocateMessageQueueAveragely:连续分配(默认策略)
- AllocateMessageQueueAveragelyByCirCle:轮流分配
- AllocateMessageQueueByConfig:通过配置
- AllocateMessageQueueConsistentHash:一致性哈希
- AllocateMessageQueueByMachineRoom:指定一个broker的topic中的queue消费
- AllocateMachineRoomNearby:按broker的机房就近分配
八、重试与死信
当生产者发送消息的返回值为RECONSUME_LATER时,消息将会被重发,重发机制与上文提到过的延时发送有关,有十六个可用等级。发送失败的消息会先进入重试topic,进行延时重试投递,如果最后还是没有投递成功,就会进入死信队列,可以专门监听死信队列的topic,进行进一步的处理。
九、高可用架构
1、集群优点
RocketMQ二主二从集群架构,master节点和可读取的slave节点都可以用作数据读取,确保某一个节点挂掉依旧可以保持可用,生产者和消费者从NameServer中获取全部节点地址。
- 数据备份
- 高可用性
- 提高性能
- 消费实时
2、主从通讯
在RocketMQ中,不必配置所有节点的地址,主从节点互相关联通讯依靠的是以下条件
- 集群的名字相同,brokerClusterName需要相同。
- 连接到相同的NameServer
- 在配置文件中,brokerId = 0代表master,brokerId = 1代表slave
3、主从同步
主从同步分为两种,同步和异步
- ASYNC_MASTER:主从异步复制,当master节点同步数据时就发送成功,效率更高但不可靠
- SYNC_MASTER:主从同步复制(推荐使用),当master和slave节点都同步数据后发送成功,效率低但可靠
4、刷盘类型
消息存储时,会将数据存入磁盘,存入磁盘的方式就叫刷盘类型
- ASYNC_FLUSH:异步刷盘(默认),将消息放进缓存后通知成功,消息可能会丢失
- SYNC_FLUSH:同步刷盘,将消息存入磁盘后通知成功,消息不会丢失。
5、同步流程
- slave连接到master,然后每隔5秒向master发送commitLog文件最大偏移量拉取还未同步的消息
- master节点收到slave发过来的偏移量进行解析,并返回查找出未同步的消息给slave。
- slave收到master节点的消息后,将这批消息写入commitLog文件中,然后更新commitLog拉取偏移量,接着向master拉取未同步的消息。
6、故障转移
在早期版本,RocketMQ并没有主从选举功能,假如某一个节点挂掉之后需要手动重启。在4.5.0之后,出现了一个新功能,故障转移(Dledger),这才解决了主从选举问题。它的本质依旧是主从选举协议,开启Dledger后,宕机节点的数据将会交给Dledger来进行管理,然后选举出新的master节点。这个功能默认是关闭的,需要至少三个节点才能开启这个Dledger功能。