Java深入解析篇三十三之消息队列

消息队列(Message Queue)详解

本文深入讲解消息队列核心原理与实战,涵盖 Kafka、RabbitMQ、RocketMQ 三大主流MQ,配合完整 Java 代码示例。


目录

  1. 消息队列概述与应用场景
  2. 消息模式(点对点/发布订阅)
  3. [Kafka架构(Broker/Topic/Partition/Consumer Group)](#Kafka架构(Broker/Topic/Partition/Consumer Group))
  4. Kafka生产者(acks/retries/幂等)
  5. Kafka消费者(offset管理/rebalance)
  6. RabbitMQ(Exchange类型/Queue/Binding/ACK)
  7. RocketMQ(Topic/Tag/事务消息/延迟消息)
  8. 消息可靠性保证(生产端/Broker/消费端)
  9. 顺序消息
  10. 消息幂等
  11. 死信队列
  12. 消息积压处理
  13. 事务消息
  14. [Java客户端(KafkaTemplate/Spring AMQP/RocketMQ Client)](#Java客户端(KafkaTemplate/Spring AMQP/RocketMQ Client))
  15. 消息队列选型对比
  16. 最佳实践

一、消息队列概述与应用场景

1.1 什么是消息队列

消息队列(Message Queue,MQ)是一种分布式通信中间件,用于在应用程序之间传递消息。它实现了生产者与消费者的解耦,提供异步通信、流量削峰、最终一致性等能力。

1.2 核心应用场景

场景 说明 示例
异步处理 将耗时操作异步化,提升响应速度 注册后异步发送邮件/短信
流量削峰 缓冲突发流量,保护下游系统 秒杀活动,MQ缓冲请求
系统解耦 上下游系统通过MQ通信,降低耦合 订单系统通知库存/物流/积分
数据同步 异构系统间数据同步 Canal监听binlog同步到ES
日志收集 高吞吐日志采集与传输 Filebeat → Kafka → ELK
分布式事务 跨服务最终一致性 下单扣库存扣积分

1.3 引入MQ的代价

复制代码
优点:异步、解耦、削峰
代价:
  ├── 系统复杂度增加(运维、监控)
  ├── 可用性降低(MQ宕机影响全链路)
  ├── 一致性问题(消息丢失、重复、乱序)
  └── 延迟增加(异步处理非实时)

二、消息模式(点对点/发布订阅)

2.1 点对点模式(Point-to-Point)

一条消息只被一个消费者消费,多个消费者竞争消费同一队列。

java 复制代码
// 点对点模式示意(RabbitMQ)
// 生产者发送消息到队列,只有一个消费者能收到
public class P2PDemo {
    // 生产者
    public void produce(Channel channel, String message) throws Exception {
        channel.queueDeclare("task_queue", true, false, false, null);
        channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());
    }

    // 消费者(多个实例竞争消费)
    public void consume(Channel channel) throws Exception {
        channel.basicQos(1); // 每次只预取1条,公平分发
        channel.basicConsume("task_queue", false, (consumerTag, delivery) -> {
            String msg = new String(delivery.getBody());
            System.out.println("处理任务: " + msg);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }, consumerTag -> {});
    }
}

2.2 发布订阅模式(Publish/Subscribe)

一条消息被多个消费者(订阅者)同时消费。

java 复制代码
// 发布订阅模式示意(RabbitMQ Fanout)
public class PubSubDemo {
    // 生产者发送到Exchange
    public void publish(Channel channel, String event) throws Exception {
        channel.exchangeDeclare("order_events", "fanout", true);
        channel.basicPublish("order_events", "", null, event.getBytes());
    }

    // 每个消费者绑定自己的队列到Exchange
    public void subscribe(Channel channel, String serviceName) throws Exception {
        channel.exchangeDeclare("order_events", "fanout", true);
        String queueName = channel.queueDeclare().getQueue(); // 临时队列
        channel.queueBind(queueName, "order_events", "");
        channel.basicConsume(queueName, true, (consumerTag, delivery) -> {
            System.out.println(serviceName + " 收到事件: " + new String(delivery.getBody()));
        }, consumerTag -> {});
    }
}

2.3 两种模式对比

维度 点对点 发布订阅
消费关系 1对1(竞争) 1对多(广播)
消息保留 消费后删除 各订阅者独立消费
典型实现 RabbitMQ Queue / Kafka Group RabbitMQ Fanout / Kafka多Group
适用场景 任务分发 事件通知

三、Kafka架构(Broker/Topic/Partition/Consumer Group)

3.1 整体架构

复制代码
Producer ──→ Kafka Cluster ──→ Consumer Group
              │
              ├── Broker 0 (Controller)
              │   ├── Topic-A, Partition 0 (Leader)
              │   └── Topic-B, Partition 1 (Follower)
              │
              ├── Broker 1
              │   ├── Topic-A, Partition 1 (Leader)
              │   └── Topic-A, Partition 0 (Follower)
              │
              └── Broker 2
                  ├── Topic-B, Partition 0 (Leader)
                  └── Topic-A, Partition 1 (Follower)

3.2 核心组件

组件 职责
Broker Kafka服务节点,负责消息存储与转发
Controller 集群控制器,管理分区Leader选举
Topic 消息的逻辑分类
Partition Topic的物理分片,并行度的基本单位
Replica 分区副本(Leader + Follower)
ISR In-Sync Replicas,与Leader保持同步的副本集
Consumer Group 消费者组,组内竞争消费,组间广播
Offset 消息在分区内的唯一编号,消费位点

3.3 Partition 详解

java 复制代码
// Partition 是 Kafka 并行处理的核心
// 一个 Topic 可以有多个 Partition,分布在不同 Broker 上
// 关键特性:
// 1. 分区内消息有序(追加写入)
// 2. 分区是并行消费的最小单位
// 3. 一个分区在同一时刻只能被组内一个消费者消费

// 分区数设置建议:
// partition数 = max(期望吞吐量/单分区吞吐量, 消费者实例数)

3.4 Consumer Group 工作机制

java 复制代码
// Consumer Group 核心规则:
// 1. 同一 Group 内,一个 Partition 只能被一个 Consumer 消费
// 2. 不同 Group 之间,各自独立消费(广播效果)
// 3. Consumer 数量 > Partition 数量时,多余的 Consumer 空闲

// 示例:Topic有3个分区,Group有2个消费者
// Partition 0 → Consumer A
// Partition 1 → Consumer A
// Partition 2 → Consumer B

四、Kafka生产者(acks/retries/幂等)

4.1 acks 确认机制

acks值 含义 可靠性 性能
0 不等待任何确认 最低(可能丢消息) 最高
1 Leader写入即确认 中等(Leader宕机可能丢) 中等
-1/all ISR全部写入确认 最高 最低

4.2 生产者配置与代码

java 复制代码
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class KafkaProducerExample {

    public static KafkaProducer<String, String> createProducer() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // === 可靠性配置 ===
        props.put(ProducerConfig.ACKS_CONFIG, "all");           // ISR全部确认
        props.put(ProducerConfig.RETRIES_CONFIG, 3);            // 失败重试3次
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100); // 重试间隔

        // === 幂等性配置 ===
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);       // 开启幂等
        props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5); // 幂等下最大5

        // === 性能配置 ===
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);     // 批量大小16KB
        props.put(ProducerConfig.LINGER_MS_CONFIG, 5);          // 等待5ms凑批
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // 缓冲区32MB

        return new KafkaProducer<>(props);
    }

    public static void main(String[] args) {
        KafkaProducer<String, String> producer = createProducer();
        try {
            for (int i = 0; i < 100; i++) {
                ProducerRecord<String, String> record = new ProducerRecord<>(
                    "order-topic",           // topic
                    "order-key-" + i % 10,   // key(决定分区)
                    "{\"orderId\":" + i + "}" // value
                );

                // 异步发送 + 回调
                producer.send(record, (metadata, exception) -> {
                    if (exception != null) {
                        System.err.println("发送失败: " + exception.getMessage());
                    } else {
                        System.out.printf("发送成功: partition=%d, offset=%d%n",
                            metadata.partition(), metadata.offset());
                    }
                });
            }
            producer.flush(); // 确保所有消息发送完成
        } finally {
            producer.close();
        }
    }
}

4.3 幂等生产者原理

复制代码
幂等性(Idempotent Producer):
  开启 enable.idempotence=true 后:
  ├── Broker 为每个 Producer 分配 PID(Producer ID)
  ├── 每条消息携带 SequenceNumber(按分区递增)
  ├── Broker 端去重:相同 PID + Partition + SeqNum 只写入一次
  └── 保证:单分区、单会话内 Exactly-Once

局限性:
  ├── 仅限单分区(跨分区需事务)
  └── 仅限单会话(Producer重启后PID变化)

五、Kafka消费者(offset管理/rebalance)

5.1 消费者代码示例

java 复制代码
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

public class KafkaConsumerExample {

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service-group");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // === Offset 管理 ===
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);  // 关闭自动提交
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); // 无offset时从头消费

        // === Rebalance 配置 ===
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 30000);
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 10000);
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 300000);
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Collections.singletonList("order-topic"));

        try {
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
                for (ConsumerRecord<String, String> record : records) {
                    try {
                        // 业务处理
                        processOrder(record.value());

                        // 手动提交offset(逐条)
                        consumer.commitSync();
                    } catch (Exception e) {
                        System.err.println("消费失败: " + e.getMessage());
                        // 可选:记录到死信Topic / 重试
                    }
                }
            }
        } finally {
            consumer.close();
        }
    }

    private static void processOrder(String orderJson) {
        System.out.println("处理订单: " + orderJson);
    }
}

5.2 Offset 提交策略对比

策略 配置/方法 优点 缺点
自动提交 enable.auto.commit=true 简单 可能重复消费/丢失
同步手动 commitSync() 可靠 阻塞,性能低
异步手动 commitAsync() 性能好 失败不重试
混合策略 异步+关闭时同步 兼顾性能与可靠 稍复杂

5.3 Rebalance 机制

复制代码
触发条件:
  ├── 消费者加入/离开 Group
  ├── 消费者被踢出(超过 max.poll.interval.ms 未poll)
  ├── Topic 分区数变化
  └── 订阅的 Topic 列表变化

分配策略:
  ├── Range(默认):按Topic范围分配,可能不均匀
  ├── RoundRobin:轮询分配,较均匀
  ├── Sticky:尽量保持上次分配,减少迁移
  └── CooperativeSticky(推荐):增量式Rebalance,避免STW

Rebalance 影响:
  ├── STW(Stop The World):所有消费者暂停
  ├── 重复消费:offset未提交的消息被重新分配
  └── 解决:减少Rebalance频率 + 消费幂等

六、RabbitMQ(Exchange类型/Queue/Binding/ACK)

6.1 Exchange 类型详解

类型 路由规则 场景
Direct routing key 精确匹配 点对点,日志级别分发
Fanout 广播到所有绑定队列 事件通知,系统广播
Topic routing key 模式匹配(*一个词,#多个词) 多维度路由
Headers 消息头属性匹配 复杂条件匹配(少用)

6.2 Spring AMQP 完整示例

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

// === 配置类:声明 Exchange、Queue、Binding ===
@Configuration
public class RabbitMQConfig {

    public static final String ORDER_EXCHANGE = "order.exchange";
    public static final String ORDER_QUEUE = "order.queue";
    public static final String ORDER_ROUTING_KEY = "order.create";

    // 死信交换机和队列
    public static final String DLX_EXCHANGE = "order.dlx.exchange";
    public static final String DLX_QUEUE = "order.dlx.queue";

    @Bean
    public DirectExchange orderExchange() {
        return ExchangeBuilder.directExchange(ORDER_EXCHANGE)
                .durable(true)  // 持久化
                .build();
    }

    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable(ORDER_QUEUE)
                .withArgument("x-dead-letter-exchange", DLX_EXCHANGE)    // 死信交换机
                .withArgument("x-dead-letter-routing-key", "dlx.order") // 死信路由键
                .withArgument("x-message-ttl", 60000)                   // TTL 60秒
                .build();
    }

    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(ORDER_ROUTING_KEY);
    }

    // 死信队列配置
    @Bean
    public DirectExchange dlxExchange() {
        return ExchangeBuilder.directExchange(DLX_EXCHANGE).durable(true).build();
    }

    @Bean
    public Queue dlxQueue() {
        return QueueBuilder.durable(DLX_QUEUE).build();
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx.order");
    }
}

// === 生产者 ===
@Service
public class OrderMessageProducer {

    private final RabbitTemplate rabbitTemplate;

    public OrderMessageProducer(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        // 开启 Publisher Confirm
        this.rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                System.out.println("消息确认到达Broker: " + correlationData);
            } else {
                System.err.println("消息未到达Broker: " + cause);
                // 重试或记录
            }
        });
        // 开启 Return(消息无法路由到队列时回调)
        this.rabbitTemplate.setReturnsCallback(returned -> {
            System.err.println("消息被退回: " + returned.getMessage());
        });
        this.rabbitTemplate.setMandatory(true);
    }

    public void sendOrderMessage(String orderId) {
        rabbitTemplate.convertAndSend(
            RabbitMQConfig.ORDER_EXCHANGE,
            RabbitMQConfig.ORDER_ROUTING_KEY,
            orderId,
            message -> {
                // 消息持久化(默认已开启)
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return message;
            }
        );
    }
}

// === 消费者 ===
@Service
public class OrderMessageConsumer {

    @RabbitListener(queues = RabbitMQConfig.ORDER_QUEUE)
    public void handleOrderMessage(String orderId, Channel channel,
                                   @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
        try {
            // 业务处理
            System.out.println("处理订单: " + orderId);
            // 手动ACK
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 拒绝消息,不重回队列(进入死信队列)
            channel.basicNack(deliveryTag, false, false);
        }
    }

    // 死信队列消费者
    @RabbitListener(queues = RabbitMQConfig.DLX_QUEUE)
    public void handleDeadLetter(String orderId) {
        System.err.println("死信消息,需人工处理: " + orderId);
        // 告警、记录、人工介入
    }
}

6.3 ACK 机制

复制代码
ACK 流程:
  ├── autoAck=true:消息投递即确认(不安全,消费失败则丢失)
  └── autoAck=false(推荐):
      ├── basicAck(tag, multiple)    → 确认消费成功
      ├── basicNack(tag, multiple, requeue) → 拒绝
      │   ├── requeue=true  → 重回队列(可能死循环)
      │   └── requeue=false → 进入死信队列
      └── basicReject(tag, requeue) → 拒绝单条

注意事项:
  ├── 必须处理所有消息(ack/nack),否则Channel阻塞
  ├── 避免无限requeue(设置重试次数)
  └── 批量ack提升性能:basicAck(tag, multiple=true)

七、RocketMQ(Topic/Tag/事务消息/延迟消息)

7.1 核心概念

概念 说明
NameServer 轻量注册中心,管理Topic路由
Broker 消息存储与转发(Master/Slave)
Topic 消息一级分类
Tag 消息二级分类(过滤)
MessageQueue Topic下的分区(默认4个)
ConsumerGroup 消费者组(集群/广播模式)

7.2 生产者与消费者

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;

// === 生产者 ===
public class RocketMQProducerExample {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("order-producer-group");
        producer.setNamesrvAddr("localhost:9876");
        producer.setRetryTimesWhenSendFailed(3);  // 同步发送重试
        producer.setSendMsgTimeout(5000);
        producer.start();

        // 普通消息
        Message msg = new Message(
            "OrderTopic",                    // Topic
            "TagCreate",                     // Tag(二级分类)
            "OrderID_001",                   // Keys(业务唯一标识)
            "{\"orderId\":\"001\",\"amount\":99.9}".getBytes(StandardCharsets.UTF_8)
        );

        SendResult result = producer.send(msg);
        System.out.printf("发送结果: status=%s, msgId=%s, queueId=%d%n",
            result.getSendStatus(), result.getMsgId(), result.getMessageQueue().getQueueId());

        producer.shutdown();
    }
}

// === 消费者 ===
public class RocketMQConsumerExample {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-consumer-group");
        consumer.setNamesrvAddr("localhost:9876");

        // 订阅 Topic + Tag过滤(多个Tag用 || 分隔)
        consumer.subscribe("OrderTopic", "TagCreate || TagPay");

        // 集群消费(默认):组内竞争
        // consumer.setMessageModel(MessageModel.CLUSTERING);

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (var msg : msgs) {
                try {
                    String body = new String(msg.getBody(), StandardCharsets.UTF_8);
                    System.out.printf("消费消息: topic=%s, tag=%s, body=%s%n",
                        msg.getTopic(), msg.getTags(), body);
                } catch (Exception e) {
                    // 返回 RECONSUME_LATER 触发重试
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("Consumer Started.");
    }
}

7.3 延迟消息

java 复制代码
// RocketMQ 支持18个延迟级别:
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
// 对应 level: 1  2  3   4  5  6  7  8  9  10 11 12 13 14  15  16  17 18

public class DelayMessageExample {
    public static void sendDelayMessage(DefaultMQProducer producer) throws Exception {
        Message msg = new Message("DelayTopic", "延迟30分钟执行".getBytes(StandardCharsets.UTF_8));
        msg.setDelayTimeLevel(16); // 30分钟
        producer.send(msg);
    }
}

八、消息可靠性保证(生产端/Broker/消费端)

8.1 三个环节

复制代码
Producer ──[环节1]──→ Broker ──[环节2]──→ Consumer
   │                    │                    │
   发送确认             存储持久化            消费确认

8.2 各环节保障措施

环节 Kafka RabbitMQ RocketMQ
生产端 acks=all + retries + 幂等 Publisher Confirm + Return 同步发送 + 重试
Broker 副本(replication.factor≥3) + ISR 持久化 + 镜像队列 同步刷盘 + 主从同步
消费端 手动提交offset 手动ACK 返回CONSUME_SUCCESS

8.3 Kafka 可靠性配置

java 复制代码
// 生产端
props.put("acks", "all");
props.put("retries", Integer.MAX_VALUE);
props.put("enable.idempotence", true);

// Broker端(server.properties)
// replication.factor=3          # 副本数
// min.insync.replicas=2         # 最小同步副本数
// unclean.leader.election.enable=false  # 禁止非ISR副本成为Leader

// 消费端
props.put("enable.auto.commit", false);
// 业务处理成功后手动 commitSync()

8.4 RabbitMQ 可靠性配置

java 复制代码
// application.yml
spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 开启Confirm
    publisher-returns: true             # 开启Return
    listener:
      simple:
        acknowledge-mode: manual        # 手动ACK
        retry:
          enabled: true
          max-attempts: 3
          initial-interval: 1000ms

九、顺序消息

9.1 问题本质

消息队列的分区/队列机制天然导致跨分区无序。要保证顺序,核心原则:同一业务key的消息路由到同一分区,且单线程消费

9.2 Kafka 顺序消息

java 复制代码
// 生产端:相同orderId路由到同一分区
public class OrderPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        int partitionCount = cluster.partitionCountForTopic(topic);
        // 按orderId hash,保证同一订单到同一分区
        return Math.abs(key.hashCode()) % partitionCount;
    }

    @Override
    public void close() {}
    @Override
    public void configure(Map<String, ?> configs) {}
}

// 消费端:单线程处理同一分区的消息(Kafka默认保证)
// 注意:不要在消费者内部开多线程处理同一分区的消息

9.3 RocketMQ 顺序消息

java 复制代码
// 生产端:MessageQueueSelector 保证同一订单到同一队列
SendResult result = producer.send(msg, (mqs, msg, arg) -> {
    String orderId = (String) arg;
    int index = Math.abs(orderId.hashCode()) % mqs.size();
    return mqs.get(index);
}, orderId);

// 消费端:使用 MessageListenerOrderly(单线程消费同一队列)
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (var msg : msgs) {
        System.out.println("顺序消费: " + new String(msg.getBody()));
    }
    return ConsumeOrderlyStatus.SUCCESS;
});

十、消息幂等

10.1 为什么会重复

复制代码
重复消费场景:
  ├── 生产端重试:网络抖动,Broker已收到但ACK丢失,Producer重发
  ├── Rebalance:消费者处理完但未提交offset,分区被重新分配
  ├── 消费端重试:处理超时,Broker认为失败重新投递
  └── 手动重发:运维操作/补偿机制

10.2 幂等方案

java 复制代码
// 方案一:Redis去重(推荐,高性能)
@Service
public class IdempotentService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final long EXPIRE_HOURS = 24;

    public boolean isDuplicate(String messageId) {
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent("mq:idempotent:" + messageId, "1", EXPIRE_HOURS, TimeUnit.HOURS);
        return Boolean.FALSE.equals(success); // 返回true表示重复
    }
}

// 方案二:数据库去重表
// CREATE TABLE mq_dedup (
//     message_id VARCHAR(64) PRIMARY KEY,
//     consumer_group VARCHAR(64),
//     consume_time DATETIME,
//     status TINYINT
// );

// 方案三:状态机(适合订单场景)
public boolean updateOrderStatus(String orderId, int fromStatus, int toStatus) {
    // UPDATE orders SET status = #{toStatus}
    // WHERE order_id = #{orderId} AND status = #{fromStatus}
    // 状态只能单向流转,天然幂等
    int rows = orderMapper.updateStatus(orderId, fromStatus, toStatus);
    return rows > 0;
}

10.3 消费者幂等集成

java 复制代码
@RabbitListener(queues = "order.queue")
public void consume(Message message, Channel channel) throws Exception {
    String messageId = message.getMessageProperties().getMessageId();
    long deliveryTag = message.getMessageProperties().getDeliveryTag();

    // 幂等检查
    if (idempotentService.isDuplicate(messageId)) {
        System.out.println("重复消息,跳过: " + messageId);
        channel.basicAck(deliveryTag, false);
        return;
    }

    try {
        // 业务处理
        processBusiness(message);
        channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
        channel.basicNack(deliveryTag, false, false);
    }
}

十一、死信队列

11.1 死信产生条件(RabbitMQ)

复制代码
消息变为死信的三种情况:
  ├── 消息被拒绝(basicReject/basicNack)且 requeue=false
  ├── 消息TTL过期(队列级或消息级)
  └── 队列达到最大长度(x-max-length)

11.2 RabbitMQ 死信队列实现

java 复制代码
@Configuration
public class DeadLetterConfig {

    // 业务队列 → 绑定死信交换机
    @Bean
    public Queue businessQueue() {
        return QueueBuilder.durable("business.queue")
            .withArgument("x-dead-letter-exchange", "dlx.exchange")
            .withArgument("x-dead-letter-routing-key", "dlx.key")
            .withArgument("x-message-ttl", 30000)  // 30秒过期
            .withArgument("x-max-length", 10000)   // 最大长度
            .build();
    }

    // 死信交换机
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlx.exchange", true, false);
    }

    // 死信队列
    @Bean
    public Queue dlxQueue() {
        return QueueBuilder.durable("dlx.queue").build();
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx.key");
    }
}

// 死信消费者:告警 + 记录 + 人工处理
@RabbitListener(queues = "dlx.queue")
public void handleDeadLetter(Message message) {
    String body = new String(message.getBody());
    String reason = message.getMessageProperties()
        .getHeader("x-death") != null ? "rejected/expired/overflow" : "unknown";

    log.error("死信消息告警: body={}, reason={}", body, reason);
    // 1. 发送告警通知(钉钉/邮件)
    // 2. 持久化到数据库供人工排查
    // 3. 修复后可重新投递到业务队列
}

11.3 RocketMQ 死信队列

java 复制代码
// RocketMQ:消费重试16次后自动进入死信队列
// 死信Topic命名:%DLQ%ConsumerGroupName
// 需要单独订阅死信Topic进行处理

DefaultMQPushConsumer dlqConsumer = new DefaultMQPushConsumer("dlq-handler-group");
dlqConsumer.subscribe("%DLQ%order-consumer-group", "*");
dlqConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    for (var msg : msgs) {
        log.error("RocketMQ死信: msgId={}, body={}", msg.getMsgId(),
            new String(msg.getBody()));
        // 告警 + 人工处理
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
dlqConsumer.start();

十二、消息积压处理

12.1 应急处理方案

复制代码
紧急处理流程:
  ├── 第一步:快速扩容
  │   ├── 增加消费者实例数(不超过分区/队列数)
  │   ├── 增加Topic分区数(Kafka: kafka-topics --alter --partitions)
  │   └── 增加队列数(RocketMQ控制台)
  │
  ├── 第二步:临时转发(分区数不够时)
  │   ├── 新建临时Topic(分区数扩大N倍)
  │   ├── 临时消费者:只做转发,不做业务处理
  │   └── 大量消费者消费临时Topic
  │
  └── 第三步:降级非核心逻辑
      ├── 跳过日志记录、通知等非核心操作
      └── 批量入库代替逐条入库

12.2 临时转发方案代码

java 复制代码
// 临时消费者:快速转发到新Topic
@KafkaListener(topics = "original-topic", groupId = "temp-forwarder")
public void forwardToNewTopic(ConsumerRecord<String, String> record) {
    // 不做任何业务处理,直接转发
    kafkaTemplate.send("temp-topic-expanded", record.key(), record.value());
}

// 大量消费者消费临时Topic(分区数扩大10倍)
@KafkaListener(topics = "temp-topic-expanded", groupId = "real-consumer",
               concurrency = "20")  // 20个消费线程
public void realConsume(ConsumerRecord<String, String> record) {
    processBusiness(record.value());
}

12.3 预防措施

java 复制代码
// 监控积压(Kafka JMX / 命令行)
// kafka-consumer-groups.sh --describe --group order-service-group
// 关注 LAG 列(积压量)

// 告警规则:
// LAG > 10000  → 警告
// LAG > 100000 → 严重
// LAG 持续增长  → 紧急

// 消费端性能优化:
// 1. 批量消费(max.poll.records=500)
// 2. 批量入库(JDBC batch insert)
// 3. 异步处理非核心逻辑
// 4. 连接池复用(DB/Redis/HTTP)

十三、事务消息

13.1 RocketMQ 事务消息(核心)

java 复制代码
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

// === 事务消息生产者 ===
public class TransactionMessageExample {

    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer("tx-producer-group");
        producer.setNamesrvAddr("localhost:9876");

        // 设置事务监听器
        producer.setTransactionListener(new OrderTransactionListener());
        producer.start();

        // 发送事务消息
        Message msg = new Message("OrderTopic", "TagCreate",
            "{\"orderId\":\"TX001\",\"amount\":199.9}".getBytes());

        // 发送半消息 + 执行本地事务
        producer.sendMessageInTransaction(msg, "orderId=TX001");
    }
}

// === 事务监听器 ===
class OrderTransactionListener implements TransactionListener {

    /**
     * 执行本地事务(半消息发送成功后回调)
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String orderId = (String) arg;
        try {
            // 执行本地事务:创建订单 + 扣减库存
            orderService.createOrder(orderId);
            inventoryService.deductStock(orderId);
            // 本地事务成功 → 提交消息
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            // 本地事务失败 → 回滚消息
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    /**
     * 事务回查(Broker长时间未收到确认时回调)
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        String orderId = msg.getKeys();
        // 查询本地事务状态(如查数据库订单是否存在)
        boolean exists = orderService.orderExists(orderId);
        if (exists) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }
}

13.2 事务消息流程

复制代码
┌──────────┐         ┌──────────┐         ┌──────────┐
│ Producer │         │  Broker  │         │ Consumer │
└────┬─────┘         └────┬─────┘         └────┬─────┘
     │  ① 发送半消息       │                    │
     │────────────────────→│                    │
     │  ② 半消息存储成功   │                    │
     │←────────────────────│                    │
     │                     │                    │
     │ ③ 执行本地事务      │                    │
     │ (DB操作等)          │                    │
     │                     │                    │
     │  ④ Commit/Rollback │                    │
     │────────────────────→│                    │
     │                     │  ⑤ 投递消息        │
     │                     │───────────────────→│
     │                     │                    │
     │  (若④超时)          │                    │
     │  ⑥ 事务回查        │                    │
     │←────────────────────│                    │
     │  ⑦ 返回事务状态     │                    │
     │────────────────────→│                    │

13.3 Kafka 事务

java 复制代码
// Kafka 事务:跨分区原子写入 + 消费-处理-提交原子操作
Properties props = new Properties();
props.put("transactional.id", "order-tx-001"); // 唯一事务ID
props.put("enable.idempotence", true);

KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions();

try {
    producer.beginTransaction();

    // 跨分区写入(原子性)
    producer.send(new ProducerRecord<>("topic-a", "key1", "value1"));
    producer.send(new ProducerRecord<>("topic-b", "key2", "value2"));

    // 消费-处理-提交(Exactly-Once)
    producer.sendOffsetsToTransaction(offsets, consumerGroupId);

    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

十四、Java客户端(KafkaTemplate/Spring AMQP/RocketMQ Client)

14.1 Spring Kafka(KafkaTemplate)

java 复制代码
// === Maven依赖 ===
// <dependency>
//     <groupId>org.springframework.kafka</groupId>
//     <artifactId>spring-kafka</artifactId>
// </dependency>

// === application.yml ===
// spring:
//   kafka:
//     bootstrap-servers: localhost:9092
//     producer:
//       acks: all
//       retries: 3
//       key-serializer: org.apache.kafka.common.serialization.StringSerializer
//       value-serializer: org.apache.kafka.common.serialization.StringSerializer
//     consumer:
//       group-id: order-group
//       auto-offset-reset: earliest
//       enable-auto-commit: false
//       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
//       value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
//     listener:
//       ack-mode: manual_immediate

// === 生产者 ===
@Service
public class KafkaOrderProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void sendOrder(OrderDTO order) {
        CompletableFuture<SendResult<String, String>> future =
            kafkaTemplate.send("order-topic", order.getOrderId(), JSON.toJSONString(order));

        future.whenComplete((result, ex) -> {
            if (ex != null) {
                log.error("发送失败: orderId={}", order.getOrderId(), ex);
            } else {
                log.info("发送成功: partition={}, offset={}",
                    result.getRecordMetadata().partition(),
                    result.getRecordMetadata().offset());
            }
        });
    }
}

// === 消费者 ===
@Service
public class KafkaOrderConsumer {

    @KafkaListener(topics = "order-topic", groupId = "order-group", concurrency = "3")
    public void consume(ConsumerRecord<String, String> record, Acknowledgment ack) {
        try {
            OrderDTO order = JSON.parseObject(record.value(), OrderDTO.class);
            log.info("消费订单: {}", order.getOrderId());
            // 业务处理...
            ack.acknowledge(); // 手动提交offset
        } catch (Exception e) {
            log.error("消费异常", e);
            // 不ack → 消息会被重新投递(需幂等)
        }
    }
}

14.2 Spring AMQP(RabbitMQ)

java 复制代码
// === Maven依赖 ===
// <dependency>
//     <groupId>org.springframework.boot</groupId>
//     <artifactId>spring-boot-starter-amqp</artifactId>
// </dependency>

// === 生产者(使用RabbitTemplate)===
@Service
public class RabbitOrderProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendOrder(OrderDTO order) {
        rabbitTemplate.convertAndSend(
            "order.exchange",
            "order.create",
            order,
            message -> {
                message.getMessageProperties().setMessageId(UUID.randomUUID().toString());
                message.getMessageProperties().setTimestamp(new Date());
                return message;
            }
        );
    }
}

// === 消费者(注解方式)===
@Service
public class RabbitOrderConsumer {

    @RabbitListener(queues = "order.queue", concurrency = "3-5")
    public void consume(OrderDTO order, Channel channel,
                        @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        try {
            log.info("处理订单: {}", order.getOrderId());
            channel.basicAck(tag, false);
        } catch (Exception e) {
            channel.basicNack(tag, false, false); // 进入死信队列
        }
    }
}

14.3 RocketMQ Spring Boot Starter

java 复制代码
// === Maven依赖 ===
// <dependency>
//     <groupId>org.apache.rocketmq</groupId>
//     <artifactId>rocketmq-spring-boot-starter</artifactId>
//     <version>2.2.3</version>
// </dependency>

// === application.yml ===
// rocketmq:
//   name-server: localhost:9876
//   producer:
//     group: order-producer-group
//     send-message-timeout: 5000
//     retry-times-when-send-failed: 3

// === 生产者 ===
@Service
public class RocketMQOrderProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void sendOrder(OrderDTO order) {
        // 同步发送
        SendResult result = rocketMQTemplate.syncSend(
            "OrderTopic:TagCreate",  // Topic:Tag
            MessageBuilder.withPayload(order)
                .setHeader("KEYS", order.getOrderId())
                .build()
        );
        log.info("发送结果: {}", result.getSendStatus());
    }

    public void sendDelayOrder(OrderDTO order) {
        // 延迟消息(level 3 = 10秒)
        rocketMQTemplate.syncSend("OrderTopic:TagDelay",
            MessageBuilder.withPayload(order).build(),
            5000,  // 超时
            3);    // 延迟级别
    }
}

// === 消费者 ===
@Service
@RocketMQMessageListener(
    topic = "OrderTopic",
    selectorExpression = "TagCreate || TagPay",  // Tag过滤
    consumerGroup = "order-consumer-group",
    consumeMode = ConsumeMode.CONCURRENTLY
)
public class RocketMQOrderConsumer implements RocketMQListener<OrderDTO> {

    @Override
    public void onMessage(OrderDTO order) {
        log.info("消费订单: {}", order.getOrderId());
        // 抛出异常会触发重试
    }
}

十五、消息队列选型对比

15.1 综合对比表

维度 Kafka RabbitMQ RocketMQ
吞吐量 百万级/s 万级/s 十万级/s
延迟 ms级(批量) μs级 ms级
可靠性 非常高
事务消息 支持 不原生支持 原生支持
延迟消息 不支持 TTL+DLX 18个级别
顺序消息 分区内有序 单队列有序 队列内有序
消息回溯 支持 不支持 支持
消息过滤 不支持(消费端) routing key Tag + SQL
协议 自定义 AMQP/MQTT 自定义
开发语言 Scala/Java Erlang Java
社区生态 极活跃 活跃 国内活跃
运维难度 中(依赖ZK/KRaft)

15.2 选型建议

复制代码
选 Kafka:
  ├── 日志收集(ELK/EFK)
  ├── 大数据/流处理(Flink/Spark)
  ├── 高吞吐场景(百万级TPS)
  └── 消息回溯需求

选 RabbitMQ:
  ├── 企业级应用集成
  ├── 复杂路由需求(多种Exchange)
  ├── 中小规模系统(万级TPS)
  └── 需要AMQP协议兼容

选 RocketMQ:
  ├── 电商/金融(事务消息)
  ├── 延迟消息需求
  ├── 国内生态(阿里系)
  └── 需要消息过滤(Tag)

十六、最佳实践

16.1 生产端最佳实践

java 复制代码
// 1. 必须设置key(用于分区路由和消息追踪)
ProducerRecord<String, String> record = new ProducerRecord<>(
    "topic", orderId, payload);  // key = orderId

// 2. 异步发送 + 回调处理失败
producer.send(record, (metadata, ex) -> {
    if (ex != null) {
        // 记录失败消息到本地表,定时补偿重发
        failedMessageService.save(record, ex);
    }
});

// 3. 批量发送提升吞吐
props.put("batch.size", 32768);   // 32KB
props.put("linger.ms", 10);       // 等待10ms凑批

// 4. 消息体不要过大(建议 < 1MB)
// 大消息考虑压缩或存OSS只传URL

16.2 消费端最佳实践

java 复制代码
// 1. 手动提交offset(业务成功后)
// 2. 消费幂等(必须!)
// 3. 合理设置并发度
@KafkaListener(topics = "order-topic", concurrency = "6")  // 6个消费线程

// 4. 批量消费提升性能
@KafkaListener(topics = "order-topic", batch = "true")
public void batchConsume(List<ConsumerRecord<String, String>> records) {
    List<OrderDTO> orders = records.stream()
        .map(r -> JSON.parseObject(r.value(), OrderDTO.class))
        .collect(Collectors.toList());
    // 批量入库
    orderService.batchInsert(orders);
}

// 5. 消费失败处理策略
// 重试N次 → 死信队列 → 告警 → 人工处理
// 不要无限重试!

16.3 运维最佳实践

复制代码
1. 监控指标:
   ├── 消费积压(LAG)
   ├── 生产/消费TPS
   ├── Broker磁盘使用率
   ├── 网络IO
   └── 消费者Rebalance次数

2. 容量规划:
   ├── 分区数 = 消费者数 × 每消费者分区数
   ├── 副本数 ≥ 3
   ├── 磁盘保留策略(时间/大小)
   └── 预留30%容量buffer

3. 高可用:
   ├── Broker集群 ≥ 3节点
   ├── 跨机架/可用区部署
   ├── min.insync.replicas = 2
   └── 定期演练故障切换

4. 安全:
   ├── 认证(SASL/SSL)
   ├── 授权(ACL)
   ├── 消息加密(敏感数据)
   └── 网络隔离

16.4 常见踩坑总结

问题 原因 解决方案
消息丢失 acks=0/自动ACK acks=all + 手动ACK
重复消费 Rebalance/重试 幂等(去重表/Redis)
消息乱序 多分区/多线程 同key同分区 + 单线程消费
消费积压 消费慢/消费者少 扩容 + 批量 + 降级
Rebalance频繁 处理超时/心跳超时 调大max.poll.interval
内存溢出 无界队列/消息过大 有界队列 + 消息压缩
死循环重试 消费异常无限requeue 限制重试次数 + 死信队列
相关推荐
码匠许师傅2 小时前
【C++ 面试真题】聊聊 C++ 的序列容器
java·c++·面试
IT爱学堂2 小时前
尚硅谷Java+AI大模型应用开发革新版本 2025年3月
java·开发语言·人工智能
C++ 老炮儿的技术栈2 小时前
Qt5.9.1 Windows 完整开发环境搭建流程
开发语言·c++·windows·qt·编辑器·代码化
xcLeigh2 小时前
Go入门:无类型常量与类型常量的区别
服务器·开发语言·golang
泡沫冰@2 小时前
GO 语言基础
开发语言·算法·golang
不爱编程的小九九2 小时前
小九源码-springboot004-springboot智能阅读推荐系统
java·spring boot·后端
键盘会跳舞3 小时前
C++:std::tuple 源码级深度拆解——变参模板、SFINAE与模板元编程核心技巧
开发语言·c++·sfinae·变参模板
东华万里3 小时前
第40篇C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·面试·大学生专区