每日Java面试场景题知识点之-RabbitMQ消息重复消费问题
场景描述
在分布式系统中,由于网络问题或消费者重启,同一条消息可能被多次处理,导致业务逻辑重复执行。这种情况在订单处理、支付回调、库存扣减等关键业务场景中尤为严重,可能造成重复下单、重复支付、库存超卖等问题。
技术栈使用
1. RabbitMQ配置类
java
@Configuration
public class RabbitMQConfig {
@Bean
public Queue orderQueue() {
return new Queue("order.queue", true, false, false);
}
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.exchange");
}
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue())
.to(orderExchange())
.with("order.create");
}
}
2. 消息生产者
java
@Service
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendOrder(OrderMessage message) {
// 设置消息确认机制
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
// 消息发送失败,记录日志并重试
log.error("消息发送失败: {}", cause);
// 这里可以实现重试逻辑
}
});
// 发送消息
rabbitTemplate.convertAndSend("order.exchange", "order.create", message);
}
}
解决方案
1. 幂等性设计
在业务逻辑层面实现幂等性,确保同一条消息多次处理不会产生不同结果。
java
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@RabbitListener(queues = "order.queue")
public void handleOrder(OrderMessage message) {
String messageId = message.getMessageId();
// 使用Redis检查消息是否已处理
Boolean processed = redisTemplate.opsForValue().setIfAbsent(
"message:processed:" + messageId,
"1",
24,
TimeUnit.HOURS
);
if (Boolean.TRUE.equals(processed)) {
// 处理订单逻辑
processOrder(message);
} else {
log.info("消息已处理过,跳过处理: {}", messageId);
}
}
private void processOrder(OrderMessage message) {
// 实际业务逻辑
// 1. 检查订单是否已存在
// 2. 创建订单
// 3. 扣减库存
// 4. 发送通知
}
}
2. 数据库唯一约束
在数据库层面添加唯一约束,防止重复数据插入。
java
@Entity
@Table(name = "orders", uniqueConstraints = {
@UniqueConstraint(columnNames = {"order_id", "message_id"})
})
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderId;
private String messageId;
private BigDecimal amount;
private String status;
// 其他字段
}
3. 消息去重表
创建专门的消息去重表,记录已处理的消息ID。
java
@Entity
@Table(name = "message_deduplication")
public class MessageDeduplication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String messageId;
private String messageType;
private Long createTime;
private Long expireTime;
}
4. 手动确认机制
确保消费者在处理完成后再确认消息,避免消息重复投递。
java
@Service
public class OrderService {
@Autowired
private MessageDeduplicationRepository deduplicationRepository;
@RabbitListener(queues = "order.queue")
public void handleOrder(OrderMessage message, Channel channel,
@Headers Map<String, Object> headers) throws IOException {
try {
// 检查消息是否已处理
if (isMessageProcessed(message.getMessageId())) {
log.info("消息已处理过,跳过处理: {}", message.getMessageId());
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
return;
}
// 处理订单逻辑
processOrder(message);
// 记录消息处理状态
recordMessageProcessed(message.getMessageId());
// 手动确认消息
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
} catch (Exception e) {
log.error("处理订单消息失败: {}", e.getMessage(), e);
// 根据业务需求决定是否重新投递
channel.basicNack((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false, false);
}
}
private boolean isMessageProcessed(String messageId) {
return deduplicationRepository.existsByMessageId(messageId);
}
private void recordMessageProcessed(String messageId) {
MessageDeduplication record = new MessageDeduplication();
record.setMessageId(messageId);
record.setMessageType("order");
record.setCreateTime(System.currentTimeMillis());
record.setExpireTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000);
deduplicationRepository.save(record);
}
}
最佳实践
- 消息ID唯一性:确保每条消息都有唯一的ID,可以使用UUID或业务相关的唯一标识。
- 过期时间设置:为去重记录设置合理的过期时间,避免数据无限增长。
- 异常处理:完善异常处理机制,确保消息在处理失败时能够正确重试或进入死信队列。
- 监控告警:建立消息处理的监控机制,及时发现和处理异常情况。
- 性能优化:合理使用Redis等缓存技术,提高消息去重的性能。
总结
RabbitMQ消息重复消费问题是在分布式系统中常见的技术挑战。通过幂等性设计、数据库唯一约束、消息去重表和手动确认机制等多种手段,可以有效解决消息重复消费的问题。在实际项目中,需要根据具体的业务场景和技术架构选择合适的解决方案,确保系统的稳定性和数据的一致性。
感谢读者观看!