消息队列全文
集群
集群模式
- 单master模式,单机
- 多master模式
多个节点维护一个topic
- 多master多slave模式(同步)
- 主从同步成功后,才给客户端返回
- 主从通过
brokerId
区分,0为主,1为从
- 多master多slave模式(异步)
- 消息写入master节点即可返回,异步线程负责主从同步
mster
的配置标记为异步主节点brokerRole=ASYNC_MASTER
刷盘
- 同步刷盘
- 消息必须落盘,才会返回客户端ack
- 同步刷盘+同步复制,可靠性是最高的,但是性能不高
- 异步刷盘
- 启动异步线程,进行落盘
- 异步刷盘+同步复制,性能能接受,且可靠性较高
- 主从复制速度 > 刷盘
存储设计
- Message,消息对象
- messageId
- messageKey
- equals
- hashCode
- Topic,主题
- tags
- subTopic
- group 消费组/生产组
- Queue,队列
- Offset,偏移量
对应关系
- 一个主题有多少消息
- 一个主题有多个队列
- 一个主题可以来源于多个组,也可以被多个组消费
- 一个组也可以有多个主题,也可以消费多个主题
- 一个主题有多个队列,也对应着多个偏移量
消费并发度
- 一个主题划分出多个Queue,不同的Queue部署到不同的机器上
消息持久化
存储文件
- commitLog,消息存储目录
- config,运行配置
- topic.json,topic配置
- subscriptionGroup.json,消息消费组配置信息
- delayOffset.json 延时消息队列拉取进度
- consumerOffset.json 集群消费模式消息消费进度
- consumerFilter.json 主题消息过滤消息
- consumequeue,消息消费队列存储目录
- index,消息索引文件存储目录
- abort,如果存在该文件,说明Broker非正常关闭
- checkpoint,文件检查点
- CommitLog文件最后一次刷盘时间戳
- consumerqueue最后一次刷盘时间戳
- index索引文件最后一次刷盘时间戳
存储方式
- 主要通过commitLog和consumerqueue实现持久化
- commitLog存储各个主题消息的元数据,消息内容、队列、tags、key等
- 每一个commitLog为1个G
- consumequeue,消息的逻辑队列,类似数据库索引文件,指向物理存储地址
- 每个Topic的队列都有一个对应的ConsumeQueue的文件
- 存储消息条目,20个字节,不存储消息本地,存储消息在commitLog的偏移量,长度,tags标签的hash
- 作为commitLog的索引文件,消息到达commitLog后,由专门的线程转发任务
- 保证磁盘顺序写,可以根据consumequeue索引,找到下一个写入commitLog的位置
- consumequeue固定二十个字节,通过pageCache进行操作,再找到commitLog中消息的位置,速度很快
- consumequeue丢失,也可以通过commitLog恢复
- IndexFile,索引文件,加速消息查询的速度
- 用MessageKey生成Hash索引,记录CommitLog中的offset
过期文件删除
- RocketMQ通过内存映射来操作CommitLog和ConsumeQueue文件的,需要及时删除,避免内存和磁盘浪费
- 非当前写的文件在一定时间间隔内没有被再次更新,则认为是过期文件,可以被删除,默认过期时间为42小时
如果最后一次更新到现在,超过42个小时,则可以删除
- 定时任务触发删除,每个10s执行一次
- 如果待删除的文件,被线程引用,会进行删除拒绝,并记录当前时间,当时间间隔大于
destroyMapedFileIntervalForcibly
,会将引用减少1000,直到为0,可以被删除
RocketMQ中零拷贝
- 零拷贝,不需要通过CPU进行数据拷贝,减少操作系统进行IO操作的次数,和用户态与内核态的切换
- mmap内存映射,将内核缓冲区映射到用户缓冲区,但是写入时也是需要CPU拷贝,在内核中进行拷贝
分布式事务
- 独立模块的合并操作需要保证事务的原子性
RocketMQ的处理
- 发送半事务消息,只保存在CommitLog,不构建索引,消费者无法消费
- 事务回查机制,当事务提交成功后,将半事务转换为全事务,消费者可以消费
- 方案
- A发送半事务消息,并执行业务
- A提交事务,通过回查机制,把半事务消息转换为全事务消息
- B消费消息
- 半事务消息队列
生产者
- 当生产者组中的一个生产者挂掉了,RocketMQ可以通过其他的生产者的回查接口进行回查和提交全事务
// 监听器,进行事务回查
TranscationListenerImpl transcationListener = new TranscationListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("TranscationProducers");
// 设置nameserver的地址
producer.setNamesrvAddr("127.0.0.1:9876");
// 创建回查线程池
ExecutorService executor = new ThreadPoolExecutor(2, 5, 100,
TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transcation-msg-check-thread");
return thread;
}
}, new ThreadPoolExecutor.CallerRunsPolicy());
producer.setExecutorService(executor);
producer.setTransactionListener(transcationListener);
//启动producer
producer.start();
// 半事务消息发送
Message msg = new Message("TranscationTopic", null, ("A -> B 转 100$").getBytes(StandardCharsets.UTF_8));
TransactionSendResult result = producer.sendMessageInTransaction(msg, null);
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());
System.out.println(time + " - " + result);
// 睡100秒
Thread.sleep(100000);
producer.shutdown();
System.out.println("producer end...");
TranscationListenerImpl
回查实现类
class TranscationListenerImpl implements TransactionListener {
@Override
// 本地事务处理
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());
System.out.println("executeLocalTransaction: " + time + " - " + message.getTransactionId() + " - " + new String(message.getBody()));
// 本地事务执行状态
// 如果这个地方不提交 可以返回 LocalTransactionState.UNKNOW
// 那么就会让RocketMQ 60秒回查一次
return LocalTransactionState.UNKNOW;
}
@Override
// RocketMQ 每个60秒调一次 检查一次
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());
// 事务状态检查。。。
System.out.println("checkLocalTransaction: " + time + " - " + messageExt.getTransactionId() + " - " + new String(messageExt.getBody()));
return LocalTransactionState.COMMIT_MESSAGE;
}
}
消费者
- 生产者提交成功后,才会接收到消息
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TranscationConsumers");
// 指定注册中心
consumer.setNamesrvAddr("127.0.0.1:9876");
// 订阅Topic 指定tag
consumer.subscribe("TranscationTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list) {
System.out.println(msg.getTransactionId() + " - " + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();