Java-第十九部分-消息队列-RocketMQ集群、存储、事务

321 阅读5分钟

消息队列全文

集群

集群模式

  • 单master模式,单机
  • 多master模式

多个节点维护一个topic

  • 多master多slave模式(同步)
  1. 主从同步成功后,才给客户端返回
  2. 主从通过brokerId区分,0为主,1为从
  • 多master多slave模式(异步)
  1. 消息写入master节点即可返回,异步线程负责主从同步
  2. mster的配置标记为异步主节点brokerRole=ASYNC_MASTER

刷盘

  • 同步刷盘
  1. 消息必须落盘,才会返回客户端ack
  2. 同步刷盘+同步复制,可靠性是最高的,但是性能不高
  • 异步刷盘
  1. 启动异步线程,进行落盘
  2. 异步刷盘+同步复制,性能能接受,且可靠性较高
  3. 主从复制速度 > 刷盘

存储设计

  • Message,消息对象
  1. messageId
  2. messageKey
  3. equals
  4. hashCode
  • Topic,主题
  1. tags
  2. subTopic
  • group 消费组/生产组
  • Queue,队列
  • Offset,偏移量

对应关系

  • 一个主题有多少消息
  • 一个主题有多个队列
  • 一个主题可以来源于多个组,也可以被多个组消费
  • 一个组也可以有多个主题,也可以消费多个主题
  • 一个主题有多个队列,也对应着多个偏移量

消费并发度

  • 一个主题划分出多个Queue,不同的Queue部署到不同的机器上

消息持久化

存储文件

  • commitLog,消息存储目录
  • config,运行配置
  1. topic.json,topic配置
  2. subscriptionGroup.json,消息消费组配置信息
  3. delayOffset.json 延时消息队列拉取进度
  4. consumerOffset.json 集群消费模式消息消费进度
  5. consumerFilter.json 主题消息过滤消息
  • consumequeue,消息消费队列存储目录
  • index,消息索引文件存储目录
  • abort,如果存在该文件,说明Broker非正常关闭
  • checkpoint,文件检查点
  1. CommitLog文件最后一次刷盘时间戳
  2. consumerqueue最后一次刷盘时间戳
  3. index索引文件最后一次刷盘时间戳

存储方式

  • 主要通过commitLog和consumerqueue实现持久化
  • commitLog存储各个主题消息的元数据,消息内容、队列、tags、key等
  1. 每一个commitLog为1个G
  • consumequeue,消息的逻辑队列,类似数据库索引文件,指向物理存储地址
  1. 每个Topic的队列都有一个对应的ConsumeQueue的文件
  2. 存储消息条目,20个字节,不存储消息本地,存储消息在commitLog的偏移量,长度,tags标签的hash
  3. 作为commitLog的索引文件,消息到达commitLog后,由专门的线程转发任务
  4. 保证磁盘顺序写,可以根据consumequeue索引,找到下一个写入commitLog的位置
  5. consumequeue固定二十个字节,通过pageCache进行操作,再找到commitLog中消息的位置,速度很快
  6. consumequeue丢失,也可以通过commitLog恢复
  • IndexFile,索引文件,加速消息查询的速度
  1. 用MessageKey生成Hash索引,记录CommitLog中的offset

过期文件删除

  • RocketMQ通过内存映射来操作CommitLog和ConsumeQueue文件的,需要及时删除,避免内存和磁盘浪费
  • 非当前写的文件在一定时间间隔内没有被再次更新,则认为是过期文件,可以被删除,默认过期时间为42小时

如果最后一次更新到现在,超过42个小时,则可以删除

  • 定时任务触发删除,每个10s执行一次
  • 如果待删除的文件,被线程引用,会进行删除拒绝,并记录当前时间,当时间间隔大于destroyMapedFileIntervalForcibly,会将引用减少1000,直到为0,可以被删除

RocketMQ中零拷贝

  • 零拷贝,不需要通过CPU进行数据拷贝,减少操作系统进行IO操作的次数,和用户态与内核态的切换
  • mmap内存映射,将内核缓冲区映射到用户缓冲区,但是写入时也是需要CPU拷贝,在内核中进行拷贝

分布式事务

  • 独立模块的合并操作需要保证事务的原子性

RocketMQ的处理

  • 发送半事务消息,只保存在CommitLog,不构建索引,消费者无法消费
  • 事务回查机制,当事务提交成功后,将半事务转换为全事务,消费者可以消费
  • 方案
  1. A发送半事务消息,并执行业务
  2. A提交事务,通过回查机制,把半事务消息转换为全事务消息
  3. B消费消息
  • 半事务消息队列 image.png

生产者

  • 当生产者组中的一个生产者挂掉了,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();