RocketMQ 是一个分布式消息中间件,广泛应用于高并发、高吞吐的场景中。然而,在实际使用中,由于网络抖动、服务重启等原因,可能会导致消息重复消费的问题。以下是我总结日常项目遇到的问题,将详细讲解 RocketMQ 消息重复消费的情况及对应的解决措施。
一、RocketMQ 消息重复消费的原因
1、网络异常
- 生产者发送消息后,由于网络问题未能及时收到 ACK 确认,生产者会重新发送消息。
2、Broker 故障或重启
- 如果 Broker 在处理消息时发生故障或重启,可能会导致部分消息未被正确标记为已消费,从而引发重复消费。
3、消费者宕机或重启
- 消费者在处理完消息后,还未向 Broker 发送消费成功的确认(ACK),就发生了宕机或重启,Broker 会重新投递该消息。
4、幂等性问题
- 某些业务逻辑本身不具备幂等性,即使消息只被消费一次,也可能因为业务逻辑的设计缺陷导致重复处理。
5、事务消息回查机制
- 在事务消息场景下,如果生产者提交消息的状态超时,Broker 可能会多次触发回查机制,导致消息被重复消费。
二、解决 RocketMQ 消息重复消费的措施
1. 确保业务逻辑的幂等性
- 幂等性是指同一操作被执行多次与执行一次的效果相同。这是解决消息重复消费的核心思想。
解决方式:
- 使用唯一标识符(如消息 ID 或业务主键)记录消息是否已被处理。
- 在数据库中维护一个“消息状态表”,记录每条消息的消费状态。
示例代码:
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;
@Service
@RocketMQMessageListener(topic = "test_topic", consumerGroup = "test_group")
public class MessageConsumer implements RocketMQListener<String> {
// 假设有一个数据库表 message_status(id, msgId, consumed)
private final MessageStatusRepository messageStatusRepository;
public MessageConsumer(MessageStatusRepository messageStatusRepository) {
this.messageStatusRepository = messageStatusRepository;
}
@Override
public void onMessage(String message) {
try {
// 提取消息中的唯一标识(例如:msgId)
String msgId = extractMsgId(message);
// 查询消息是否已经被消费
if (messageStatusRepository.existsByMsgId(msgId)) {
System.out.println("消息已消费,跳过处理: " + msgId);
return; // 已消费的消息直接返回
}
// 处理业务逻辑
processBusinessLogic(message);
// 标记消息为已消费
messageStatusRepository.save(new MessageStatus(msgId, true));
System.out.println("消息处理成功: " + msgId);
} catch (Exception e) {
e.printStackTrace();
System.err.println("消息处理失败: " + message);
}
}
private String extractMsgId(String message) {
// 根据业务规则提取消息 ID
return message.split(":")[0];
}
private void processBusinessLogic(String message) {
// 具体业务逻辑处理
System.out.println("处理业务逻辑: " + message);
}
}
数据库表设计:
CREATE TABLE message_status (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
msg_id VARCHAR(255) NOT NULL UNIQUE, -- 消息唯一标识
consumed BOOLEAN NOT NULL DEFAULT FALSE -- 是否已消费
);
2. 使用全局锁
- 如果业务场景允许,可以通过分布式锁(如 Redis 或 Zookeeper)确保同一消息不会被多个消费者同时处理。
解决方式:
- 在消费消息前,尝试获取分布式锁;如果获取失败,则说明该消息正在被其他消费者处理,当前消费者直接跳过。
示例代码:
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
@RocketMQMessageListener(topic = "test_topic", consumerGroup = "test_group")
public class DistributedLockMessageConsumer implements RocketMQListener<String> {
private final StringRedisTemplate redisTemplate;
public DistributedLockMessageConsumer(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void onMessage(String message) {
try {
// 提取消息中的唯一标识(例如:msgId)
String msgId = extractMsgId(message);
// 尝试获取分布式锁
String lockKey = "lock:" + msgId;
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, "LOCKED", 10, TimeUnit.SECONDS);
if (!lockSuccess) {
System.out.println("消息正在被其他消费者处理,跳过: " + msgId);
return; // 已有锁,跳过处理
}
// 处理业务逻辑
processBusinessLogic(message);
System.out.println("消息处理成功: " + msgId);
} catch (Exception e) {
e.printStackTrace();
System.err.println("消息处理失败: " + message);
} finally {
// 释放分布式锁
String msgId = extractMsgId(message);
String lockKey = "lock:" + msgId;
redisTemplate.delete(lockKey);
}
}
private String extractMsgId(String message) {
// 根据业务规则提取消息 ID
return message.split(":")[0];
}
private void processBusinessLogic(String message) {
// 具体业务逻辑处理
System.out.println("处理业务逻辑: " + message);
}
}
3. 使用 RocketMQ 的顺序消息
- 如果业务场景要求消息严格按照顺序处理,可以使用 RocketMQ 的顺序消息功能。顺序消息可以避免因乱序导致的重复消费问题。
解决方式:
- 配置顺序消息队列,确保同一队列的消息按顺序消费。
示例代码:
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class OrderlyMessageConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("orderly_topic", "*");
// 设置顺序消息监听器
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
for (MessageExt msg : msgs) {
try {
String msgId = new String(msg.getBody());
System.out.println("顺序消息处理: " + msgId);
// 处理业务逻辑
processBusinessLogic(msgId);
return org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus.SUCCESS;
} catch (Exception e) {
e.printStackTrace();
return org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
return org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();
System.out.println("顺序消息消费者启动成功...");
}
private static void processBusinessLogic(String msgId) {
// 具体业务逻辑处理
System.out.println("处理顺序消息业务逻辑: " + msgId);
}
}
4. 优化消费确认机制
- 确保消费者在处理完消息后,及时向 Broker 发送消费成功的确认(ACK)。如果消费者在处理过程中发生异常,应捕获异常并重试。
示例代码:
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class ReliableMessageConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("reliable_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("test_topic", "*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
try {
String msgId = new String(msg.getBody());
System.out.println("处理消息: " + msgId);
// 处理业务逻辑
processBusinessLogic(msgId);
return org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
e.printStackTrace();
return org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("可靠消息消费者启动成功...");
}
private static void processBusinessLogic(String msgId) {
// 具体业务逻辑处理
System.out.println("处理可靠消息业务逻辑: " + msgId);
}
}
反正给大家总结起来,就是:
- 幂等性:适用于大多数业务场景,是最通用的解决方案。
- 分布式锁:适用于对并发处理要求较高的场景。
- 顺序消息:适用于严格要求消息顺序的场景。
- 优化消费确认机制:适用于需要确保消费可靠性的场景。

6678

被折叠的 条评论
为什么被折叠?



