Java 分布式消息队列:从原理到实战的完整指南
Java 分布式消息队列:从原理到实战的完整指南
在微服务架构盛行的今天,分布式消息队列几乎是每个中大型系统的"标配"。它像系统中的"快递中转站",让各个服务之间不再直接面对面喊话,而是通过一个可靠的中间人传递消息。这篇文章,我们就来把 Java 生态下的分布式消息队列彻底讲清楚。
一、为什么需要消息队列?
先说一个最朴素的场景:
用户下单 → 扣库存 → 生成物流单 → 发短信通知 → 加积分
如果所有步骤同步串行执行,用户点一下"提交订单"可能要等 3~5 秒。更要命的是,如果"发短信"的服务挂了,整个下单流程就失败了——但订单其实已经创建成功了。
消息队列解决的就是这类问题:
| 痛点 | 消息队列如何解决 |
|---|---|
| 耦合度过高 | 生产者和消费者通过队列解耦,互不依赖 |
| 同步阻塞响应慢 | 非核心链路异步化,主流程快速返回 |
| 流量突增打垮系统 | 削峰填谷,消息先入队,消费者按自己的节奏处理 |
| 可靠性要求高 | 消息持久化 + 重试机制,保证不丢、不重复 |
| 广播/分发需求 | 一条消息可以被多个服务各自消费(发布-订阅模式) |
二、核心概念速览
在深入代码之前,先把几个关键术语对齐:
- Producer(生产者):发送消息的一方,把消息投递到队列里。
- Consumer(消费者):接收并处理消息的一方,从队列里取消息。
- Broker(代理/中间件):消息队列服务端,负责存储和转发消息。
- Topic / Queue:消息的分类目的地。Topic 是发布-订阅模式(一条消息多个消费者都能收到),Queue 是点对点模式(一条消息只被一个消费者消费)。
- Partition / 分区:为了水平扩展,一个 Topic 可以分成多个 Partition,分布在不同 Broker 上。
- Offset(偏移量):消费者在 Partition 中的消费位置,用于记录"消费到哪了"。
- ACK(确认机制):消费者处理完消息后向 Broker 发送确认,Broker 才认为消息消费成功。
消息投递语义
| 语义 | 含义 | 适用场景 |
|---|---|---|
| At Most Once | 最多一次,可能丢 | 日志、监控指标等容忍丢失的场景 |
| At Least Once | 至少一次,可能重复 | 大多数业务场景,配合幂等处理 |
| Exactly Once | 精确一次 | 事务性场景,实现成本高 |
实践中最常用的是 At Least Once + 消费端幂等,因为 Exactly Once 的实现通常有性能代价。
三、主流消息队列对比
Java 生态中最常用的四种消息队列:
3.1 总览对比
| 特性 | Kafka | RocketMQ | RabbitMQ | ActiveMQ |
|---|---|---|---|---|
| 开发语言 | Scala/Java | Java | Erlang | Java |
| 吞吐量 | 极高(百万级/s) | 高(十万级/s) | 中(万级/s) | 中低 |
| 延迟 | 毫秒级 | 毫秒级 | 微秒级 | 毫秒级 |
| 消息顺序性 | Partition 内有序 | 严格顺序 | 不保证 | 不保证 |
| 消息可靠性 | 高 | 非常高 | 高 | 中 |
| 事务消息 | 支持(较重) | 原生支持 | 不支持 | 支持 |
| 延迟消息 | 不原生支持 | 原生支持 | 插件支持 | 支持 |
| 消息回溯 | 支持(按 offset) | 支持(按时间) | 不支持 | 不支持 |
| 运维复杂度 | 中 | 中 | 低 | 低 |
| 适用场景 | 日志、大数据、流处理 | 电商、金融业务消息 | 企业应用、复杂路由 | 轻量级、JMS 兼容 |
3.2 选型建议
如果你的场景是:
├── 日志收集 / 大数据流处理 / 高吞吐 → Kafka
├── 电商订单 / 金融交易 / 需要事务消息和延迟消息 → RocketMQ
├── 复杂路由 / 低延迟 / 中小规模 → RabbitMQ
└── 轻量级 / 快速上手 / JMS 标准兼容 → ActiveMQ
在国内 Java 后端,Kafka 和 RocketMQ 是绝对主流,下面重点讲这两个。
四、Kafka 实战
4.1 引入依赖
<!-- pom.xml -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.6.1</version>
</dependency>
如果使用 Spring Boot:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>3.1.5</version>
</dependency>
4.2 生产者示例
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class KafkaProducerDemo {
public static void main(String[] args) {
Properties props = new Properties();
// Broker 地址列表
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// 消息 key 的序列化器
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
// 消息 value 的序列化器
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
// 确认机制:all 表示所有副本都确认才算发送成功
props.put(ProducerConfig.ACKS_CONFIG, "all");
// 发送失败时的重试次数
props.put(ProducerConfig.RETRIES_CONFIG, 3);
// 批量发送大小(字节),适当增大可提高吞吐
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// 批量等待时间(毫秒),与 batch.size 配合使用
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 10; i++) {
// 参数:topic, key, value
// 同一个 key 总会路由到同一个 partition,保证相关消息有序
ProducerRecord<String, String> record =
new ProducerRecord<>("order-topic", "order-key-" + i, "订单消息内容-" + i);
// 异步发送,带回调
producer.send(record, (metadata, exception) -> {
if (exception == null) {
System.out.printf("消息发送成功: topic=%s, partition=%d, offset=%d%n",
metadata.topic(), metadata.partition(), metadata.offset());
} else {
System.err.println("消息发送失败: " + exception.getMessage());
}
});
}
producer.close(); // close 会等待所有未完成的发送完成
}
}
关键参数解读:
acks=all:消息写入所有 ISR 副本后才返回成功,最高可靠性。key的作用:Kafka 根据 key 做哈希分区,保证同一个 key 的消息总是进入同一个 Partition,从而保证同一业务实体的消息有序(比如同一个订单 ID 的消息)。
4.3 消费者示例
import org.apache.kafka.clients.consumer.*;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class KafkaConsumerDemo {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
// 消费者组 ID —— 同一组的消费者共同消费 topic,不同组各自独立消费
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-consumer-group");
// 自动提交 offset 关闭,改用手动提交,更可控
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
// 从最早的消息开始消费(可选: latest, earliest, none)
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("order-topic"));
try {
while (true) {
// poll 拉取消息,参数是最大等待时间
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息: partition=%d, offset=%d, key=%s, value=%s%n",
record.partition(), record.offset(), record.key(), record.value());
// TODO: 在这里处理业务逻辑
// 处理成功后手动提交 offset
// 如果处理失败,不提交 offset,下次 poll 会重新拉到这条消息
}
// 批量处理完后手动提交
consumer.commitSync();
}
} finally {
consumer.close();
}
}
}
4.4 消费端幂等设计
由于 At Least Once 语义下消息可能重复投递,消费端必须做幂等处理:
/**
* 幂等消费示例:用 Redis 记录已处理消息的 ID
*/
public class IdempotentConsumer {
private final RedisTemplate<String, String> redisTemplate;
private final OrderService orderService;
public void consumeMessage(String messageId, String messageBody) {
// 1. 检查是否已处理过
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent("msg:processed:" + messageId, "1", Duration.ofDays(7));
if (Boolean.TRUE.equals(isNew)) {
// 2. 首次处理,执行业务逻辑
try {
orderService.process(messageBody);
} catch (Exception e) {
// 3. 处理失败,删除标记,允许重试
redisTemplate.delete("msg:processed:" + messageId);
throw e;
}
} else {
// 已处理过,跳过(重复消息)
System.out.println("重复消息,跳过: " + messageId);
}
}
}
五、RocketMQ 实战
5.1 引入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>5.2.0</version>
</dependency>
5.2 生产者示例
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.client.producer.SendResult;
public class RocketMQProducerDemo {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
producer.setNamesrvAddr("localhost:9876"); // NameServer 地址
producer.setRetryTimesWhenSendFailed(3); // 同步发送失败重试次数
producer.start();
for (int i = 0; i < 10; i++) {
Message msg = new Message(
"OrderTopic", // Topic
"TagA", // Tag:用于消息过滤
("订单消息-" + i).getBytes() // 消息体
);
msg.setKeys("ORDER_KEY_" + i); // 业务唯一标识,方便消息追踪
// 同步发送:发送后等待 Broker 确认
SendResult result = producer.send(msg);
System.out.printf("发送结果: msgId=%s, status=%s%n",
result.getMsgId(), result.getSendStatus());
}
producer.shutdown();
}
}
5.3 事务消息——RocketMQ 的杀手锏
事务消息解决的是本地事务和消息发送的原子性问题。比如"扣减库存 + 发送订单消息",要么都成功,要么都失败。
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.Message;
public class TransactionProducerDemo {
public static void main(String[] args) throws Exception {
TransactionMQProducer producer = new TransactionMQProducer("tx_producer_group");
producer.setNamesrvAddr("localhost:9876");
// 设置事务监听器:定义"执行本地事务"和"事务回查"两个回调
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 在这里执行本地事务(如扣库存、写订单)
try {
// dbService.deductStock(orderId);
return LocalTransactionState.COMMIT_MESSAGE; // 本地事务成功 → 提交消息
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE; // 失败 → 回滚消息
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 事务回查:Broker 长时间没收到确认时,主动问"你那个本地事务到底成功了没?"
// 根据业务状态返回 COMMIT / ROLLBACK / UNKNOWN
String orderId = msg.getKeys();
boolean success = checkOrderStatus(orderId); // 查数据库
return success ? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.ROLLBACK_MESSAGE;
}
});
producer.start();
Message msg = new Message("OrderTopic", "createOrder", "订单数据".getBytes());
msg.setKeys("ORDER_001");
// 发送事务消息
producer.sendMessageInTransaction(msg, null);
// producer.shutdown(); // 实际应用中不要急着关闭
}
private static boolean checkOrderStatus(String orderId) {
// 查询本地数据库,判断订单是否创建成功
return true; // 简化示例
}
}
事务消息的执行流程:
1. Producer 发送"半消息"(消费者暂时看不到)
2. 半消息发送成功 → 执行本地事务
3. 本地事务成功 → 向 Broker 发送 COMMIT,消息变为可见
本地事务失败 → 向 Broker 发送 ROLLBACK,消息被丢弃
4. 如果 Producer 没有返回 COMMIT/ROLLBACK(比如宕机了)
→ Broker 定期发起事务回查,调用 checkLocalTransaction()
5.4 消费者示例
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class RocketMQConsumerDemo {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("OrderTopic", "TagA || TagB"); // 订阅 Topic + Tag 过滤
// 注册消息监听器
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.printf("收到消息: msgId=%s, body=%s%n",
msg.getMsgId(), new String(msg.getBody()));
// 处理业务逻辑
try {
processOrder(new String(msg.getBody()));
} catch (Exception e) {
// 返回 RECONSUME_LATER,消息会稍后重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("消费者已启动");
}
private static void processOrder(String body) {
System.out.println("处理订单: " + body);
}
}
六、Spring Boot 整合方案
在实际项目中,我们通常不会直接用原生客户端,而是通过 Spring Boot 整合。
6.1 Spring Boot + Kafka
配置(application.yml):
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
acks: all
retries: 3
consumer:
group-id: order-consumer-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
enable-auto-commit: false
auto-offset-reset: earliest
listener:
ack-mode: manual # 手动提交 offset
生产者:
@Service
public class OrderMessageProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void sendOrderMessage(String orderId, String orderJson) {
// send(topic, key, value) —— key 用于分区路由
kafkaTemplate.send("order-topic", orderId, orderJson)
.addCallback(
result -> System.out.println("发送成功: " + result.getRecordMetadata().offset()),
ex -> System.err.println("发送失败: " + ex.getMessage())
);
}
}
消费者:
@Component
public class OrderMessageConsumer {
@Autowired
private OrderService orderService;
@KafkaListener(topics = "order-topic", groupId = "order-consumer-group")
public void handleOrderMessage(ConsumerRecord<String, String> record,
Acknowledgment ack) {
try {
String orderId = record.key();
String orderJson = record.value();
System.out.printf("处理订单: key=%s, value=%s, partition=%d%n",
orderId, orderJson, record.partition());
// 业务处理
orderService.process(orderJson);
// 处理成功,手动提交 offset
ack.acknowledge();
} catch (Exception e) {
// 不调用 ack.acknowledge(),消息会在下次 poll 时重新拉取
log.error("处理消息失败,将重试", e);
// 注意:这里不要 ack,让 Kafka 重投
}
}
}
6.2 Spring Boot + RocketMQ
配置(application.yml):
rocketmq:
name-server: localhost:9876
producer:
group: order_producer_group
send-message-timeout: 3000
retry-times-when-send-failed: 3
生产者:
@Service
public class RocketMQOrderProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void sendOrder(String orderId, String orderJson) {
// destination 格式: topic:tag
rocketMQTemplate.convertAndSend("OrderTopic:TagA", orderJson);
}
// 同步发送,带返回值
public SendResult sendOrderSync(String orderJson) {
return rocketMQTemplate.syncSend("OrderTopic:TagA", orderJson);
}
}
消费者:
@Component
@RocketMQMessageListener(
topic = "OrderTopic",
consumerGroup = "order_consumer_group",
selectorExpression = "TagA || TagB" // Tag 过滤
)
public class RocketMQOrderConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("收到订单消息: " + message);
// 处理业务逻辑
// 如果抛异常,RocketMQ 会自动重试
}
}
七、生产环境最佳实践
7.1 消息不丢失的三道防线
消息从生产到消费,经过三个环节,每个环节都可能丢消息:
Producer → Broker(存储) → Consumer
| 环节 | 丢消息原因 | 防护措施 |
|---|---|---|
| Producer → Broker | 网络异常、Broker 宕机 | acks=all + 重试 + 同步发送或回调确认 |
| Broker 存储 | 磁盘故障、副本不足 | 多副本(replication factor ≥ 3)+ ISR 机制 |
| Broker → Consumer | 消费者处理失败但已提交 offset | 手动提交 offset,处理成功后才提交 |
7.2 消息不重复——消费端幂等
消息队列保证"至少一次"投递,重复是常态。幂等方案:
// 方案一:Redis 去重表(推荐)
public boolean processIdempotent(String messageId, Runnable businessLogic) {
Boolean first = redis.opsForValue()
.setIfAbsent("msg:" + messageId, "1", Duration.ofDays(7));
if (Boolean.TRUE.equals(first)) {
businessLogic.run();
return true;
}
return false; // 重复消息
}
// 方案二:数据库唯一索引
// 消息处理前先插入 msg_record 表(message_id 唯一索引)
// 插入成功 → 首次处理;插入失败(DuplicateKeyException)→ 重复消息
// 方案三:业务状态判断
// 比如订单消息,先查订单状态,如果已经是"已支付",就跳过
7.3 消息顺序性保证
有些业务要求消息按顺序处理(比如同一订单的"创建→支付→发货"):
保证顺序的核心:同一业务实体的消息路由到同一个 Partition/Queue
// Kafka:用 orderId 作为 key,自动路由到同一 partition
producer.send(new ProducerRecord<>("order-topic", orderId, message));
// RocketMQ:用 MessageQueueSelector 手动选择队列
producer.send(msg, (mqs, m, arg) -> {
int index = Math.abs(arg.hashCode()) % mqs.size();
return mqs.get(index);
}, orderId);
注意:顺序消费会降低并行度,只对真正需要顺序的 Topic 使用。
7.4 消息积压处理
当消费速度跟不上生产速度,消息会积压。应对策略:
- 临时扩容消费者:增加消费者实例数(注意:Kafka 中消费者数不能超过 Partition 数,否则多余的消费者闲置)。
- 增加 Partition:Kafka 可以动态增加 Partition(但会破坏 key 的分区路由)。
- 批量消费:一次 poll 多条消息,批量处理。
- 降级生产:通知上游限流或降级。
- 死信队列:多次重试仍失败的消息转入死信队列,人工介入处理。
7.5 监控指标
生产环境必须监控的关键指标:
| 指标 | 含义 | 告警阈值建议 |
|---|---|---|
| 消息积压量(Lag) | 未消费消息数 | > 10000 |
| 生产 TPS | 每秒发送消息数 | 根据容量评估 |
| 消费 TPS | 每秒消费消息数 | 应与生产 TPS 匹配 |
| 消息延迟 | 从发送到被消费的时间 | > 5s |
| Broker 磁盘使用率 | 存储空间占用 | > 80% |
| 消费失败率 | 消费异常比例 | > 1% |
八、架构模式示例
8.1 异步下单场景
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
┌──────▼───────┐
│ Order Service│ ← 创建订单(写DB),发消息到 MQ
└──────┬───────┘
│ send message
┌──────▼───────┐
│ Message Queue│ (Kafka / RocketMQ)
└──┬───┬───┬──┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Inventory │ │ Logistics│ │ Notify │
│ Service │ │ Service │ │ Service │
└──────────┘ └──────────┘ └──────────┘
扣库存 生成物流单 发短信/推送
核心代码片段:
// Order Service —— 下单后发消息
@Transactional
public OrderResult createOrder(OrderRequest request) {
// 1. 创建订单(本地事务)
Order order = orderRepository.save(request.toOrder());
// 2. 发送消息通知下游服务(异步)
// 用事务消息保证"订单创建"和"消息发送"的原子性
rocketMQTemplate.sendMessageInTransaction(
"OrderTopic:OrderCreated",
JSON.toJSONString(order),
order.getId()
);
// 3. 立即返回,不等下游处理
return OrderResult.success(order.getId());
}
// Inventory Service —— 消费消息,扣库存
@RocketMQMessageListener(topic = "OrderTopic", consumerGroup = "inventory_group")
public class InventoryConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
Order order = JSON.parseObject(message, Order.class);
inventoryService.deduct(order.getProductId(), order.getQuantity());
}
}
8.2 削峰填谷场景
秒杀活动中,瞬间涌入大量请求:
// 秒杀接口:请求入队,快速返回"排队中"
@PostMapping("/seckill")
public Result seckill(@RequestBody SeckillRequest request) {
// 不直接处理,而是放入消息队列
kafkaTemplate.send("seckill-topic", request.getUserId(), JSON.toJSONString(request));
return Result.success("正在排队处理,请稍后查看结果");
}
// 消费者:按固定速率消费,保护数据库
@KafkaListener(topics = "seckill-topic", groupId = "seckill-group")
public void processSeckill(ConsumerRecord<String, String> record, Acknowledgment ack) {
SeckillRequest request = JSON.parseObject(record.value(), SeckillRequest.class);
seckillService.execute(request); // 实际扣库存、生成订单
ack.acknowledge();
}
九、常见踩坑记录
坑 1:Kafka 消费者数 > Partition 数
现象:启动了 8 个消费者实例,但只有 4 个在消费,另外 4 个空闲。
原因:Topic 只有 4 个 Partition,Kafka 一个 Partition 只能被同一组的一个消费者消费。
解决:消费者实例数 ≤ Partition 数。需要更多消费者就先增加 Partition。
坑 2:消息体过大
现象:Kafka 默认消息体限制 1MB,超过会报 RecordTooLargeException。
解决:
- 方案一:调大 max.message.bytes(不推荐,大消息影响性能和内存)
- 方案二:消息体存到 OSS/Redis,MQ 里只传引用 URL(推荐)
坑 3:消费者 rebalance 导致消费暂停
现象:消费者组成员变化(上下线)时触发 rebalance,期间所有消费者暂停消费。
原因:Kafka 的协调机制,rebalance 期间无法消费。
解决:
- 使用静态成员(ConsumerConfig.GROUP_INSTANCE_ID_CONFIG)避免频繁 rebalance
- 合理设置 session.timeout.ms 和 heartbeat.interval.ms
- 升级到 Kafka 2.4+ 使用 Incremental Cooperative Rebalance 协议
坑 4:RocketMQ 消费者启动报 “No Route Data”
现象:消费者启动正常,但不消费消息,日志显示 "No route data of this topic"。
原因:Topic 没有创建,或者 NameServer 连接的地址不对。
解决:
- 确认 Topic 已创建(RocketMQ 默认不自动创建 Topic,需开启 autoCreateTopicEnable)
- 检查 NameServer 地址配置
十、总结
| 维度 | 建议 |
|---|---|
| 选型 | 日志/大数据选 Kafka,业务消息选 RocketMQ,轻量选 RabbitMQ |
| 可靠性 | acks=all + 多副本 + 手动提交 offset |
| 幂等 | 消费端用 Redis 去重表或数据库唯一索引 |
| 顺序 | 同 key 同 Partition,必要时用顺序消费模式 |
| 积压 | 扩容消费者、批量消费、死信队列兜底 |
| 监控 | 积压量、TPS、延迟、失败率,缺一不可 |
消息队列不是银弹——它引入了异步复杂度、消息丢失/重复风险、运维成本。但在分布式系统中,它带来的解耦、削峰、可靠性收益远大于成本。关键是理解每个参数背后的语义,而不是照抄配置。
希望这篇文章能帮你在实际项目中少踩坑、多写对。有问题欢迎留言讨论。
参考资料:Kafka 官方文档、RocketMQ 官方文档、Spring Kafka 参考文档
- 点赞
- 收藏
- 关注作者
评论(0)