RocketMQ作为阿里巴巴开源的分布式消息中间件,已经成为Apache顶级项目,在金融、电商、物流等多个领域有着广泛应用。我第一次接触RocketMQ是在2016年参与一个大型电商平台重构项目,当时我们需要一个能够支撑日均亿级消息量的消息队列系统。经过对比测试,RocketMQ以其出色的稳定性和性能表现脱颖而出。
RocketMQ的设计哲学可以概括为"简单而高效"。它的核心特点包括:
高可用性:采用主从架构和多副本机制,确保单点故障时服务不中断。在我负责的系统中,曾经遇到过Broker节点宕机的情况,但由于配置了同步复制,消息没有丢失且自动完成了故障转移。
高吞吐量:通过CommitLog顺序写入和零拷贝技术,单机可支持10万级TPS。我们做过压测,在16核32G的机器上,RocketMQ的写入性能可以达到约15万TPS。
低延迟:消息投递延迟控制在毫秒级。对于我们的实时订单系统,从下单到库存扣减的延迟通常保持在3-5毫秒。
消息类型丰富:除了基本的发布订阅模式,还支持:
RocketMQ的架构设计非常清晰,主要由四个核心组件构成:
NameServer是RocketMQ的"通讯录",负责服务发现和路由管理。它的设计有几个精妙之处:
无状态设计:每个NameServer节点都是独立的,不相互通信。这种设计使得集群扩展非常容易,只需要简单添加节点即可。
最终一致性:通过心跳机制维护数据,Broker每30秒发送一次心跳,NameServer如果120秒没收到心跳则认为Broker下线。
轻量级:数据全存储在内存中,响应速度极快。在我们的生产环境中,NameServer的CPU使用率通常保持在5%以下。
提示:生产环境建议至少部署3台NameServer组成集群,遵循2N+1原则保证高可用。
Broker是真正存储和转发消息的组件,其架构设计体现了RocketMQ的精髓:
java复制// Broker核心模块示意图
Broker
├── Remoting Module // 网络通信层,基于Netty实现
├── Client Manager // 管理所有连接的客户端
├── Store Service // 消息存储服务
│ ├── CommitLog // 所有消息的物理存储
│ ├── ConsumeQueue // 逻辑消费队列
│ └── IndexFile // 消息索引文件
├── HA Service // 高可用服务,处理主从复制
└── Config Manager // 配置管理
存储机制是Broker最核心的部分:
这种设计带来了几个优势:
生产者和消费者的设计也体现了RocketMQ的灵活性:
生产者支持三种发送模式:
消费者有两种消费模式:
在我们的实际使用中,90%的场景都采用Push模式,因为它更简单高效。但对于需要精确控制消费节奏的场景(如流控),Pull模式会更合适。
下表是RocketMQ与Kafka、RabbitMQ、ActiveMQ的详细对比:
| 特性 | RocketMQ | Kafka | RabbitMQ | ActiveMQ |
|---|---|---|---|---|
| 设计目标 | 金融级交易 | 日志处理 | 企业级消息 | 传统消息代理 |
| 吞吐量 | 10万+ TPS | 10万+ TPS | 5万+ TPS | 1万+ TPS |
| 延迟 | 毫秒级 | 毫秒级 | 微秒级 | 毫秒级 |
| 持久化 | 磁盘持久化 | 磁盘持久化 | 内存/磁盘 | 内存/磁盘 |
| 事务消息 | 支持 | 不支持 | 支持 | 支持 |
| 消息顺序 | 严格顺序 | 分区顺序 | 不保证 | 保证 |
| 消息回溯 | 支持 | 支持 | 有限支持 | 有限支持 |
| 协议支持 | 自定义协议 | 自定义协议 | AMQP等 | OpenWire等 |
| 开发语言 | Java | Scala/Java | Erlang | Java |
| 管理界面 | 提供 | 需第三方 | 提供 | 提供 |
根据我的项目经验,不同场景下的选型建议如下:
金融支付场景:优先选择RocketMQ
日志收集场景:Kafka更合适
企业应用集成:RabbitMQ更适合
传统系统迁移:ActiveMQ可能更易集成
注意:我们在2018年曾尝试用Kafka处理交易消息,结果因为不支持事务消息导致对账困难,最终切换回RocketMQ。这个教训告诉我们,技术选型必须匹配业务场景。
NameServer在RocketMQ架构中扮演着至关重要的角色,但它的设计却出奇地简单高效。理解NameServer的工作原理,对于排查路由相关问题非常有帮助。
当Broker启动时,会向所有NameServer节点注册自己的信息:
java复制// 伪代码展示Broker注册过程
public void registerWithNameServer() {
while (true) {
try {
// 构建注册数据
RegisterBrokerRequest request = buildRegisterRequest();
// 向所有NameServer注册
for (NameServerAddr addr : nameServerAddrs) {
nameServerClient.register(request, addr);
}
// 30秒后再次注册(心跳)
Thread.sleep(30000);
} catch (Exception e) {
log.error("Register with NameServer failed", e);
}
}
}
生产者和消费者启动时,会从NameServer获取路由信息:
这种设计有几个优点:
在生产环境中,NameServer的高可用配置需要注意以下几点:
我们曾经遇到过因为NameServer配置不当导致的问题:某次运维人员只配置了一个NameServer地址,当该NameServer维护时,整个消息系统不可用。后来我们制定了严格的检查清单,确保:
RocketMQ的存储设计是其高性能的核心所在。理解这部分原理,对于性能调优和问题排查至关重要。
CommitLog是消息的物理存储文件,设计特点包括:
这种设计的优势非常明显:
ConsumeQueue是逻辑消费队列,其结构如下:
| 偏移量(8B) | 大小(4B) | 消息Tag哈希值(8B) |
|---|---|---|
| 记录消息在CommitLog的物理偏移 | 消息长度 | 用于Tag过滤 |
每个ConsumeQueue文件保存30万条记录,固定大小约5.72MB(20B * 300,000)。
这种设计的精妙之处在于:
RocketMQ提供两种刷盘方式,适用于不同场景:
properties复制# broker.conf配置
flushDiskType=SYNC_FLUSH
特点:
适用场景:
properties复制# broker.conf配置
flushDiskType=ASYNC_FLUSH
特点:
适用场景:
经验分享:我们核心交易系统使用同步刷盘+同步复制,虽然性能有所下降,但在多次机房断电情况下,没有丢失一条交易消息,值得这个性能代价。
RocketMQ的主从复制支持两种模式,选择取决于业务需求:
properties复制brokerRole=SYNC_MASTER
特点:
properties复制brokerRole=ASYNC_MASTER
特点:
配置建议:
合理配置生产者可以显著提高系统性能和可靠性。以下是一些关键配置项:
java复制DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
// 设置NameServer地址
producer.setNamesrvAddr("name-server1:9876;name-server2:9876");
// 发送超时时间(毫秒)
producer.setSendMsgTimeout(3000);
// 失败重试次数
producer.setRetryTimesWhenSendFailed(2);
// 异步发送时队列深度
producer.setAsyncQueueSize(5000);
// 压缩消息阈值(默认4KB)
producer.setCompressMsgBodyOverHowmuch(4096);
配置建议:
RocketMQ提供三种发送模式,各有适用场景:
java复制try {
SendResult result = producer.send(message);
System.out.println("消息发送成功:" + result);
} catch (Exception e) {
// 重试或记录错误
}
特点:
java复制producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 处理成功
}
@Override
public void onException(Throwable e) {
// 处理失败
}
});
特点:
java复制producer.sendOneway(message);
特点:
踩坑记录:我们曾在一个促销活动中全部使用单向发送,结果网络抖动导致大量消息丢失。后来调整为关键路径同步发送,非关键路径异步发送,找到了可靠性和性能的平衡点。
对于高吞吐场景,批量发送可以显著提高性能:
java复制List<Message> messages = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
messages.add(new Message("TopicTest", "TagA", "Key" + i, ("Hello"+i).getBytes()));
}
// 批量发送
SendResult result = producer.send(messages);
最佳实践:
RocketMQ提供两种消费者实现,根据业务需求选择:
java复制DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.subscribe("TopicTest", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 处理消息
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
特点:
java复制DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("PullConsumerGroup");
consumer.subscribe("TopicTest", "*");
consumer.start();
while (true) {
List<MessageExt> messages = consumer.poll(100);
if (!messages.isEmpty()) {
// 处理消息
}
}
特点:
根据业务需求选择合适的消费模式:
java复制consumer.setMessageModel(MessageModel.CLUSTERING);
特点:
java复制consumer.setMessageModel(MessageModel.BROADCASTING);
特点:
注意事项:广播模式下,消费进度保存在客户端,需要确保磁盘可靠。我们曾遇到广播模式消费者磁盘损坏导致进度丢失的问题,后来增加了进度备份机制。
合理配置并发参数可以提高消费能力:
java复制// 最小消费线程数
consumer.setConsumeThreadMin(20);
// 最大消费线程数
consumer.setConsumeThreadMax(64);
// 单次拉取消息数
consumer.setPullBatchSize(32);
// 单次消费消息数
consumer.setConsumeMessageBatchMaxSize(10);
调优建议:
RocketMQ支持两种顺序消息:
全局顺序:
分区顺序:
java复制// 订单ID作为分片键,确保同一订单的消息进入同一队列
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long orderId = (Long) arg;
long index = orderId % mqs.size();
return mqs.get((int) index);
}
}, orderId);
java复制consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
// 顺序处理消息
for (MessageExt msg : msgs) {
try {
processOrderMessage(msg);
} catch (Exception e) {
// 返回SUSPEND_CURRENT_QUEUE_A_MOMENT会暂停当前队列
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
实战经验:我们在订单状态流转中使用顺序消息,最初队列数设置太少导致消费延迟。后来根据业务量调整为16个队列,每个队列处理不同订单号段,既保证了顺序又提高了并发。
RocketMQ事务消息采用两阶段提交设计:
第一阶段:发送half消息
第二阶段:执行本地事务
状态检查(补偿机制)
java复制// 1. 创建事务生产者
TransactionMQProducer producer = new TransactionMQProducer("TransactionProducerGroup");
producer.setNamesrvAddr("name-server:9876");
// 2. 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 执行本地事务
boolean success = doBusinessTransaction();
return success ? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.ROLLBACK_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.UNKNOW;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 检查本地事务状态
return checkBusinessStatus() ? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.ROLLBACK_MESSAGE;
}
});
// 3. 发送事务消息
TransactionSendResult result = producer.sendMessageInTransaction(message, null);
transactionTimeout参数调整踩坑记录:我们曾遇到事务消息堆积问题,原因是本地事务执行慢导致大量消息处于UNKNOW状态。后来优化了本地事务性能,并增加了监控告警,问题得到解决。
RocketMQ提供18个固定延迟级别:
| 级别 | 延迟时间 | 级别 | 延迟时间 |
|---|---|---|---|
| 1 | 1s | 10 | 6m |
| 2 | 5s | 11 | 7m |
| 3 | 10s | 12 | 8m |
| 4 | 30s | 13 | 9m |
| 5 | 1m | 14 | 10m |
| 6 | 2m | 15 | 20m |
| 7 | 3m | 16 | 30m |
| 8 | 4m | 17 | 1h |
| 9 | 5m | 18 | 2h |
使用方法:
java复制message.setDelayTimeLevel(3); // 延迟10秒
延迟消息的实现非常巧妙:
对于需要更灵活延迟时间的场景,可以采用以下方案:
定时任务+普通消息
java复制// 计算延迟时间
long delayMillis = targetTime - System.currentTimeMillis();
if (delayMillis <= 0) {
// 立即发送
producer.send(message);
} else {
// 定时任务延迟发送
scheduler.schedule(() -> producer.send(message), delayMillis, TimeUnit.MILLISECONDS);
}
Redis ZSet实现
java复制// 存储消息
jedis.zadd("delayed:messages", targetTime, messageJson);
// 定时任务扫描
Set<String> readyMessages = jedis.zrangeByScore("delayed:messages", 0, currentTime);
for (String message : readyMessages) {
producer.send(parseMessage(message));
jedis.zrem("delayed:messages", message);
}
时间轮算法
java复制// 初始化时间轮
HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 60);
// 添加延迟任务
timer.newTimeout(timeout -> producer.send(message), delay, TimeUnit.SECONDS);
经验分享:我们在订单超时取消功能中,最初使用RocketMQ的延迟消息,但因为级别不够灵活,后来改用Redis ZSet方案,可以支持任意时间的精确延迟。
Tag过滤是最常用的过滤方式,使用时需要注意:
||组合多个Tag到一条消息TagA || TagB语法*会接收所有消息,浪费带宽java复制// 生产者 - 为消息设置单个Tag
Message msg = new Message("OrderTopic", "PAY_SUCCESS", orderId, body);
// 消费者 - 订阅多个Tag
consumer.subscribe("OrderTopic", "PAY_SUCCESS || ORDER_CANCEL");
SQL过滤基于消息属性,功能更强大但性能较低:
java复制// 设置消息属性
msg.putUserProperty("amount", "100");
msg.putUserProperty("region", "east");
// 消费者使用SQL过滤
consumer.subscribe("OrderTopic",
"amount > 50 AND region IN ('east', 'north')");
支持的操作符:
>, >=, <, <=, =, <>AND, OR, NOTIN, IS NULL, BETWEEN注意事项:SQL过滤在Broker端执行,会增加CPU负担,高吞吐场景慎用。
对于复杂过滤逻辑,可以实现MessageFilter接口:
java复制public class CustomFilter implements MessageFilter {
@Override
public boolean match(MessageExt msg) {
// 自定义过滤逻辑
String body = new String(msg.getBody());
return body.contains("urgent");
}
}
// Broker配置
filterServerNums=1
consumer.setMessageFilter(new CustomFilter());
适用场景:
RocketMQ生产者默认采用轮询策略发送消息,但也支持自定义:
java复制// 自定义队列选择器
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 根据业务ID哈希选择队列
int index = Math.abs(arg.hashCode()) % mqs.size();
return mqs.get(index);
}
}, shardingKey);
高级策略:
消费者的负载均衡策略更加丰富,可以通过以下配置调整:
java复制// 设置分配策略(默认为平均分配)
consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragely());
// 其他内置策略:
// AllocateMessageQueueAveragelyByCircle 环形分配
// AllocateMessageQueueByConfig 按配置分配
// AllocateMessageQueueByMachineRoom 机房优先
再平衡过程:
问题排查:我们曾遇到消费不均问题,原因是部分消费者启动慢导致分配不均。解决方案是设置
consumeTimeout和调整rebalanceInterval。
生产环境部署建议:
集群规模:
配置优化:
properties复制# 主从配置
brokerRole=SYNC_MASTER
flushDiskType=SYNC_FLUSH
# 网络线程
sendMessageThreadPoolNums=16
pullMessageThreadPoolNums=32
# 内存映射
mappedFileSizeCommitLog=1073741824 # 1GB
mappedFileSizeConsumeQueue=300000 # 30万条
监控指标:
消息轨迹能完整记录消息生命周期,配置方法:
properties复制# broker.conf
traceTopicEnable=true
traceTopicName=RMQ_SYS_TRACE_TOPIC
轨迹数据包括:
关键监控指标及采集方法:
消息堆积:
bash复制mqadmin consumerProgress -n name-server:9876 -g ConsumerGroup
TPS统计:
bash复制mqadmin brokerStats -n name-server:9876 -b broker-ip:10911
Prometheus集成:
yaml复制# 配置RocketMQ Exporter
- job_name: 'rocketmq-exporter'
static_configs:
- targets: ['exporter:5557']
告警规则:
yaml复制# 消息堆积告警
- alert: RocketMQMsgBacklog
expr: rocketmq_consumer_diff > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "Consumer group {{ $labels.consumerGroup }} has backlog"
常用运维命令速查:
| 命令 | 用途 | 示例 |
|---|---|---|
| clusterList | 查看集群状态 | mqadmin clusterList -n ns-ip:9876 |
| topicStatus | 查看Topic状态 | mqadmin topicStatus -n ns-ip:9876 -t TopicTest |
| consumerProgress | 消费进度 | mqadmin consumerProgress -n ns-ip:9876 -g ConsumerGroup |
| queryMsgById | 根据MsgId查询 | mqadmin queryMsgById -n ns-ip:9876 -i 0A9A003F00002A9F |
| sendMsgStatus | 发送测试消息 | mqadmin sendMsgStatus -n ns-ip:9876 -t TopicTest -p "test" |
可能原因:
解决方案:
生产者:
java复制// 同步发送+重试
producer.setRetryTimesWhenSendFailed(3);
SendResult result = producer.send(msg);
Broker:
properties复制# 同步刷盘+同步复制
flushDiskType=SYNC_FLUSH
brokerRole=SYNC_MASTER
消费者:
java复制// 正确处理消费失败
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
保证消息幂等的常见方法:
唯一ID+去重表:
sql复制CREATE TABLE msg_idempotent (
msg_id VARCHAR(64) PRIMARY KEY,
status TINYINT,
created_time DATETIME
);
业务状态检查:
java复制Order order = orderDao.get(orderId);
if (order.getStatus() == OrderStatus.PAID) {
return; // 已处理
}
Redis原子操作:
java复制String key = "order:" + orderId + ":processed";
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
if (!result) {
return; // 已处理
}
处理步骤:
定位原因:
临时方案:
bash复制# 跳过堆积消息(重置offset)
mqadmin resetOffsetByTime -n ns-ip:9876 -g ConsumerGroup -t TopicTest -s now
# 扩容消费者
kubectl scale deployment consumer-deployment --replicas=10
长期方案:
RocketMQ 5.0引入基于Raft的DLEDGER模式:
properties复制# 启用DLEDGER
enableDLegerCommitLog=true
dLegerGroup=RaftNode00
dLegerPeers=n0-0:40911;n0-1:40912;n0-2:40913
dLegerSelfId=n0-0
优势:
新的Proxy架构特点:
5.0版本的消息轨迹改进:
Q1:RocketMQ如何保证高可用?
答案要点:
Q2:CommitLog和ConsumeQueue的区别?
对比分析:
| CommitLog | ConsumeQueue | |
|---|---|---|
| 存储内容 | 原始消息 | 消息索引 |
| 存储方式 | 顺序写入 | 随机写入 |
| 文件大小 | 1GB固定 | 5.72MB固定 |
| 用途 | 持久化存储 | 逻辑队列 |
Q3:顺序消息的实现原理?
技术细节:
生产者:相同业务ID选择同一队列
java复制// 使用MessageQueueSelector保证相同订单发到同一队列
producer.send(msg, (mqs, msg, arg) -> {
long orderId = (long) arg;
return mqs.get((int) (orderId % mqs.size()));
}, orderId);
Broker:单队列顺序写入
消费者:顺序消费单队列
java复制consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
// 顺序