RocketMQ

搭建 RocketMQ 服务

  1. 安装 Java

oracle JDK 下载官网

https://www.oracle.com/java/technologies/downloads/

  1. 下载 RocketMQ

rocketMQ 下载官网

https://rocketmq.apache.org/download/

  1. 在 bin 目录中,先后启动 nameserver 和 broker
  1. 启动生产者消费者测试收发消息

./tools.sh org.apache.rocketmq.example.quickstart.Producer

./tools.sh org.apache.rocketmq.example.quickstart.Consumer

搭建 RocketMQ 可视化管理服务

rocketMQ 官网下载 dashboard

使用 Maven 编译

编译完后生成 rocketmq-dashboard-2.0.0.jar

将 jar 包上传到 linux 机器,修改配置,运行 jar 包

复制代码
rocketmq:
  config:
    namesrvAddrs:
      - 192.168.10.102:9876

nohup java -jar rocketmq-dashboard-2.0.0.jar 1>dashboard.log 2>&1 &

访问 http://192.168.10.102:8080

RocketMQ 消息模型

运行架构

  1. nameserver:nameServer 不依赖于任何其他的服务,自己独立就能启动。并且,不管是 broker 还是客户端,都需要明确指定 nameServer 的服务地址
  2. broker:RocketMQ 最核心的消息存储、传递、查询等功能都要由 broker 提供
  3. client:client 包括消息生产者和消息消费者

消息模型

消息确认机制

1.1. 单向发送

消息生产者只管往 Broker 发送消息,而不关心 Broker 端有没有成功接收到消息,如果发送失败,生产者无法补救

1.2. 同步发送

同步发送方式下,消息生产者在往 Broker 端发送消息后,会阻塞当前线程,等待 Broker 端的响应结果

1.3. 异步发送

异步发送机制下,生产者在向 Broker 发送消息时,会同时注册一个回调函数。接下来生产者并不等待 Broker 的响应。当 Broker 端有响应数据过来时,自动触发回调函数进行对应的处理

广播消息

广播模式和集群模式是 RocketMQ 的消费者端处理消息最基本的两种模式;

集群模式下,一个消息,只会被一个消费者组中的一个消费者实例处理一次

广播模式下,一个消息,则会推送给所有消费者实例处理,不再关心消费者组

复制代码
consumer.setMessageModel(MessageModel.BROADCASTING);

过滤消息

同⼀个 Topic 下有多种不同的消息,消费者只希望关注某一类消息

1.1. 简单过滤

生产者端需要在发送消息时,增加 Tag 属性,消费者端就可以通过 Tag 属性订阅自己感兴趣的内容

java 复制代码
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);
}
java 复制代码
consumer.subscribe("TagFilterTest", "TagA");

1.2. SQL 过滤

如果要进行更复杂的消息过滤,比如数字比较、模糊匹配等,就需要使用 SQL 过滤方式。SQL 过滤方式可以通过 Tag 属性以及用户自定义的属性一起,以标准 SQL 的方式进行消息过滤

java 复制代码
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("age", i + "");
    msg.putUserProperty("region", "shanghai");
    msg.putUserProperty("level", (i % 2) == 0 ? "vip" : "normal");
    SendResult sendResult = producer.send(msg);
    System.out.printf("%s\n", sendResult);
}
java 复制代码
// 2. 订阅 + SQL过滤(核心)
consumer.subscribe("SqlFilterTest", MessageSelector.bySql("age > 10 AND level = 'vip'"));

顺序消息

生产者通过 MessageSelector,将 orderId 相同的消息转发到同一个 MessageQueue 中

复制代码
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);
    }
}

消费者通过 MessageListenerOrderly 实现顺序消费

复制代码
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;
    }
});

实现原理

  1. 生产者将一批有顺序要求的消息,放到同一个 MessageQueue 上,通过 MessageQueue 的 FIFO 特性保证这一批消息的顺序;如果不指定 MessageSelector 对象,那么生产者会采用轮询的方式将多条消息依次发送到不同的 MessageQueue 上
  2. 消费者需要实现 MessageListenerOrderly 接口,服务端处理MessageListenerOrderly 时,会给一个 MessageQueue 加锁,消费者拿到 MessageQueue 上所有的消息,然后再去读取下一个 MessageQueue 的消息

注意点

  1. 顺序消息是局部有序的;大部分业务场景下,我们需要的其实是局部有序。如果要保持全局有序,那就只保留一个 MessageQueue,性能显然非常低;
  2. 生产者端在保证"局部有序"的前提下,把不同业务 Key 的消息分散到不同队列,从而提升并发能力;
  3. 如果一条消息处理失败,RocketMQ 会将后续消息阻塞住,让消费者进行重试;但是,如果消费者一直处理失败,超过最大重试次数,那么 RocketMQ 就会跳过这一条消息,处理后面的消息,这会造成消息乱序;
  4. 消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 作为替代。

延迟消息

延迟消息不会立即被消费者消费,而是"延迟一段时间后"才投递给消费者

1.1. 固定的延迟级别

对于指定固定延迟级别的延迟消息,RocketMQ 的实现方式是预设一个系统 Topic,名字叫做SCHEDULE_TOPIC_XXXXX;在这个 Topic 下,预设了 18 个 MessageQueue,这里每个对列就对应了一种延迟级别

复制代码
message.setDelayTimeLevel(3); // 延迟级别3,表示延迟10秒

1.2. 指定消息延迟时间

指定时间点的延迟消息,RocketMQ 是通过时间轮算法实现的

复制代码
message.setDelayTimeMs(1000 * 10);  // 延迟10秒

批量消息

生产者要发送的消息比较多时,可以将多条消息合并成一个批量消息,一次性发送出去;这样可以减少网络 IO,提升消息发送的吞吐量

复制代码
List<Message> msgs = new ArrayList<>();
msgs.add(new Message("TopicA", "TagA", "Hello1".getBytes()));
msgs.add(new Message("TopicA", "TagA", "Hello2".getBytes()));
msgs.add(new Message("TopicA", "TagA", "Hello3".getBytes()));
producer.send(msgs);

同一批消息的 Topic 必须相同,且不支持延迟消息

事务消息

事务消息(Transactional Message)保证"本地事务"和"消息发送"要么一起成功,要么一起失败(最终一致性)

事务消息执行流程

  1. 生产者将消息发送至 RocketMQ 服务端;
  2. RocketMQ 服务端将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息;
  3. 生产者开始执行本地事务逻辑;
  4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit 或是 Rollback),服务端收到确认结果后处理逻辑如下:
    1. 二次确认结果为 Commit:服务端将半事务消息标记为可投递,并投递给消费者;
    2. 二次确认结果为 Rollback:服务端将回滚事务,不会将半事务消息投递给消费者;
  1. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为 Unknown 未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查;

  2. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果;

  3. 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行处理;

    TransactionMQProducer producer = new TransactionMQProducer("tx_group");
    producer.setTransactionListener(new TransactionListener() {
    // 执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    try {
    // 本地事务(比如数据库操作)
    return LocalTransactionState.COMMIT_MESSAGE;
    } catch (Exception e) {
    return LocalTransactionState.ROLLBACK_MESSAGE;
    }
    }
    // 回查事务状态
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    // 查询数据库状态
    return LocalTransactionState.COMMIT_MESSAGE;
    }
    });

使用场景

注意点

半消息是对消费者不可见的一种消息;实际上,RocketMQ 是将消息转到了一个系统 Topic:RMQ_SYS_TRANS_HALF_TOPIC;

事务消息中,本地事务回查次数通过参数 transactionCheckMax 设定,默认15次;本地事务回查的间隔通过参数 transactionCheckInterval 设定,默认 60 秒,超过回查次数后,消息将会被丢弃

RocketMQ 注意事项

1. 消息幂等

在 MQ 系统中,对于消息幂等有三种实现语义:

at most once 最多一次:每条消息最多只会被消费一次

at least once 至少一次:每条消息至少会被消费一次

exactly once 刚好一次:每条消息都只会确定的消费一次

RocketMQ 默认提供 at least once,即消息至少被消费一次,但可能重复消费,如果需要 at most once,可以通过先 ACK 再处理业务实现,但会带来消息丢失风险;RocketMQ 保证不了 exactly once,所以,使用 RocketMQ 时,需要由业务系统自行保证消息的幂等性,如唯一 ID、去重表、状态机

1.1. 消息重复场景

  1. 发送时消息重复

当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败;如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息

  1. 投递时消息重复

消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断;为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息

2. 错误消息重试

在重试时,RocketMQ 会为每个消费者组创建一个对应的重试队列:

"%RETRY%"+ConsumeGroup,通过关注重试队列,可以及时了解消费者端的运行情况

RocketMQ 默认允许每条消息最多重试 16 次

3. 死信队列

消息消费失败超过最大重试次数时,RocketMQ 会将其发送到这个消费者组对应的死信队列中,死信队列的名称是 %DLQ%+ConsumGroup,死信队列的有效期跟正常消息相同,默认3天(对应 broker.conf 中的 fileReservedTime),超过这个时间的消息都会被删除

默认创建出来的死信队列,他里面的消息是无法读取的,这是因为这些默认的死信队列,他们的权限 perm 被设置成了 2(2:禁读,4:禁写,6:可读可写),需要手动修改死信队列的权限,才能被消费

RocketMQ 常见面试题

MQ 如何保证消息不丢失

要保证消息不丢失,首先要清楚哪些环节可能会丢消息

  1. 生产者发送消息到服务端
  2. Broker 写数据过程中
  3. Broker 主从同步时
  4. 消费者消费消息时

生产者发送消息如何保证不丢失

  1. 生产者发送确认机制:生产者发送确认,失败重试,Broker 确认应答(发送成功,但响应丢失会导致重复发送,需要消费端做幂等处理)
  2. RocketMQ 事务消息机制:解决"本地事务和消息发送之间的一致性问题",通过半消息和事务回查机制,保证消息要么随着事务一起提交,要么一起回滚,从而避免业务成功但消息未发送或业务失败但消息发送成功的情况

Broker 写入数据如何保证不丢失:同步刷盘保证消息的持久化

Broker 主从同步如何保证不丢失:同步复制保证从节点有完整数据

消费者消费消息如何保证不丢失:消费者消费确认,失败重试机制(消费成功但未提交 offset 会重复消费,但不会丢)

MQ 如何保证消息的顺序性

顺序性通常指的是局部有序

RocketMQ 从两个方面保证消息有序

  1. Producer 通过定制数据分片算法(MessageQueueSelector),将一组有序的消息写入到同一个 MessageQueue 中,通过队列的 FIFO 特性,保证消息的处理顺序
  2. Consumer 每次集中从一个 MessageQueue 中拿取消息,一个队列(队列加锁)对应一个消费者线程池中的一个线程 (MessageListenerOrderly),在消费失败阻塞队列后续消息

MQ 如何保证消息的幂等性

RocketMQ 保证消息至少被消费一次,可能会重复消费,因此需要在消费端通过业务唯一标识、数据库唯一约束、幂等表或状态机等方式实现幂等处理,保证同一条消息只被处理一次

MQ 如何快速处理积压的消息

产生消息积压的根本原因还是 Consumer 处理消息的效率太低,可以针对处理消息比较慢的消费者组,增加更多的 Consumer 实例;要注意的是,同一个消费者组下的多个 Consumer 需要和对应 Topic 下的 MessageQueue 建立对应关系,而一个 MessageQueue 最多只能被一个 Consumer 消费,因此,增加的 Consumer 实例最多也只能和 Topic 下的 MessageQueue 个数相同;这时需要创建一个新的 Topic,配置足够多的 MessageQueue,然后把 Consumer 实例的 Topic 转向新的 Topic,并紧急上线一组新的消费者,只负责消费旧 Topic 中的消息,并转存到新的 Topic 中

RocketMQ 消费者多个线程消费同一个 messagequeue 时,如何保证不会并发修改 offset

在 RocketMQ 并发消费模式下,同一个 MessageQueue 的消息可以被多个线程并发处理,但 offset 的提交由内部的 ProcessQueue 控制,offset 会"按批次、按顺序提交",推进到"连续已完成"的位置 ,从而避免并发更新 offset 导致消息丢失, 如果某条消息消费失败,会阻塞该队列后续 offset 的推进,但不会阻止其他线程继续处理消息,从而在保证不丢消息的前提下尽可能提高吞吐量

相关推荐
JH30732 小时前
RedLock-红锁
java·redis
AC赳赳老秦2 小时前
OpenClaw多平台部署:Windows+Linux跨系统协同,实现全场景覆盖
linux·服务器·前端·网络·windows·deepseek·openclaw
念恒123062 小时前
进程--程序地址空间下篇(进程地址空间)
linux·c语言
___波子 Pro Max.3 小时前
Linux 外挂 SSD 根目录下的 `.Trash-1000` 到底是什么
linux
rannn_1113 小时前
【Redis|原理篇2】Redis网络模型、通信协议、内存回收
java·网络·redis·后端·缓存
hhb_6183 小时前
Linux底层运维自动化挂载与磁盘分区实战指南
linux·运维·自动化
遇见你的雩风3 小时前
网络原理(一)
java·网络
952364 小时前
Spring IoC&DI
java·数据库·spring
十六年开源服务商4 小时前
游戏与设计驱动WordPress建站2026
java·前端·游戏