面试官问:”说说RocketMQ的rebalance机制“

70 阅读16分钟

开场白(展现理解深度)

"Rebalance(重平衡)是RocketMQ中非常核心的机制,它保证了消费者组中的每个消费者能够公平地消费队列。我会从它是什么、为什么需要、什么时候触发、怎么分配、有什么问题这五个方面来回答。"


一、什么是Rebalance?(定义)

1.1 核心概念

Rebalance(重平衡) 是指:当消费者组中消费者数量发生变化、或者Topic的队列数发生变化时,重新分配消费者与消息队列的对应关系。

举个例子:
Topic: order_topic (8个队列: q0-q7)
ConsumerGroup: order_group

初始状态:2个消费者
Consumer1 消费: q0, q1, q2, q3
Consumer2 消费: q4, q5, q6, q7

新增Consumer3后触发Rebalance:
Consumer1 消费: q0, q1, q2
Consumer2 消费: q3, q4, q5
Consumer3 消费: q6, q7

1.2 为什么需要Rebalance?

核心目标:负载均衡

场景不Rebalance的问题Rebalance解决
新增消费者新消费者闲置,旧消费者过载重新分配,负载均衡
消费者下线部分队列无人消费,消息积压其他消费者接管队列
扩容队列新队列无人消费分配给现有消费者

二、Rebalance触发时机(关键)

2.1 四种触发场景

// 1. 消费者数量变化(最常见)
// 场景1:新消费者启动加入消费组
Consumer consumer = new DefaultMQPushConsumer("order_group");
consumer.start();  // 触发Rebalance

// 场景2:消费者宕机或主动关闭
consumer.shutdown();  // 触发Rebalance

// 场景3:消费者心跳超时(默认120秒)
// Broker检测到消费者30秒未发心跳,认为下线,触发Rebalance

// 2. 订阅的Topic队列数变化(较少见)
// 扩容队列:从8个扩到16个
mqAdmin.createTopic("order_topic", 16);  // 触发Rebalance

// 3. 消费者重启(本质还是数量变化)
// 先下线 → Rebalance → 再上线 → 再Rebalance

// 4. 定期触发(兜底机制)
// 消费者每20秒会主动触发一次Rebalance(检查是否需要调整)
private static final long REBALANCE_INTERVAL = 20 * 1000;

2.2 触发流程

时间线:
┌─────────────────────────────────────────────────┐
 1. 消费者变化 (如Consumer3上线)                  
└────────────────┬────────────────────────────────┘
                 
┌─────────────────────────────────────────────────┐
 2. Consumer3向Broker发送心跳                     
    携带: ConsumerGroup、订阅的Topic、消费模式等   
└────────────────┬────────────────────────────────┘
                 
┌─────────────────────────────────────────────────┐
 3. Broker更新消费者列表                          
    order_group: [Consumer1, Consumer2, Consumer3]
└────────────────┬────────────────────────────────┘
                 
┌─────────────────────────────────────────────────┐
 4. Broker通知所有消费者: "消费者列表变了"         
    (通过心跳响应返回最新的Consumer列表)           
└────────────────┬────────────────────────────────┘
                 
┌─────────────────────────────────────────────────┐
 5. 每个消费者各自计算分配结果                     
    Consumer1: 我应该消费 q0, q1, q2              
    Consumer2: 我应该消费 q3, q4, q5              
    Consumer3: 我应该消费 q6, q7                  
└────────────────┬────────────────────────────────┘
                 
┌─────────────────────────────────────────────────┐
 6. 停止旧队列,启动新队列消费                     
    Consumer1停止q3消费,继续q0、q1、q2          
└─────────────────────────────────────────────────┘

三、Rebalance分配策略(算法)

3.1 六种分配策略

RocketMQ提供了6种分配策略,默认使用平均分配策略

策略1:平均分配(AllocateMessageQueueAveragely)- 默认

特点: 尽量平均分配,多余的队列分配给前面的消费者。

// 示例:8个队列,3个消费者
// 8 ÷ 3 = 2余2,每人至少2个,余下2个给前2个消费者

Consumer1: q0, q1, q2  (3个)
Consumer2: q3, q4, q5  (3个)
Consumer3: q6, q7      (2个)

// 算法逻辑:
队列总数 = 8
消费者总数 = 3
平均每人 = 8 / 3 = 2 (向下取整)
余数 = 8 % 3 = 2

for (int i = 0; i < 消费者总数; i++) {
    int startIndex = i * 平均每人 + Math.min(i, 余数);
    int size = 平均每人 + (i < 余数 ? 1 : 0);
    // 分配队列...
}

优点: 分配相对均衡
缺点: 可能导致部分消费者多一个队列

策略2:平均轮询分配(AllocateMessageQueueAveragelyByCircle)

特点: 轮流分配,像发牌一样。

// 示例:8个队列,3个消费者

Consumer1: q0, q3, q6
Consumer2: q1, q4, q7
Consumer3: q2, q5

// 算法逻辑:
for (int i = 0; i < 队列总数; i++) {
    int consumerIndex = i % 消费者总数;
    分配队列[i]给消费者[consumerIndex];
}

优点: 分配最均匀
缺点: 队列分散,可能影响顺序消费

策略3:一致性Hash(AllocateMessageQueueConsistentHash)

特点: 使用一致性Hash算法,减少Rebalance时的队列迁移。

// 场景:适合消费者频繁上下线的情况
// 优点:新增/下线消费者只影响邻近节点的队列分配
// 缺点:可能分配不均

Consumer1: q0, q1, q5
Consumer2: q2, q6
Consumer3: q3, q4, q7

策略4:配置分配(AllocateMessageQueueByConfig)

特点: 手动指定哪个消费者消费哪些队列。

consumer.setAllocateMessageQueueStrategy(
    new AllocateMessageQueueByConfig(
        Arrays.asList(
            new MessageQueue("order_topic", "broker-a", 0),
            new MessageQueue("order_topic", "broker-a", 1)
        )
    )
);

使用场景: 特殊业务需求,需要固定分配

策略5:机房就近分配(AllocateMessageQueueByMachineRoom)

特点: 优先分配同机房的队列,减少跨机房流量。

// 场景:多机房部署
机房A的消费者 → 优先消费机房A的队列
机房B的消费者 → 优先消费机房B的队列

策略6:按Broker平均分配(AllocateMachineRoomNearby)

特点: 先按Broker分配,再在Broker内平均分配。

// 适合多Broker集群
Broker-A: q0-q3
Broker-B: q4-q7

Consumer1: Broker-A的所有队列
Consumer2: Broker-B的所有队列

3.2 如何选择策略?

场景推荐策略原因
普通场景平均分配(默认)简单、够用
顺序消费平均分配保证队列连续性
消费者频繁变化一致性Hash减少队列迁移
多机房部署机房就近减少跨机房流量
特殊需求配置分配完全可控

四、Rebalance的执行过程(源码级)

4.1 消费者端流程

// RebalanceService.java - 核心类
public void run() {
    while (!this.isStopped()) {
        // 每20秒执行一次Rebalance
        this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();
    }
}

// MQClientInstance.java
public void doRebalance() {
    for (Map.Entry<String, MQConsumerInner> entry : 
         this.consumerTable.entrySet()) {
        MQConsumerInner consumer = entry.getValue();
        consumer.doRebalance();  // 每个消费者执行Rebalance
    }
}

// RebalanceImpl.java - 核心逻辑
public void doRebalance(boolean isOrder) {
    // 获取该消费者订阅的所有Topic
    Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
    
    for (Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
        String topic = entry.getKey();
        
        // 1. 从Broker获取该Topic的所有队列
        Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
        
        // 2. 从Broker获取该ConsumerGroup的所有消费者ID
        List<String> cidAll = 
            this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
        
        if (mqSet != null && cidAll != null) {
            // 3. 排序(保证所有消费者看到的顺序一致)
            List<MessageQueue> mqAll = new ArrayList<>(mqSet);
            Collections.sort(mqAll);  // 队列排序
            Collections.sort(cidAll);  // 消费者ID排序
            
            // 4. 执行分配策略
            AllocateMessageQueueStrategy strategy = 
                this.allocateMessageQueueStrategy;
            List<MessageQueue> allocateResult = 
                strategy.allocate(
                    this.consumerGroup,
                    this.mQClientFactory.getClientId(),
                    mqAll,
                    cidAll
                );
            
            // 5. 更新消费队列
            this.updateProcessQueueTableInRebalance(topic, allocateResult);
        }
    }
}

4.2 关键步骤详解

步骤1:获取队列和消费者列表

// 从Broker获取Topic的队列信息
GET /topic/route?topic=order_topic

// 响应:
{
    "brokerDatas": [
        {"brokerName": "broker-a", "brokerAddrs": {...}}
    ],
    "queueDatas": [
        {"brokerName": "broker-a", "readQueueNums": 8, "writeQueueNums": 8}
    ]
}

// 从Broker获取消费者列表
GET /consumer/list?consumerGroup=order_group&topic=order_topic

// 响应:
{
    "consumerIdList": [
        "192.168.1.100@12345",
        "192.168.1.101@12345",
        "192.168.1.102@12345"
    ]
}

步骤2:排序(非常重要!)

// 为什么要排序?
// 保证所有消费者看到的队列顺序和消费者顺序完全一致

// 队列排序
List<MessageQueue> mqAll = [q0, q7, q3, q1, q5, q2, q6, q4];
Collections.sort(mqAll);  // 排序后: [q0, q1, q2, q3, q4, q5, q6, q7]

// 消费者ID排序
List<String> cidAll = ["192.168.1.102@12345", "192.168.1.100@12345", ...];
Collections.sort(cidAll);  // 按字典序排序

// 如果不排序会怎样?
// 不同消费者可能计算出不同的分配结果,导致:
// 1. 同一个队列被多个消费者消费(重复消费)
// 2. 某些队列没有消费者消费(消息积压)

步骤3:执行分配策略

// 以平均分配为例
public List<MessageQueue> allocate(
    String consumerGroup,
    String currentCID,
    List<MessageQueue> mqAll,
    List<String> cidAll
) {
    // 找到当前消费者的索引
    int index = cidAll.indexOf(currentCID);  // 假设index=1
    
    int mod = mqAll.size() % cidAll.size();  // 8 % 3 = 2
    int averageSize = mqAll.size() / cidAll.size();  // 8 / 3 = 2
    
    // 计算起始位置和数量
    int startIndex = index * averageSize + Math.min(index, mod);
    // index=1: 1*2 + min(1,2) = 2+1 = 3
    
    int range = averageSize + (index < mod ? 1 : 0);
    // index=1: 2 + (1<2?1:0) = 3
    
    // 分配队列
    List<MessageQueue> result = new ArrayList<>();
    for (int i = 0; i < range; i++) {
        result.add(mqAll.get((startIndex + i) % mqAll.size()));
    }
    // Consumer2分配: q3, q4, q5
    
    return result;
}

步骤4:更新消费队列

// updateProcessQueueTableInRebalance方法
// 核心逻辑:找出变化的队列

// 旧的消费队列
Set<MessageQueue> oldQueues = [q0, q1, q2, q3];

// 新的分配结果
List<MessageQueue> newQueues = [q0, q1, q2];

// 找出需要移除的队列
Set<MessageQueue> removeQueues = [q3];  // 不在新分配中

// 找出需要新增的队列
Set<MessageQueue> addQueues = [];  // 已经都在消费了

// 移除队列
for (MessageQueue mq : removeQueues) {
    ProcessQueue pq = this.processQueueTable.remove(mq);
    if (pq != null) {
        pq.setDropped(true);  // 标记为已丢弃,停止消费
        this.removeUnnecessaryMessageQueue(mq, pq);  // 持久化消费进度
    }
}

// 新增队列
for (MessageQueue mq : addQueues) {
    ProcessQueue pq = new ProcessQueue();
    long offset = this.computePullFromWhere(mq);  // 计算从哪开始消费
    this.processQueueTable.put(mq, pq);
    this.pullMessageService.pullMessage(mq, offset);  // 开始拉取消息
}

五、Rebalance的问题与优化(重点)

5.1 问题1:重复消费

原因: Rebalance过程中,消费进度可能未及时提交。

时间轴:
T1: Consumer1消费q0的offset=100-110(共10条消息)
T2: 消息还在处理中,未提交offset
T3: Consumer1被分配到其他队列,q0分配给Consumer2
T4: Consumer2从Broker获取q0的offset=100(上次提交的位置)
T5: Consumer2重复消费100-110

结果:offset 100-110被消费了2次!

真实案例:

场景:订单支付通知消费
问题:Rebalance时,部分订单收到了2次支付成功通知

排查:
1. 日志显示Consumer1处理offset=1000时,发生Rebalance
2. Consumer2接管后,从offset=990开始消费(上次提交位置)
3. offset 990-1000之间的消息被重复处理

解决方案:

方案1:业务幂等(推荐)

@Transactional
public void handlePayNotify(PayMessage msg) {
    // 1. 先查询订单状态
    Order order = orderMapper.selectById(msg.getOrderId());
    if (order.getStatus() == OrderStatus.PAID) {
        log.info("订单已支付,忽略重复通知");
        return;  // 幂等处理
    }
    
    // 2. 更新订单状态(使用乐观锁)
    int rows = orderMapper.updateStatus(
        msg.getOrderId(), 
        OrderStatus.UNPAID,  // 旧状态
        OrderStatus.PAID     // 新状态
    );
    
    if (rows == 0) {
        log.warn("订单状态已变更,可能重复消费");
        return;
    }
    
    // 3. 业务处理...
}

// SQL - 使用乐观锁
UPDATE orders 
SET status = 2, version = version + 1
WHERE id = ? AND status = 1 AND version = ?;

方案2:消息去重表

@Transactional
public void consume(Message msg) {
    String msgId = msg.getMsgId();
    
    // 1. 先插入去重表
    try {
        msgDedupeMapper.insert(msgId);
    } catch (DuplicateKeyException e) {
        log.info("消息{}已处理,忽略", msgId);
        return;  // 重复消息
    }
    
    // 2. 业务处理
    businessService.handle(msg);
}

// 表结构
CREATE TABLE msg_dedupe (
    msg_id VARCHAR(64) PRIMARY KEY,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB;

// 定期清理(保留7天)
DELETE FROM msg_dedupe 
WHERE create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);

方案3:减少Rebalance频率

// 调整Rebalance间隔(默认20秒)
consumer.setRebalanceInterval(60000);  // 改为60秒

// 增加心跳频率(减少误判下线)
consumer.setHeartbeatBrokerInterval(3000);  // 默认30秒,改为3秒

5.2 问题2:消息丢失

原因: Rebalance时,广播模式下消费进度不持久化。

场景:广播模式(BROADCASTING)

T1: Consumer1消费q0到offset=100
T2: Consumer1宕机,消费进度未持久化到Broker
T3: Consumer1重启后,从Broker获取不到消费进度
T4: Consumer1从CONSUME_FROM_LAST_OFFSET开始(最新位置)
T5: offset 100到最新之间的消息丢失!

解决方案:

方案1:改用集群模式(推荐)

// 集群模式:消费进度持久化到Broker
consumer.setMessageModel(MessageModel.CLUSTERING);

方案2:手动持久化消费进度

// 广播模式下,自己持久化消费进度到Redis/MySQL
public void consume(MessageExt msg) {
    // 业务处理
    businessService.handle(msg);
    
    // 手动持久化消费进度
    String key = String.format("offset:%s:%s:%d", 
        msg.getTopic(), msg.getBrokerName(), msg.getQueueId());
    redis.set(key, msg.getQueueOffset());
}

// 启动时从Redis读取消费进度
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(...) {
        // 消费逻辑
    }
});

// 自定义消费进度存储
consumer.setOffsetStore(new CustomOffsetStore(redis));

5.3 问题3:消费暂停

原因: Rebalance期间,消费者停止拉取消息。

时间轴:
T1: Consumer1正常消费q0、q1、q2、q3
T2: Consumer2上线,触发Rebalance
T3: Consumer1停止所有队列消费(等待Rebalance完成)
T4: Rebalance计算分配结果(可能需要1-2秒)
T5: Consumer1恢复消费q0、q1、q2(q3分配给Consumer2)

影响:T3-T5期间,所有队列停止消费约1-2秒

真实案例:

场景:秒杀活动,订单消息消费

问题:
- 秒杀开始时,动态扩容了10个消费者
- 每扩容一个,就触发一次Rebalance
- 导致消费暂停了10次,每次1-2秒
- 总共暂停了10-20秒,订单积压严重

后果:
- 用户支付后长时间未收到确认
- 大量客诉

解决方案:

方案1:提前扩容(推荐)

// 秒杀前30分钟,提前扩容好消费者
// 避免活动期间频繁Rebalance

方案2:批量扩容

# 不要一个一个启动,批量启动
# 使用脚本批量启动,减少Rebalance次数

for i in {1..10}; do
    nohup java -jar consumer.jar --instance=$i &
    sleep 2  # 间隔2秒,让Rebalance合并
done

方案3:使用AllocateMachineRoomNearby策略

// 支持"本地优先"策略,减少Rebalance影响
consumer.setAllocateMessageQueueStrategy(
    new AllocateMachineRoomNearby(
        new AllocateMessageQueueAveragely()
    )
);

5.4 问题4:分配不均

原因: 队列数不是消费者数的倍数。

场景1:队列数少于消费者数
Topic: order_topic (2个队列)
ConsumerGroup: order_group (3个消费者)

分配结果:
Consumer1: q0
Consumer2: q1
Consumer3: 空闲(没有队列可消费)

问题:资源浪费
场景2:队列数不均匀
Topic: order_topic (7个队列)
ConsumerGroup: order_group (3个消费者)

分配结果:
Consumer1: q0, q1, q2  (3个队列,负载33.3%)
Consumer2: q3, q4      (2个队列,负载33.3%)
Consumer3: q5, q6      (2个队列,负载33.3%)

看起来均衡,但如果q0、q1、q2消息量特别大呢?
Consumer1: 负载80%
Consumer2: 负载10%
Consumer3: 负载10%

问题:负载不均

解决方案:

方案1:队列数设置为消费者数的倍数

// 推荐配置
消费者数 = 3 → 队列数 = 691215...
消费者数 = 4 → 队列数 = 8121620...
消费者数 = 5 → 队列数 = 10152025...

// 创建Topic时指定队列数
mqAdmin.createTopic(
    "order_topic",
    16  // 队列数:支持1-16个消费者
);

方案2:按消息量分队列

// 热点数据打散到多个队列
// 例如:大客户订单单独分配队列

// 发送消息时,按用户ID取模
int queueId = userId % queueNum;
producer.send(msg, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, ...) {
        return mqs.get(queueId);
    }
});

方案3:动态调整消费者数量

// 监控队列消息积压情况
Map<MessageQueue, Long> backlogs = consumer.getConsumerRunningInfo();

// 根据积压动态扩缩容
if (avgBacklog > 10000) {
    // 扩容消费者
    scaleUp();
} else if (avgBacklog < 1000) {
    // 缩容消费者
    scaleDown();
}

六、Rebalance最佳实践

6.1 设计原则

原则说明举例
队列数≥消费者数避免消费者空闲8个队列 → 最多8个消费者
队列数是2的幂便于扩容和分配4、8、16、32
提前扩容避免高峰期Rebalance秒杀前30分钟扩容
业务幂等防止重复消费订单状态检查
监控告警及时发现异常Rebalance次数、耗时

6.2 配置建议

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_group");

// 1. 心跳间隔(默认30秒,建议10秒)
consumer.setHeartbeatBrokerInterval(10000);
// 作用:更快发现消费者上下线,减少误判

// 2. 持久化消费进度间隔(默认5秒)
consumer.setPersistConsumerOffsetInterval(5000);
// 作用:减少Rebalance时的重复消费范围

// 3. 消费超时时间(默认15分钟)
consumer.setConsumeTimeout(15);
// 作用:避免消息处理过慢,导致Rebalance

// 4. 最大重试次数(默认16次)
consumer.setMaxReconsumeTimes(3);
// 作用:快速失败,避免消息堆积

// 5. 消费线程数(建议:队列数的2-4倍)
consumer.setConsumeThreadMin(20);
consumer.setConsumeThreadMax(20);
// 作用:充分利用资源,提升消费速度

// 6. 每次拉取消息数量(默认32)
consumer.setPullBatchSize(32);
// 作用:平衡网络开销和处理效率

// 7. 每次提交消费进度最大消息跨度(默认2000)
consumer.setConsumeConcurrentlyMaxSpan(2000);
// 作用:控制重复消费范围

6.3 监控指标

// 核心监控指标
public class RebalanceMonitor {
    
    // 1. Rebalance次数
    private static final Counter rebalanceCount = 
        Counter.build()
            .name("rocketmq_rebalance_total")
            .help("Rebalance次数")
            .labelNames("consumer_group")
            .register();
    
    // 2. Rebalance耗时
    private static final Histogram rebalanceDuration = 
        Histogram.build()
            .name("rocketmq_rebalance_duration_seconds")
            .help("Rebalance耗时")
            .labelNames("consumer_group")
            .register();
    
    // 3. 队列分配数量
    private static final Gauge allocatedQueues = 
        Gauge.build()
            .name("rocketmq_allocated_queues")
            .help("当前消费的队列数量")
            .labelNames("consumer_group", "consumer_id")
            .register();
    
    // 4. 消息积压
    private static final Gauge messageBacklog = 
        Gauge.build()
            .name("rocketmq_message_backlog")
            .help("消息积压数量")
            .labelNames("consumer_group", "topic", "queue_id")
            .register();
}

// 告警规则
- Rebalance次数 > 10次/分钟:消费者频繁上下线
- Rebalance耗时 > 5秒:分配算法性能问题
- 消息积压 > 10000条:消费能力不足或Rebalance过于频繁
- 队列分配不均:负载不均衡

6.4 故障排查

问题1:频繁Rebalance

# 查看消费者列表
mqadmin consumerConnection -g order_group

# 查看Rebalance日志
grep "rebalance" consumer.log | tail -100

# 常见原因:
1. 消费者频繁重启(检查应用日志)
2. 网络抖动导致心跳超时(检查网络)
3. 消费线程池满,导致心跳线程饿死(增加线程池)
4. GC时间过长,导致心跳超时(优化GC)

问题2:Rebalance后消息积压

# 查看消费进度
mqadmin consumerProgress -g order_group

# 查看每个队列的消费情况
# 常见原因:
1. Rebalance后,部分消费者分配的队列过多(调整队列数)
2. 消费速度慢(增加消费线程)
3. 消息处理逻辑耗时长(优化业务逻辑)

七、面试高频追问

Q1: "Rebalance是在Broker端做的,还是在消费者端做的?"

标准答案:

"消费者端做的。 Broker只负责维护消费者列表和队列信息,Rebalance的计算和执行都在消费者端。"

详细解释:

Broker的职责:
1. 维护ConsumerGroup中的消费者列表
2. 提供Topic的队列信息
3. 通过心跳响应,通知消费者"消费者列表有变化"

消费者的职责:
1. 从Broker获取消费者列表和队列信息
2. 本地计算分配结果(执行分配策略)
3. 停止旧队列、启动新队列消费

为什么这样设计?
- 减轻Broker压力(Broker不需要计算分配)
- 避免单点故障(Broker挂了也不影响Rebalance)
- 灵活性高(不同消费者可以有不同的分配策略)

Q2: "所有消费者的Rebalance结果一定一致吗?"

标准答案:

"理论上一致,但实际可能短暂不一致。 因为每个消费者独立计算,可能看到的消费者列表版本不同。"

详细解释:

一致的前提:
1. 所有消费者看到的队列列表相同
2. 所有消费者看到的消费者列表相同
3. 所有消费者使用相同的分配策略
4. 队列和消费者ID排序一致

可能不一致的情况:
T1: Consumer1收到心跳响应:[C1, C2]
T2: Consumer3上线
T3: Consumer2收到心跳响应:[C1, C2, C3]
T4: Consumer1和Consumer2此时Rebalance结果不一致

但很快会恢复一致:
T5: Consumer1下次心跳(20秒内)也会收到[C1, C2, C3]
T6: Consumer1重新Rebalance,结果与Consumer2一致

RocketMQ的解决方案:
- 每20秒定期Rebalance(兜底机制)
- 保证最终一致性

Q3: "Rebalance会导致消息丢失吗?"

标准答案:

"集群模式不会,广播模式可能会。"

详细解释:

集群模式(CLUSTERING):
- 消费进度持久化到Broker
- Rebalance时,新消费者从Broker读取消费进度
- 不会丢失消息,但可能重复消费

广播模式(BROADCASTING):
- 消费进度持久化到本地(默认在${user.home}/.rocketmq_offsets/)
- 如果消费者宕机,重启后本地文件丢失
- 从CONSUME_FROM_LAST_OFFSET开始,中间的消息丢失

建议:
- 生产环境优先使用集群模式
- 广播模式要自己实现消费进度持久化

Q4: "如何避免Rebalance?"

标准答案:

"无法完全避免,但可以减少频率。"

优化方法:

// 1. 提前扩容,避免高峰期Rebalance
// 秒杀活动前30分钟扩容

// 2. 增加心跳频率,减少误判
consumer.setHeartbeatBrokerInterval(10000);  // 改为10秒

// 3. 优化消费逻辑,避免超时
// 消费超时会被误认为宕机,触发Rebalance

// 4. 使用固定分配策略
consumer.setAllocateMessageQueueStrategy(
    new AllocateMessageQueueByConfig(fixedQueues)
);

// 5. 减少消费者变更
// 不要频繁重启、上下线

Q5: "Rebalance过程中,消息会重复消费吗?"

标准答案:

"会的,这是Rebalance的常见问题。"

原因:

T1: Consumer1消费offset 100-110
T2: 消息还在处理中(未提交offset)
T3: Rebalance触发,q0分配给Consumer2
T4: Consumer1停止消费,未能提交offset
T5: Consumer2从offset 100开始消费(上次提交位置)
T6: offset 100-110被重复消费

解决方案:

// 1. 业务幂等(最重要)
// 通过订单状态、版本号、去重表等方式保证幂等

// 2. 减少消息处理时间
// 快速处理,及时提交offset

// 3. 调整offset提交频率
consumer.setPersistConsumerOffsetInterval(3000);  // 改为3秒

// 4. 使用事务
// 消息处理和offset提交在同一事务中

八、实战案例(真实经历)

案例1:电商大促,Rebalance导致订单积压

背景: 双11当天凌晨0点,订单消息突然积压,从0涨到50万条,持续了5分钟。

现场排查:

# 1. 查看消费者状态
mqadmin consumerConnection -g order_group

# 发现:消费者从8个突然变成16个(扩容了8个)

# 2. 查看Rebalance日志
grep "rebalance" consumer.log

# 输出:
00:00:01 [RebalanceService] do rebalance, group=order_group
00:00:03 [RebalanceService] do rebalance, group=order_group
00:00:05 [RebalanceService] do rebalance, group=order_group
...  # 连续Rebalance了8次!

# 3. 查看消费进度
mqadmin consumerProgress -g order_group

# 发现:所有队列的消费进度停滞了约2分钟

问题原因:

扩容方式不对:
- 运维同事手动一个一个启动消费者
- 每启动一个,就触发一次Rebalance
- 8个消费者启动完,触发了8次Rebalance
- 每次Rebalance暂停消费1-2秒
- 总共暂停了8-16秒

加上:
- 当时消息发送速率:5万条/秒
- 暂停15秒 = 积压75万条消息
- 恢复消费后,需要5分钟才能消费完积压

解决方案:

# 1. 紧急止血:手动删除部分消费者,减少Rebalance
# (先让一半消费者稳定消费)

# 2. 优化扩容脚本:
#!/bin/bash
# 批量启动,减少Rebalance次数

for i in {1..8}; do
    nohup java -jar consumer.jar --instance=$i > /dev/null 2>&1 &
    if [ $((i % 4)) -eq 0 ]; then
        sleep 25  # 每4个一批,等待Rebalance完成
    fi
done

# 3. 预案:提前30分钟扩容
# 避免0点高峰时扩容

效果:

  • 积压从75万降到0,耗时从5分钟降到30秒
  • 后续大促提前扩容,再无Rebalance导致的积压

经验教训:

  1. 高峰期不要扩容,要提前扩容
  2. 扩容要批量,不要一个一个启动
  3. 监控Rebalance次数,异常时及时告警

案例2:消费者频繁重启,导致重复消费

背景: 测试环境的订单消息经常被重复消费,导致订单状态混乱。

问题排查:

# 1. 查看消费者日志
grep "重复消费" consumer.log

# 输出:
[WARN] 订单 202401010001 重复消费,当前状态=PAID
[WARN] 订单 202401010002 重复消费,当前状态=PAID
...  # 每天都有几十条

# 2. 查看Rebalance日志
grep "rebalance" consumer.log | wc -l
# 输出:500+  # 一天500多次Rebalance!

# 3. 排查原因
ps aux | grep consumer
# 发现:消费者进程每隔几分钟就重启一次

根本原因:

// 消费者代码有个定时任务
@Scheduled(cron = "0 */5 * * * ?")  // 每5分钟执行一次
public void refreshConfig() {
    // 重新加载配置
    loadConfig();
    
    // ❌ 错误:重启消费者
    consumer.shutdown();
    consumer = createConsumer();
    consumer.start();
}

// 问题:
// 1. 每5分钟重启一次消费者
// 2. 每次重启触发Rebalance
// 3. Rebalance导致重复消费

解决方案:

// 方案1:不要重启消费者,只更新配置(推荐)
@Scheduled(cron = "0 */5 * * * ?")
public void refreshConfig() {
    // 只更新配置,不重启
    loadConfig();
    // consumer不需要重启,配置自动生效
}

// 方案2:如果真的需要重启,做好幂等
@Transactional
public ConsumeOrderlyStatus consumeMessage(MessageExt msg) {
    Order order = JSON.parseObject(msg.getBody(), Order.class);
    
    // 幂等检查
    Order existOrder = orderMapper.selectById(order.getId());
    if (existOrder.getStatus() == order.getStatus()) {
        log.info("订单{}状态未变化,忽略", order.getId());
        return ConsumeOrderlyStatus.SUCCESS;
    }
    
    // 乐观锁更新
    int rows = orderMapper.updateWithVersion(order);
    if (rows == 0) {
        log.warn("订单{}更新失败,可能重复消费", order.getId());
        return ConsumeOrderlyStatus.SUCCESS;  // 幂等,返回成功
    }
    
    return ConsumeOrderlyStatus.SUCCESS;
}

效果:

  • Rebalance次数:从500次/天 → 10次/天
  • 重复消费:从几十次/天 → 0

经验总结:

  1. 不要频繁重启消费者
  2. 业务层必须做幂等
  3. 监控Rebalance次数,异常时排查

九、总结:如何满分回答

回答模板(推荐)

第一步:定义(30秒)

"Rebalance是RocketMQ的负载均衡机制,当消费者或队列数量变化时,重新分配消费者与队列的对应关系。"

第二步:触发时机(30秒)

"主要有4种触发场景:消费者上线、消费者下线、队列数变化、定期触发。其中消费者上下线最常见。"

第三步:分配策略(1分钟)

"RocketMQ提供了6种分配策略,默认是平均分配。核心思想是:所有消费者看到的队列和消费者列表必须一致,通过排序保证,然后各自独立计算分配结果。"

第四步:问题与优化(1-2分钟)

"Rebalance有3个主要问题:重复消费、消费暂停、分配不均。我们项目中遇到过XX问题,通过XX方案解决的。"

第五步:实战经验(可选,加分项)

"我们在双11期间遇到过Rebalance导致消息积压的问题,原因是扩容方式不对,后来优化了扩容脚本,并提前扩容,就没有问题了。"

关键词汇(必须提到)

  • 负载均衡
  • 队列重新分配
  • 分配策略(平均分配、一致性Hash等)
  • 重复消费
  • 幂等性
  • 消费暂停
  • 排序保证一致性

加分点

  • ✅ 能说出源码中的关键类(RebalanceServiceRebalanceImpl
  • ✅ 能画出Rebalance流程图
  • ✅ 能说出监控指标和优化方案
  • ✅ 有真实的故障排查经验

十、扩展阅读

  • RocketMQ官方文档:rocketmq.apache.org/
  • RocketMQ源码:org.apache.rocketmq.client.impl.consumer.RebalanceImpl
  • 《RocketMQ技术内幕》 - 丁威、周继锋

祝你面试顺利!记住:理解原理比背答案更重要。