一、成因分析
1. 消息丢失
- 生产者未持久化消息,服务宕机丢失
- 消息队列未开启持久化
- 消费者收到消息但未处理完就宕机,消息被确认(ack)后丢失
- 网络异常、超时、队列满等
2. 重复消费
- 消费者处理超时/失败,消息被重新投递
- 消息未正确ack,队列自动重试
- 消息队列本身的“至少一次”投递语义
- 消费者业务未做幂等性处理
二、排查与定位
- 队列监控:消息堆积、未消费、消费失败、重试次数
- 日志分析:生产、消费、ack、nack、异常日志
- 消息ID追踪:每条消息唯一ID,便于排查丢失/重复
三、完整解决方案
1. 防止消息丢失
1.1 生产者端
- 消息持久化:RabbitMQ/Kafka等支持消息持久化
- 发送确认机制:RabbitMQ的publisher confirm、Kafka的acks
- 失败重试:发送失败自动重试
RabbitMQ生产者持久化示例(php-amqplib):
$msg = new AMQPMessage($data, ['delivery_mode' => 2]); // 2=持久化
$channel->basic_publish($msg, '', 'queue_name');
1.2 队列端
- 队列持久化:声明队列为持久化
- 高可用集群:多节点、主从、分区、副本
RabbitMQ持久化队列声明:
$channel->queue_declare('queue_name', false, true, false, false); // 第三个参数true=持久化
1.3 消费者端
- 手动ack:消费成功后再ack,失败不ack
- 消费失败nack/requeue:失败时消息重新入队
- 消费超时/宕机自动重投
RabbitMQ手动ack示例:
$channel->basic_consume('queue_name', '', false, false, false, false, function($msg) {
try {
// 业务处理
$channel->basic_ack($msg->delivery_info['delivery_tag']);
} catch (Exception $e) {
$channel->basic_nack($msg->delivery_info['delivery_tag'], false, true); // 重新入队
}
});
2. 防止重复消费
2.1 消费端幂等性设计
- 每条消息有唯一ID(如订单号、消息ID、UUID)
- 消费前先查幂等表/Redis,已处理过的消息直接跳过
- 业务操作本身要幂等(如扣款、发货、发券等)
PHP幂等消费示例:
$msgId = $msg['id'];
if ($redis->setnx("msg:consumed:$msgId", 1)) {
$redis->expire("msg:consumed:$msgId", 86400);
// 业务处理
} else {
// 已处理,跳过
}
2.2 消息唯一性与去重
- 生产者生成全局唯一消息ID
- 消费者/业务层用唯一ID去重
3. 消息重试与死信队列
- 消费失败/超时的消息自动重试,重试多次后进入死信队列(DLQ),人工或定时补偿
- RabbitMQ、Kafka等都支持DLQ
4. 监控与报警
- 监控消息堆积、消费失败、重试次数、死信队列
- 异常时自动报警,及时处理
四、实际优化流程举例
- 生产者、队列、消费者全链路开启持久化和确认机制
- 所有消息带唯一ID,消费端做幂等性校验
- 消费失败自动重试,重试多次后进死信队列
- 监控消息堆积、失败、死信,自动报警
- 定期清理/补偿死信消息
五、总结
- 消息丢失:靠持久化、确认机制、重试、集群高可用防止
- 重复消费:靠幂等性设计、唯一ID、业务去重防止
- 监控和报警:保证问题及时发现和补偿