分布式中间件 消息队列Rocketmq 详解

目录

常见概念

发送消息的三种方式

同步发送:等待消息返回后再继续进行下面的操作。

异步发送:不等待消息返回直接进入后续流程。

单向发送:只负责发送,不管消息是否发送成功。

接收消息的两种方式

[拉模式:消费者主动去 Broker 上拉取消息。](#拉模式:消费者主动去 Broker 上拉取消息。)

[推模式:消费者等待 Broker 把消息推送过来。](#推模式:消费者等待 Broker 把消息推送过来。)

消息类型

顺序消息

广播消息

集群消息

延迟消息

批量消息

过滤消息

事务消息(重点)

实现步骤

注意事项

代码示例

如何保证消息不丢失

[Rocketmq 的持久化机制](#Rocketmq 的持久化机制)

如何确保消息顺序


常见概念

Producer 生产者

Consumer 消费者

Broker 管道 负责消息的暂存和传输

NameServer 负责管理管道

Topic 主题 用于区分消息的种类 是消息队列中的一个逻辑上的概念

一个发送者可以发送消息给一个或者多个 Topic

一个消息的接收者可以订阅一个或者多个 Topic

Message Queue 消息队列

相当于是主题的分区 用于并行发送或者接收消息

一个主题可能包含多个分区 消息可能会被分发到不同的分区中、

正因为消息被分散到多个 "队列(分区)" 中,才实现了并行处理:

发送端:可以同时向多个队列发送消息(比如多个生产者分别写入不同分区),避免单队列的写入瓶颈。

接收端:多个消费者可以同时从不同队列中读取消息(比如每个消费者负责一个分区),各自处理自己队列中的消息,无需等待其他消费者,大幅提高整体处理效率。

发送消息的三种方式

同步发送:等待消息返回后再继续进行下面的操作。

复制代码
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("SyncProducer");
    producer.setNamesrvAddr("192.168.43.137:9876");
    producer.start();
    for (int i = 0; i < 2; i++) {
        Message msg = new Message("Simple", //主题
                                  "TagA",  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                                  "Simple-Sync".getBytes(StandardCharsets.UTF_8) //消息体。
                                 );
        // send()方法会阻塞
        SendResult send = producer.send(msg);
        System.out.printf(i + ".发送消息成功:%s%n", send);
    }
    producer.shutdown();
}

异步发送:不等待消息返回直接进入后续流程。

broker将结果返回后调用callback函数,并使用CountDownLatch计数。

producer.send(message, new SendCallback() {}) 是一个异步的过程

主线程会立即返回,而不会等待 Callback 而阻塞

复制代码
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
    producer.setNamesrvAddr("192.168.43.137:9876");
    producer.start();
    CountDownLatch countDownLatch = new CountDownLatch(100);//计数
    for (int i = 0; i < 100; i++) {
        Message message = new Message("Simple", "TagA", "Simple-Async".getBytes(StandardCharsets.UTF_8));
        final int index = i;
        producer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                countDownLatch.countDown();
                System.out.printf("%d 消息发送成功%s%n", index, sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                countDownLatch.countDown();
                System.out.printf("%d 消息失败%s%n", index, throwable);
                throwable.printStackTrace();
            }
        }
                     );
    }
    countDownLatch.await(5, TimeUnit.SECONDS);
    producer.shutdown();
}

单向发送:只负责发送,不管消息是否发送成功。

复制代码
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
    producer.setNamesrvAddr("192.168.43.137:9876");
    producer.start();
    for (int i = 0; i < 10; i++) {
        Message message = new Message("Simple","TagA", "Simple-Oneway".getBytes(StandardCharsets.UTF_8));
        producer.sendOneway(message);
        System.out.printf("%d 消息发送完成 %n" , i);
    }
    Thread.sleep(5000);
    producer.shutdown();
}

接收消息的两种方式

拉模式:消费者主动去 Broker 上拉取消息。

复制代码
public static void main(String[] args) throws MQClientException {
    DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer("SimplePullConsumer");
    pullConsumer.setNamesrvAddr("192.168.43.137:9876");//执行nameserver地址
    Set<String> topics = new HashSet<>();
    topics.add("Simple");//添加Topic
    topics.add("TopicTest");
    pullConsumer.setRegisterTopics(topics);
    pullConsumer.start();
    while (true) { //循环拉取消息
        pullConsumer.getRegisterTopics().forEach(n -> {
            try {
                Set<MessageQueue> messageQueues = pullConsumer.fetchSubscribeMessageQueues(n);//获取主题中的Queue
                messageQueues.forEach(l -> {
                    try {
                        //获取Queue中的偏移量
                        long offset = pullConsumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_MEMORY);
                        if (offset < 0) {
                            offset = pullConsumer.getOffsetStore().readOffset(l, ReadOffsetType.READ_FROM_STORE);
                        }
                        if (offset < 0) {
                            offset = pullConsumer.maxOffset(l);
                        }
                        if (offset < 0) {
                            offset = 0;
                        }
                        //拉取Queue中的消息。每次获取32条
                        PullResult pullResult = pullConsumer.pull(l, "*", offset, 32);
                        System.out.printf("循环拉取消息ing %s%n",pullResult);
                        switch (pullResult.getPullStatus()) {
                            case FOUND:
                                pullResult.getMsgFoundList().forEach(p -> {
                                    System.out.printf("拉取消息成功%s%n", p);
                                });
                                //更新偏移量
                                pullConsumer.updateConsumeOffset(l, pullResult.getNextBeginOffset());
                        }
                    } catch (MQClientException e) {
                        e.printStackTrace();
                    } catch (RemotingException e) {
                        e.printStackTrace();
                    } catch (MQBrokerException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        });
    }
}

推模式:消费者等待 Broker 把消息推送过来。

复制代码
public static void main(String[] args) throws MQClientException {
    DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
    pushConsumer.setNamesrvAddr("192.168.43.137:9876");
    pushConsumer.subscribe("Simple","*");
    pushConsumer.setMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            list.forEach( n->{
                System.out.printf("收到消息: %s%n" , n);
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    pushConsumer.start();
    System.out.printf("Consumer Started.%n");
}

消息类型

顺序消息

生产者局部有序的将消息发送到同一个 queue

但是多个 queue 之间是全局无序的

每次消费者读取消息都从一个 queue 中读取 (通过加锁的方式实现)

广播消息

广播消息并没有特定的消费者实例

一条消息可以发给所有订阅了该主题的消费者,不管是不是一个消费组

集群消息

集群消息并没有特定的消费者实例

一条消息可以被一个消费组内任意一个消费者消费

延迟消息

这是 Rocketmq 特有的功能,消息会过一段时间再发送出去

批量消息

将多条消息压缩成一条批量消息,一次性发送出去,减少网络 IO

过滤消息

Rocketmq 中有一个 tag 的概念

不同与主题 topic

一个应用可以就用一个Topic,而应用中的不同业务就用Tag来区分。

Tag方式有一个很大的限制,就是一个消息只能有一个Tag,这在一些比较复杂的场景就有点不足了。

复制代码
 public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("FilterTopic","TagA || TagC");
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("收到消息: %s%n" , new String(n.getBody()));
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("TagFilter Consumer Started.%n");
    }

我们还可以用 sql 表达式来过滤

生产者

复制代码
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("SyncProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        producer.start();
        String[] tags = new String[] {"TagA","TagB","TagC"};
        for (int i = 0; i < 15; i++) {
            Message msg = new Message("FilterTopic", //主题
                                      tags[i % tags.length],  //设置消息Tag,用于消费端根据指定Tag过滤消息。
                                      ("TagFilterProducer_"+tags[i % tags.length] +  "_i_" + i).getBytes(StandardCharsets.UTF_8) //消息体。
                                     );
            msg.putUserProperty("baiLi", String.valueOf(i));
            SendResult send = producer.send(msg);
            System.out.printf(i + ".发送消息成功:%s%n", send);
        }
        producer.shutdown();
    }

消费者

复制代码
 public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("SimplePushConsumer");
        pushConsumer.setNamesrvAddr("192.168.43.137:9876");
        pushConsumer.subscribe("FilterTopic", MessageSelector.bySql("(TAGS is not null And TAGS IN ('TagA','TagC'))"
        + "and (baiLi is not null and baiLi between 0 and 3)"));
        pushConsumer.setMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach( n->{
                    System.out.printf("收到消息: %s%n" , new String(n.getBody()));
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        pushConsumer.start();
        System.out.printf("SqlFilter Consumer Started.%n");
    }

事务消息(重点)

事务消息是在分布式系统中保证最终一致性的两阶段提交的消息实现。

他可以保证本地事务执行与消息发送两个操作的原子性,也就是这两个操作一起成功或者一起失败。

事务消息机制的关键是在发送消息时会将消息转为一个half半消息,

并存入RocketMQ内部的一个Topic(RMQ_SYS_TRANS_HALF_TOPIC),

这个Topic对消费者是不可见的。

再经过一系列事务检查通过后,再将消息转存到目标Topic,这样对消费者就可见了。

实现步骤

我们现在有两个微服务 一个是订单服务 一个是库存服务 订单服务在上游 库存服务在下游

我们首先是创建订单 把订单数据写入订单表中 之后才是扣减库存

  1. 在订单服务中,我们首先会发送一个库存扣减的半消息到 Rocketmq 的半消息队列中,这条消息于普通消息,但是对于消费者是不可见的
  2. 随后我们会执行本地事务 ,如创建订单,在数据库中创建记录
  3. 我们之后会根据本地事务的回调机制去进行下一步操作,如提交事务消息或回滚事务消息,

如果订单创建成功,那么我们会使先前库存扣减的半消息移到具体队列,对库存服务可见,随后消息到下游,执行相关业务逻辑,

如果订单创建失败,则会回滚事务消息,删除半消息队列中半消息

  1. 如果 RocketMq 未收到事务提交或者事务回滚的结果,会回查订单服务,以确定事务的最终状态

注意事项

  1. 本地事务操作应该保证幂等性,如订单创建和库存扣减
  2. 我们应该正确处理异常状况
  3. 事务回查策略应该恰当设置,要考虑到性能影响

代码示例

事务消息的生产者

复制代码
package transaction;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;

/**
 * 事务消息生产者
 * Created by BaiLi
 */
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionMQProducer producer = new TransactionMQProducer("TransProducer");
        producer.setNamesrvAddr("192.168.43.137:9876");
        //使用executorService异步提交事务状态,从而提高系统的性能和可靠性
        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);

        //本地事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        producer.setTransactionListener(transactionListener);

        producer.start();
        String[] tags = new String[] {"TagA","TagB","TagC","TagD","TagE"};
        for (int i = 0; i < 10; i++) {
            Message message = new Message("TransactionTopic",
                    tags[ i % tags.length],
                    ("Transaction_"+ tags[ i % tags.length]).getBytes(StandardCharsets.UTF_8));
            TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
            System.out.printf("%s%n", transactionSendResult);

            Thread.sleep(10); //延迟10毫秒
        }

        Thread.sleep(100000);//等待broker端回调
        producer.shutdown();
    }
}

本地事务的监听器

复制代码
package transaction;

import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

/**
 * 本地事务监听器
 * Created by BaiLi
 */
public class TransactionListenerImpl implements TransactionListener {

    @Override
    /**
     * 在提交完事务消息后执行。
     * 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
     * 返回ROLLBACK_MESSAGE状态的消息会被丢弃。
     * 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
     */
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String tags = message.getTags();
        //TagA的消息会立即被消费者消费到
        if(StringUtils.contains(tags,"TagA")){
            return LocalTransactionState.COMMIT_MESSAGE;
            //TagB的消息会被丢弃
        }else if(StringUtils.contains(tags,"TagB")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
            //其他消息会等待Broker进行事务状态回查。
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }

    @Override
    /**
     * 在对UNKNOWN状态的消息进行状态回查时执行。
     * 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
     * 返回ROLLBACK_MESSAGE状态的消息会被丢弃。
     * 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
     */
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        String tags = messageExt.getTags();
        //TagC的消息过一段时间会被消费者消费到
        if(StringUtils.contains(tags,"TagC")){
            return LocalTransactionState.COMMIT_MESSAGE;
            //TagD的消息也会在状态回查时被丢弃掉
        }else if(StringUtils.contains(tags,"TagD")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
            //剩下TagE的消息会在多次状态回查后最终丢弃
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
}

如何保证消息不丢失

Rocketmq 的消息流程主要在三部分,消息的生产,消息的存储,消息的消费

这三个阶段都有对应的丢失数据的可能,也有对应的解决方案

  • 生产阶段,由于网络原因,消息可能回直接丢失,我们可以采用同步发送消息失败重试;异步发送等待回调,用 countDownLatch 卡一下;或者参考计算机网络的 TCP 模型,采取 Ack 确认机制
  • 存储阶段,我们可以同步刷盘机制,如果是消息队列集群可以同步复制,CommitLog 是 RocketMQ 消息存储的 "大账本",所有消息都在这里 "记账",再通过索引(ConsumeQueue、IndexFile)实现高效的消息检索和消费,同时依靠刷盘和主从机制保证可靠性。
  • 消费阶段,我们同样可以参考计算机网络的 TCP 模型,采取 Ack 机制,如果出现异常则采取重试机制

Rocketmq 的持久化机制

RocketMQ的消息持久化机制是指将消息存储在磁盘上,以确保消息能够可靠地存储和检索。

RocketMQ 的消息持久化机制涉及到以下三个角色:CommitLog、ConsumeQueue 和 IndexFile。

  • CommitLog:消息真正的存储文件,所有的消息都存在 CommitLog文件中。

RocketMQ默认会将消息数据先存储到内存中的一个缓冲区,每当缓冲区中积累了一定量的消息或者一定时间后,就会将缓冲区中的消息批量写入到磁盘上的 CommitLog 文件中。消息在写入 CommitLog 文件后就可以被消费者消费了。

Commitlog文件的大小固定1G,写满之后生成新的文件,并且采用的是顺序写的方式。

  • ConsumeQueue:消息消费逻辑队列,类似数据库的索引文件。

RocketMQ 中每个主题下的每个消息队列都会对应一个 ConsumeQueue。ConsumeQueue存储了消息的offset以及该offset对应的消息在CommitLog文件中的位置信息,便于消费者快速定位并消费消息。

每个ConsumeQueue文件固定由30万个固定大小20byte的数据块组成;据块的内容包括:msgPhyOffset(8byte,消息在文件中的起始位置)+msgSize(4byte,消息在文件中占用的长度)+msgTagCode(8byte,消息的tag的Hash值)。

  • IndexFile:消息索引文件,主要存储消息Key与offset的对应关系,提升消息检索速度。

如果生产者在发送消息时设置了消息Key,那么RocketMQ会将消息Key值和消息的物理偏移量(offset)存储在IndexFile文件中,这样当消费者需要根据消息Key查询消息时,就可以直接在IndexFile文件中查找对应的offset,然后通过 ConsumeQueue文件快速定位并消费消息。

IndexFile文件大小固定400M,可以保存2000W个索引。

如何确保消息顺序

RocketMQ架构本身是无法保证消息有序的,但是提供了相应的API保证消息有序消费。RocketMQ API利用FIFO先进先出的特性,保证生产者消息有序进入同一队列,消费者在同一队列消费就能达到消息的有序消费。

  • 使用MessageQueueSelector编写有序消息生产者

有序消息生产者会按照一定的规则将消息发送到同一个队列中,从而保证同一个队列中的消息是有序的。RocketMQ 并不保证整个主题内所有队列的消息都是按照发送顺序排列的。

  • 使用MessageListenerOrderly进行顺序消费与之对应的MessageListenerConcurrently并行消费(push模式)

MessageListenerOrderly是RocketMQ 专门提供的一种顺序消费的接口,它可以让消费者按照消息发送的顺序,一个一个地处理消息。这个接口支持按照消息的重试次数进行顺序消费、订单ID等作为消息键来实现顺序消费、批量消费等操作。

通过加锁的方式实现(有超时机制),一个队列同时只有一个消费者;并且存在一个定时任务,每隔一段时间就会延长锁的时间,直到整个消息队列全部消费结束。

  • 消费端自己保证消息顺序消费(pull模式)
  • 消费者并发消费时设置消费线程为1

RocketMQ 的消费者可以开启多个消费线程同时消费同一个队列中的消息,如果要保证消息的顺序,需要将消费线程数设置为1。这样,在同一个队列中,每个消息只会被单个消费者线程消费,从而保证消息的顺序性

相关推荐
Slow菜鸟2 小时前
Java 开发环境安装指南(一) | 目录设计规范
java
njsgcs2 小时前
pyautocad获得所选圆弧的弧长总和
开发语言·windows·python
xiaoxue..2 小时前
深入理解JavaScript中的深拷贝与浅拷贝:内存管理的艺术
开发语言·前端·javascript·面试
BS_Li2 小时前
【Linux系统编程】进程控制
java·linux·数据库
從南走到北2 小时前
JAVA外卖霸王餐CPS优惠CPS平台自主发布小程序+公众号霸王餐源码
java·开发语言·小程序
z***67772 小时前
postgresql链接详解
数据库·postgresql
小龙报2 小时前
《嵌入式成长系列之51单片机 --- 固件烧录》
c语言·开发语言·单片机·嵌入式硬件·51单片机·创业创新·学习方法
v***91302 小时前
MYSQL的第一次
数据库·mysql
麦兜*2 小时前
Redis在Web3中的应用探索:作为链下状态缓存与索引层
java·spring boot·redis·spring cloud·缓存·docker·web3