文章目录

概述
引入 MQ 消息中间件最直接的目的是:做系统解耦合流量控制,追其根源还是为了解决互联网系统的高可用和高性能问题。
-
系统解耦:用 MQ 消息队列,可以隔离系统上下游环境变化带来的不稳定因素,实现服务的解耦,做到了系统的高可用。
-
流量控制:遇到秒杀等流量突增的场景,通过 MQ 还可以实现流量的“削峰填谷”的作用,可以根据下游的处理能力自动调节流量。
引入 MQ 消息中间件实现系统解耦,会影响系统之间数据传输的一致性。 在分布式系统中,如果两个节点之间存在数据同步,就会带来数据一致性的问题。同理,消息生产端和消息消费端的消息数据一致性问题(也就是如何确保消息不丢失)。
而引入 MQ 消息中间件解决流量控制, 会使消费端处理能力不足从而导致消息积压,这也是要解决的问题。
那面对“在使用 MQ 消息队列时,如何确保消息不丢失”这个问题时,首先,要分析其中有几个点,比如:
-
如何知道有消息丢失?
-
哪些环节可能丢消息?
-
如何确保消息不丢失?
消息中间件(MQ)在分布式系统中扮演着解耦、削峰填谷的重要角色,但消息丢失问题始终是高可用设计的核心挑战。
接下来我们将深入探讨消息丢失的根源,并提供一套完整的解决方案,涵盖生产端、存储端、消费端的关键设计,同时给出重复消费、消息积压等高频问题的应对策略。
一、消息丢失的三个核心环节
-
消息生产阶段: 从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 MQ Broker 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,这个阶段是不会出现消息丢失的。
-
消息存储阶段: 这个阶段一般会直接交给 MQ 消息中间件来保证,但是要了解它的原理,比如 Broker 会做副本,保证一条消息至少同步两个节点再返回 ack(这里涉及数据一致性原理)。
-
消息消费阶段: 消费端从 Broker 上拉取消息,只要消费端在收到消息后,不立即发送消费确认给 Broker,而是等到执行完业务逻辑后,再发送消费确认,也能保证消息的不丢失。
方案看似万无一失,每个阶段都能保证消息的不丢失,但在分布式系统中,故障不可避免,作为消费生产端,并不能保证 MQ 是不是弄丢了消息,消费者是否消费了消息,所以,本着 Design for Failure
的设计原则,还是需要一种机制,来 Check 消息是否丢失了。
1.1 生产阶段:消息如何可靠投递?
总体方案解决思路为:在消息生产端,给每个发出的消息都指定一个全局唯一 ID,或者附加一个连续递增的版本号,然后在消费端做对应的版本校验。
典型场景:网络闪断导致消息未到达Broker
解决方案:
// RocketMQ发送示例:同步发送+重试机制
public void sendMessage() {
try {
SendResult result = producer.send(msg, 3000); // 3秒超时
if (result.getSendStatus() != SendStatus.SEND_OK) {
// 记录日志并触发重试
}
} catch (Exception e) {
// 异常捕获,最多重试3次
retrySend(msg, maxRetries);
}
}
关键技术:
- 同步发送+ACK确认机制
- 失败重试策略(建议指数退避)
- 本地消息表(保障极端情况下的可靠性)
1.2 存储阶段:Broker如何确保数据不丢失?
Kafka核心配置:
# 确保消息写入所有ISR副本
acks=all
min.insync.replicas=2
# 刷盘策略(RocketMQ同理)
flush.messages=1 # 每条消息刷盘
存储机制对比:
中间件 | 持久化方式 | 复制机制 |
---|---|---|
Kafka | 分段日志+索引 | ISR同步复制 |
RocketMQ | CommitLog+ConsumeQueue | 主从异步复制 |
RabbitMQ | 镜像队列 | 镜像同步 |
1.3 消费阶段:如何避免处理失败导致丢失?
可靠消费流程:
消费者拉取消息 -> 执行业务逻辑 -> 提交消费位移
关键设计:
- 关闭自动提交(enable.auto.commit=false)
- 业务处理完成后手动提交
- 异常处理中记录失败消息
二、消息完整性检测:如何发现消息丢失?
2.1 全局唯一追踪体系
实现方案:
# 消息轨迹追踪表设计
CREATE TABLE msg_trace (
msg_id VARCHAR(64) PRIMARY KEY,
status ENUM('sent', 'consumed', 'failed'),
create_time DATETIME,
update_time DATETIME,
retry_count INT DEFAULT 0
);
检测手段:
- 定时任务扫描超时未消费消息
- 消费者上报消费状态(HTTP回调或MQ通知)
- 全链路监控(Prometheus+Grafana监控大盘)
2.2 消息轨迹追踪实现
// 拦截器实现消息轨迹记录
public class MsgTraceInterceptor implements ProducerInterceptor {
@Override
public Message beforeSend(Message message) {
String traceId = UUID.randomUUID().toString();
message.putProperty("trace_id", traceId);
// 写入追踪表(状态=sent)
traceDao.insert(traceId, "sent");
return message;
}
}
三、重复消费的终极解决方案:幂等设计
如何解决消费端幂等性问题(幂等性,就是一条命令,任意多次执行所产生的影响均与一次执行的影响相同),只要消费端具备了幂等性,那么重复消费消息的问题也就解决。
最简单的实现方案,就是在数据库中建一张消息日志表, 这个表有两个字段:消息 ID 和消息执行状态。这样,我们消费消息的逻辑可以变为:在消息日志表中增加一条消息记录,然后再根据消息记录,异步操作更新。
因为我们每次都会在插入之前检查是否消息已存在,所以就不会出现一条消息被执行多次的情况,这样就实现了一个幂等的操作。当然,基于这个思路,不仅可以使用关系型数据库,也可以通过 Redis 来代替数据库实现唯一约束的方案。
想要解决“消息丢失”和“消息重复消费”的问题,有一个前提条件就是要实现一个全局唯一 ID 生成的技术方案。
3.1 幂等性实现三要素
- 唯一标识:msg_id + 业务唯一键
- 状态检查:前置状态验证
- 原子操作:数据库唯一约束
3.2 典型幂等方案对比
方案 | 适用场景 | 实现复杂度 | 性能影响 |
---|---|---|---|
数据库唯一索引 | 低频写操作 | 低 | 中 |
Redis原子操作 | 高频写操作 | 中 | 低 |
版本号控制 | 状态流转类业务 | 高 | 中 |
在分布式系统中,全局唯一 ID 生成的实现方法有数据库自增主键、UUID、Redis,Twitter-Snowflake 算法,我总结了几种方案的特点,可以参考下。
无论哪种方法,如果你想同时满足简单、高可用和高性能,就要有取舍,所以站在实际的业务中,你的选型所考虑的平衡点是什么。在业务中比较倾向于选择 Snowflake 算法,在项目中也进行了一定的改造,主要是让算法中的 ID 生成规则更加符合业务特点,以及优化诸如时钟回拨等问题
实战案例:扣减幂等实现
UPDATE user_beans
SET beans = beans - 100
WHERE user_id = 123
AND beans >= 100
AND version = 5 -- 乐观锁控制
四、消息积压的应急处理与根治方案
因为消息发送之后才会出现积压的问题,所以和消息生产端没有关系,又因为绝大部分的消息队列单节点都能达到每秒钟几万的处理能力,相对于业务逻辑来说,性能不会出现在中间件的消息存储上面。毫无疑问,出问题的肯定是消息消费阶段,那么从消费端入手.
如果是线上突发问题,要临时扩容,增加消费端的数量,与此同时,降级一些非核心的业务。通过扩容和降级承担流量,优先处理应急问题。
其次,才是排查解决异常问题,如通过监控,日志等手段分析是否消费端的业务逻辑代码出现了问题,优化消费端的业务处理逻辑
4.1 线上突发积压应对策略
-
紧急扩容:
- 消费者实例数 = 分区数 * 2(需同步扩容分区)
- 示例:Kafka分区扩容命令
kafka-topics --alter --topic order_topic --partitions 12
比如在 Kafka 中,一个 Topic 可以配置多个 Partition(分区),数据会被写入到多个分区中,但在消费的时候,Kafka 约定一个分区只能被一个消费者消费,Topic 的分区数量决定了消费的能力,所以,可以通过增加分区来提高消费者的处理能力。
-
降级非核心业务:
- 关闭报表生成、数据分析等异步任务
-
动态限流:
// RocketMQ消费者限流设置 consumer.setPullThresholdForQueue(1000); // 单队列堆积阈值
4.2 性能优化方向
- 消费端优化:
- 批量处理(Kafka的max.poll.records调优)
- 异步处理+本地队列
- 存储层优化:
- Kafka开启压缩传输(compression.type=lz4)
- RocketMQ优化CommitLog写入模式
五、Kafka高性能设计精髓
5.1 写入性能优化
- 顺序写磁盘:通过分段日志结构实现600MB/s+的写入吞吐
- 零拷贝技术:sendfile系统调用减少内核态拷贝
- 页缓存策略:通过OS缓存实现高效读写
5.2 消费性能优化
- 稀疏索引:在10万条/秒吞吐下保持毫秒级检索
- 跳跃表查找:O(log n)时间复杂度定位消息
- 批量拉取机制:默认每次拉取500条消息
六、设计原则总结
- 故障假定原则:Design for Failure,每个环节都要考虑失败场景
- 端到端检测:建立全链路监控体系(生产→存储→消费)
- 幂等先行:所有消费逻辑默认需要支持重复处理
- 弹性设计:消费者数量与分区数保持动态平衡
通过以上方案的实施,不仅能解决消息丢失的核心问题,还能构建起高可靠、高性能的消息处理系统。在实际系统设计中,需要根据业务特点选择合适的中间件,并针对性地调整参数配置,才能达到最优效果。